diff --git a/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx b/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx index e2ee78d..d03f21e 100644 --- a/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx +++ b/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx @@ -1,5 +1,6 @@ import React, { useLayoutEffect, useMemo, useRef } from 'react'; -import { InstancedMesh, Object3D, Color, BufferGeometry, Float32BufferAttribute } from 'three'; +import { InstancedMesh, Object3D, Color, BufferGeometry, Float32BufferAttribute, ShaderMaterial } from 'three'; +import { useFrame } from '@react-three/fiber'; import { useMapStore } from '../store'; import { useUnitStore } from '../../Units/store'; import { VOXEL_SIZE, TILE_SIZE, TREE_VOXEL_SIZE, type VoxelType } from '../logic/terrain'; @@ -8,9 +9,275 @@ import type { ThreeEvent } from '@react-three/fiber'; const tempObject = new Object3D(); const tempColor = new Color(); +// ============= 水体动画 Shader ============= + +const waterVertexShader = /* glsl */ ` + // 顶点属性 + attribute vec3 color; + attribute float isCascade; // 是否是瀑布(垂直面):基于上下邻居判断 + attribute vec2 flowDirection; // 流向 (dx, dz) + + // 传递给片元着色器 + varying vec3 vColor; + varying vec3 vNormal; + varying vec3 vWorldPosition; + varying float vIsCascade; + varying vec2 vFlowDirection; + + void main() { + vColor = color; + vNormal = normalize(normalMatrix * normal); + vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; + vIsCascade = isCascade; + vFlowDirection = flowDirection; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const waterFragmentShader = /* glsl */ ` + uniform float uTime; + uniform float uCascadeSpeed; // 瀑布流速 + uniform float uRippleSpeed; // 涟漪速度 + uniform vec3 uHighlightColor; // 高光颜色(瀑布白沫) + + varying vec3 vColor; + varying vec3 vNormal; + varying vec3 vWorldPosition; + varying float vIsCascade; + varying vec2 vFlowDirection; + + // 简单的伪随机函数 + float random(vec2 st) { + return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123); + } + + // 噪声函数(用于涟漪和水花) + float noise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + + float a = random(i); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + + vec2 u = f * f * (3.0 - 2.0 * f); + + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; + } + + void main() { + vec3 finalColor = vColor; + float alpha = 0.75; + + // 判断面的朝向 - 使用更宽松的阈值 + bool isTopFace = vNormal.y > 0.7; // 顶面(水平) + bool isBottomFace = vNormal.y < -0.7; // 底面 + bool isVerticalFace = !isTopFace && !isBottomFace; // 所有非上下面都是垂直面 + + // ============= 瀑布效果(所有垂直面)============= + if (isVerticalFace) { + float worldY = vWorldPosition.y; + float worldX = vWorldPosition.x; + float worldZ = vWorldPosition.z; + + // 瀑布强度 + float cascadeStrength = vIsCascade > 0.5 ? 1.0 : 0.35; + float flowSpeed = uCascadeSpeed * 6.0; + + // ====== 1. 主水流 - 不规则的垂直流动 ====== + // 用噪声创建水流的"路径",让水流有粗细变化 + float pathNoise = noise(vec2(worldX * 5.0 + worldZ * 4.0, worldY * 2.0)); + float pathOffset = pathNoise * 0.8; // 水流横向摆动 + + // 主水流 - 不同位置有不同的速度 + float localSpeed = flowSpeed * (0.8 + pathNoise * 0.4); + float mainFlow = sin(worldY * 30.0 + uTime * localSpeed + pathOffset * 15.0) * 0.5 + 0.5; + + // 用噪声调制水流宽度(有些地方水多,有些地方水少) + float widthNoise = noise(vec2(worldX * 8.0 + worldZ * 6.0, worldY * 4.0 + uTime * 0.3)); + mainFlow *= (0.5 + widthNoise * 0.5); + + // ====== 2. 次级水流 - 更小更快的水珠 ====== + float secondaryNoise = noise(vec2(worldX * 12.0 - worldZ * 8.0, worldY * 6.0)); + float secondaryFlow = sin(worldY * 50.0 + uTime * flowSpeed * 1.3 + secondaryNoise * 10.0) * 0.5 + 0.5; + secondaryFlow *= secondaryNoise; // 用噪声掩码让它不连续 + + // ====== 3. 泡沫 - 随机分布的白色斑点 ====== + float foamNoise1 = noise(vec2(worldX * 15.0 + worldZ * 12.0, worldY * 18.0 + uTime * flowSpeed * 0.5)); + float foamNoise2 = noise(vec2(worldX * 20.0 - worldZ * 15.0, worldY * 25.0 + uTime * flowSpeed * 0.8)); + float foam = smoothstep(0.55, 0.8, foamNoise1) * smoothstep(0.4, 0.7, foamNoise2); + + // ====== 4. 飞溅水滴 - 稀疏的亮点 ====== + float dropletNoise = noise(vec2(worldX * 30.0 + worldZ * 25.0, worldY * 35.0 + uTime * flowSpeed * 1.5)); + float droplets = pow(dropletNoise, 6.0); // 非常稀疏 + + // ====== 5. 水帘的透明度变化 ====== + float thicknessNoise = noise(vec2(worldX * 6.0 + worldZ * 5.0, worldY * 3.0 + uTime * 0.2)); + + // ====== 组合所有效果 ====== + float waterFlow = mainFlow * 0.5 + secondaryFlow * 0.25 + foam * 0.2 + droplets * 0.05; + waterFlow *= cascadeStrength; + + // ====== 颜色混合 - 亮蓝青色调 ====== + vec3 deepWater = vec3(0.1, 0.45, 0.8); // 亮蓝 - 主体 + vec3 midWater = vec3(0.18, 0.55, 0.88); // 更亮蓝 - 水流 + vec3 lightWater = vec3(0.28, 0.65, 0.95); // 高亮蓝 - 高光 + vec3 foamBlue = vec3(0.4, 0.75, 0.98); // 泡沫亮蓝 + + // 基础是亮蓝色 + finalColor = deepWater; + // 水流区域变亮 + finalColor = mix(finalColor, midWater, mainFlow * 0.6); + // 次级水流更亮 + finalColor = mix(finalColor, lightWater, secondaryFlow * 0.4); + // 泡沫区域(亮蓝色调) + finalColor = mix(finalColor, foamBlue, foam * 0.35); + // 水滴闪烁(亮蓝色调) + finalColor = mix(finalColor, vec3(0.5, 0.85, 1.0), droplets * 0.25); + + // ====== 透明度 - 水薄的地方更透明 ====== + alpha = 0.6 + thicknessNoise * 0.15 + waterFlow * 0.2; + alpha = clamp(alpha, 0.55, 0.95); + } + // ============= 水平面(河流/湖泊)============= + else if (isTopFace) { + vec2 worldXZ = vWorldPosition.xz; + vec2 flowDir = vFlowDirection; + float flowStrength = length(flowDir); // 流向强度,用于区分河流和湖泊 + + // 颜色定义 + vec3 deepWater = vec3(0.12, 0.48, 0.82); + vec3 midWater = vec3(0.2, 0.58, 0.9); + vec3 lightWater = vec3(0.3, 0.68, 0.96); + vec3 highlightColor = vec3(0.42, 0.78, 0.98); + + float waterEffect = 0.0; + + // ============= 河流(有流向)============= + if (flowStrength > 0.01) { + float flowSpeed = uCascadeSpeed * 3.5; // 比瀑布慢 + + vec2 normalizedFlow = normalize(flowDir); + vec2 perpFlow = vec2(-normalizedFlow.y, normalizedFlow.x); + + // 沿流向和垂直方向的坐标 + float flowCoord = dot(worldXZ, normalizedFlow); + float perpCoord = dot(worldXZ, perpFlow); + + // ====== 1. 主水流 - 类似瀑布的流向动画 ====== + float pathNoise = noise(vec2(perpCoord * 5.0, flowCoord * 2.0)); + float pathOffset = pathNoise * 0.8; + + float localSpeed = flowSpeed * (0.8 + pathNoise * 0.4); + float mainFlow = sin(flowCoord * 25.0 - uTime * localSpeed + pathOffset * 12.0) * 0.5 + 0.5; + + // 用噪声调制水流宽度 + float widthNoise = noise(vec2(perpCoord * 8.0, flowCoord * 4.0 - uTime * 0.3)); + mainFlow *= (0.5 + widthNoise * 0.5); + + // ====== 2. 次级水流 - 更小更快的波纹 ====== + float secondaryNoise = noise(vec2(perpCoord * 12.0, flowCoord * 6.0)); + float secondaryFlow = sin(flowCoord * 45.0 - uTime * flowSpeed * 1.3 + secondaryNoise * 10.0) * 0.5 + 0.5; + secondaryFlow *= secondaryNoise; + + // ====== 3. 泡沫 - 沿流向移动的斑点 ====== + float foamNoise1 = noise(vec2(perpCoord * 15.0, flowCoord * 18.0 - uTime * flowSpeed * 0.5)); + float foamNoise2 = noise(vec2(perpCoord * 20.0, flowCoord * 25.0 - uTime * flowSpeed * 0.8)); + float foam = smoothstep(0.55, 0.8, foamNoise1) * smoothstep(0.4, 0.7, foamNoise2); + + // ====== 4. 水面闪烁 ====== + float sparkleNoise = noise(vec2(perpCoord * 30.0, flowCoord * 35.0 - uTime * flowSpeed * 1.2)); + float sparkle = pow(sparkleNoise, 6.0); + + // ====== 5. 水深变化 ====== + float depthNoise = noise(vec2(perpCoord * 6.0, flowCoord * 3.0 - uTime * 0.2)); + + // 组合效果 + waterEffect = mainFlow * 0.5 + secondaryFlow * 0.25 + foam * 0.2 + sparkle * 0.05; + + // 颜色混合 + finalColor = deepWater; + finalColor = mix(finalColor, midWater, mainFlow * 0.6); + finalColor = mix(finalColor, lightWater, secondaryFlow * 0.4); + finalColor = mix(finalColor, highlightColor, foam * 0.35); + finalColor = mix(finalColor, vec3(0.52, 0.85, 1.0), sparkle * 0.25); + + // 透明度 + alpha = 0.6 + depthNoise * 0.15 + waterEffect * 0.2; + alpha = clamp(alpha, 0.55, 0.85); + } + // ============= 湖泊(无流向)- 静态涟漪 ============= + else { + float rippleSpeed = uRippleSpeed * 2.0; + + // ====== 1. 涟漪 - 缓慢扩散的同心圆 ====== + float rippleOffset = noise(worldXZ * 0.3 + uTime * 0.05) * 5.0; + float ripple1 = sin(length(worldXZ * 3.0 + rippleOffset) - uTime * rippleSpeed * 0.3) * 0.5 + 0.5; + float ripple2 = sin(length((worldXZ + vec2(2.0, 1.5)) * 2.5) - uTime * rippleSpeed * 0.25) * 0.5 + 0.5; + float ripples = ripple1 * 0.5 + ripple2 * 0.5; + ripples *= noise(worldXZ * 1.5 + uTime * 0.08) * 0.4 + 0.6; + + // ====== 2. 微波 - 小的表面扰动 ====== + float microWave = noise(worldXZ * 8.0 + uTime * 0.15) * 0.5 + 0.5; + + // ====== 3. 闪烁高光 - 稀疏的亮点 ====== + float sparkleNoise = noise(worldXZ * 20.0 + vec2(uTime * 0.3, -uTime * 0.2)); + float sparkle = pow(sparkleNoise, 5.0); + + // 组合效果 + waterEffect = ripples * 0.5 + microWave * 0.3 + sparkle * 0.2; + + // 颜色混合 - 湖水更平静,颜色变化更小 + finalColor = deepWater; + finalColor = mix(finalColor, midWater, ripples * 0.4); + finalColor = mix(finalColor, lightWater, microWave * 0.25); + finalColor = mix(finalColor, vec3(0.52, 0.85, 1.0), sparkle * 0.2); + + // 透明度 - 湖水更稳定 + float depthNoise = noise(worldXZ * 2.0 + uTime * 0.1); + alpha = 0.7 + depthNoise * 0.08 + waterEffect * 0.1; + alpha = clamp(alpha, 0.65, 0.85); + } + } + // ============= 底面 ============= + else { + // 底面保持暗色 + finalColor *= 0.8; + alpha = 0.7; + } + + // 简单的环境光照(减弱以保留动画效果) + vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3)); + float diffuse = max(dot(vNormal, lightDir), 0.0) * 0.2 + 0.8; + finalColor *= diffuse; + + gl_FragColor = vec4(finalColor, alpha); + } +`; + interface VoxelLayerProps { // Update to include ix, iy, iz which are present in actual VoxelData - data: { x: number; y: number; z: number; color: string; heightScale?: number; isHighRes?: boolean; ix: number; iy: number; iz: number }[]; + data: { + x: number; + y: number; + z: number; + color: string; + heightScale?: number; + isHighRes?: boolean; + ix: number; + iy: number; + iz: number; + isMergedWater?: boolean; + mergedPositions?: Array<{ + ix: number; + iy: number; + iz: number; + flowDirection?: { dx: number; dz: number }; + }>; + flowDirection?: { dx: number; dz: number }; // 单个水体素的流向 + }[]; isHighRes: boolean; type: VoxelType; onClick?: (x: number, z: number) => void; @@ -20,21 +287,97 @@ interface VoxelLayerProps { * 专门用于水体的网格渲染器 * 使用 Greedy Meshing / Face Culling 思想,去除相邻水体之间的内部面 * 解决半透明材质叠加导致的颗粒感和反射问题 + * + * 动态效果: + * - 瀑布(垂直面):白色光带从上往下流动 + * - 河流(水平面):涟漪波动 + 流向光带 */ const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onClick }) => { const meshRef = useRef(null); + const materialRef = useRef(null); + + // 创建 Shader Material + const material = useMemo(() => { + return new ShaderMaterial({ + vertexShader: waterVertexShader, + fragmentShader: waterFragmentShader, + uniforms: { + uTime: { value: 0 }, + uCascadeSpeed: { value: 2.0 }, // 瀑布流速(调整为更自然的速度) + uRippleSpeed: { value: 2.0 }, // 涟漪速度 + uHighlightColor: { value: new Color('#e0f0ff') }, // 高光颜色(淡蓝白色) + }, + transparent: true, + depthWrite: false, + }); + }, []); + + // 每帧更新 time uniform - 实现动画 + useFrame((state) => { + if (material) { + material.uniforms.uTime.value = state.clock.elapsedTime; + } + }); const geometry = useMemo(() => { if (!data.length) return new BufferGeometry(); - // 1. 建立查找表 (Set of "ix|iy|iz") + // 1. 展开所有合并的水体素,构建完整的体素列表 + interface ExpandedVoxel { + x: number; + y: number; + z: number; + ix: number; + iy: number; + iz: number; + color: string; + heightScale: number; + flowDirection?: { dx: number; dz: number }; // 真实的水流方向 + } + + const expandedVoxels: ExpandedVoxel[] = []; + + data.forEach(v => { + if (v.isMergedWater && v.mergedPositions && v.mergedPositions.length > 0) { + // 展开合并的水体素(保留每个位置的流向) + v.mergedPositions.forEach(pos => { + expandedVoxels.push({ + x: pos.ix * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE), + y: pos.iy * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE), + z: pos.iz * (isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE), + ix: pos.ix, + iy: pos.iy, + iz: pos.iz, + color: v.color, + heightScale: v.heightScale || 1, + flowDirection: pos.flowDirection, // 使用该位置的真实流向 + }); + }); + } else { + // 普通水体素(直接添加,使用体素自身的流向) + expandedVoxels.push({ + x: v.x, + y: v.y, + z: v.z, + ix: v.ix, + iy: v.iy, + iz: v.iz, + color: v.color, + heightScale: v.heightScale || 1, + flowDirection: v.flowDirection, // 使用体素的流向 + }); + } + }); + + // 2. 建立查找表 (Set of "ix|iy|iz") const lookup = new Set(); - data.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`)); + expandedVoxels.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`)); const positions: number[] = []; const normals: number[] = []; const colors: number[] = []; - // const indices: number[] = []; // Not strictly needed if not sharing vertices + const isCascadeArr: number[] = []; // 瀑布标记 + const flowDirectionArr: number[] = []; // 流向 const voxelSize = isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE; const halfSize = voxelSize / 2; @@ -49,13 +392,30 @@ const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onCli { name: 'back', off: [0, 0, -1], normal: [0, 0, -1] }, ]; - data.forEach(v => { - const { x, y, z, ix, iy, iz, color, heightScale = 1 } = v; + expandedVoxels.forEach(v => { + const { x, y, z, ix, iy, iz, color, heightScale = 1, flowDirection } = v; const col = new Color(color); // 只有当 heightScale 为 1 时才能完美拼接 - // 如果有缩放,简单起见仍然按照满格判定邻居,或者忽略拼接优化(水通常是满格) const isFullBlock = Math.abs(heightScale - 1) < 0.01; + + // 检测是否是瀑布:垂直方向有邻居水体 + // 如果上方或下方有水体素,说明这是瀑布的一部分 + const hasAbove = lookup.has(`${ix}|${iy + 1}|${iz}`); + const hasBelow = lookup.has(`${ix}|${iy - 1}|${iz}`); + const isCascade = hasAbove || hasBelow; + + // 【使用真实的流向数据】 + let flowDx = 0; + let flowDz = 0; + + if (flowDirection) { + // 直接使用河流系统计算的真实流向(湖泊为 0,0) + flowDx = flowDirection.dx; + flowDz = flowDirection.dz; + } + // 注意:不再使用回退逻辑,因为真实流向已经在 terrain.ts 中正确设置 + // 湖泊的流向就是 (0,0),不需要回退 dirs.forEach(dir => { // 检查该方向是否有同类邻居 @@ -64,22 +424,11 @@ const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onCli // 如果没有邻居,渲染该面 if (!hasNeighbor) { - // 生成面的4个顶点 - // 中心点 (x, y, z) 是体素的中心吗? - // InstancedMesh logic: y + (hScale - 1) * (size/2). If hScale=1, y is center. - // VoxelData.y is world coordinate. - // Assuming voxel.y is the center Y coordinate of the block if scale is 1. - // Calculate actual center based on heightScale logic from VoxelLayer const centerY = y + (heightScale - 1) * (voxelSize / 2); const scaleY = voxelSize * heightScale; const halfY = scaleY / 2; - // Base vertex offsets for a unit cube centered at 0,0,0 - // Right: (+h, -h, +h), (+h, -h, -h), (+h, +h, -h), (+h, +h, +h) - // Need to be careful with coordinate system - // THREE.js: Y up. Right(x+), Left(x-), Top(y+), Bottom(y-), Front(z+), Back(z-) - // Vertices relative to center let v1, v2, v3, v4; @@ -125,6 +474,16 @@ const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onCli // Colors (same for all 6 verts) for(let k=0; k<6; k++) colors.push(col.r, col.g, col.b); + + // 瀑布标记:垂直面 + 有上下邻居 + const isVerticalFace = Math.abs(ny) < 0.3; + const cascadeValue = (isVerticalFace && isCascade) ? 1.0 : 0.0; + for(let k=0; k<6; k++) isCascadeArr.push(cascadeValue); + + // 流向 (same for all 6 verts) + for(let k=0; k<6; k++) { + flowDirectionArr.push(flowDx, flowDz); + } } }); }); @@ -133,6 +492,8 @@ const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onCli bufGeom.setAttribute('position', new Float32BufferAttribute(positions, 3)); bufGeom.setAttribute('normal', new Float32BufferAttribute(normals, 3)); bufGeom.setAttribute('color', new Float32BufferAttribute(colors, 3)); + bufGeom.setAttribute('isCascade', new Float32BufferAttribute(isCascadeArr, 1)); + bufGeom.setAttribute('flowDirection', new Float32BufferAttribute(flowDirectionArr, 2)); return bufGeom; }, [data, isHighRes]); @@ -141,19 +502,10 @@ const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onCli - - + /> ); } diff --git a/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts index 14f9d8a..de64b45 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts @@ -25,7 +25,7 @@ export interface DesertContext { stoneHeight: number[][]; stoneDepth: number[][]; stoneVariant: number[][]; - streamDepthMap: Map; // Added isCenter + streamDepthMap: Map; } // ============= 戈壁岩石结构生成 ============= diff --git a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts index df05910..3dca8ba 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts @@ -250,42 +250,1257 @@ const createMudIsland = (builder: VoxelBuilder, ox: number, oz: number, r: numbe // --- 3. Generators by Biome --- // ========================================= -// --- DESERT (Original + Expanded) --- -const createJointedSaguaroBase = (builder: VoxelBuilder, ox: number, oz: number, config: any) => { - const { h, r, armCount, armStyle } = config; - drawStem(builder, ox, 0, oz, h, r, C.gDark, C.gMid, true); - for (let xi = -Math.ceil(r); xi <= Math.ceil(r); xi++) { - for (let zi = -Math.ceil(r); zi <= Math.ceil(r); zi++) { - if (xi * xi + zi * zi <= r * r) builder.add(ox + xi, h, oz + zi, C.gDarkest); +// --- DESERT (Expanded & Enhanced - 统一 32x32 网格,高度增加 30-50%) --- + +// 仙人掌颜色调色板(使用更明亮的专用仙人掌色) +const CACTUS_COLORS = { + darkGreen: C.cactusGreen, // 仙人掌绿 #62944B(更亮) + midGreen: C.cactusLight, // 仙人掌亮绿 #86B56A + lightGreen: C.gLight, // 浅绿色高光 #8FB371 + shadow: C.gMid, // 阴影用中绿 #628A52(比原来的 gDarkest 亮很多) + rib: C.gDark, // 棱的颜色(稍暗一点形成对比) + spine: C.sand, // 刺的颜色 + flower: C.cactusFlower, // 仙人掌花 #FF7777 + fruitRed: C.red, // 红色果实 + fruitYellow: C.yellow, // 黄色果实 +}; + +// 参数化仙人掌配置接口 +interface SaguaroConfig { + height: number; // 主干高度 (32x32 网格单位) + radius: number; // 主干半径 + armCount: number; // 分枝数量 + armStyle: 'low' | 'balanced' | 'high' | 'wild' | 'goliath'; + hasFlower?: boolean; // 是否有花 + flowerColor?: string; // 花的颜色 +} + +// 核心参数化 Saguaro 仙人掌生成器 +const createParametricSaguaro = (builder: VoxelBuilder, ox: number, oz: number, config: SaguaroConfig) => { + const { height, radius, armCount, armStyle, hasFlower, flowerColor } = config; + + // 1. 绘制主干(带肋状纹理) + drawStem(builder, ox, 0, oz, height, radius, CACTUS_COLORS.darkGreen, CACTUS_COLORS.midGreen, true); + + // 2. 顶部封盖 + for (let xi = -Math.ceil(radius); xi <= Math.ceil(radius); xi++) { + for (let zi = -Math.ceil(radius); zi <= Math.ceil(radius); zi++) { + if (xi * xi + zi * zi <= radius * radius) { + builder.add(ox + xi, height, oz + zi, CACTUS_COLORS.shadow); + } } } + + // 3. 顶部花朵(可选) + if (hasFlower && Math.random() > 0.3) { + const fc = flowerColor || CACTUS_COLORS.flower; + builder.add(ox, height + 1, oz, fc); + builder.add(ox + 1, height + 1, oz, fc); + builder.add(ox - 1, height + 1, oz, fc); + builder.add(ox, height + 1, oz + 1, fc); + builder.add(ox, height + 1, oz - 1, fc); + } + + // 4. L形分枝 for (let i = 0; i < armCount; i++) { - let angle = (Math.PI * 2 * i) / armCount + (armStyle === 'wild' ? Math.random() : 0); - let startY = h * (armStyle === 'low' ? 0.3 : armStyle === 'high' ? 0.7 : 0.5) + (Math.random() - 0.5) * 4; - let elbowLen = (armStyle === 'wild' ? 4 + Math.random() * 4 : 3 + Math.random() * 2); - let armH = (armStyle === 'goliath' ? 6 : h * 0.4 + Math.random() * 4); - let armR = r * (armStyle === 'goliath' ? 0.6 : 0.7); - let cos = Math.cos(angle), sin = Math.sin(angle); - for (let d = r - 0.5; d <= r + elbowLen; d += 0.5) { - let ex = ox + cos * d, ez = oz + sin * d; - drawStem(builder, ex, startY, ez, 1.5, armR * 0.9, C.woodL, C.woodL, false); + const angle = (Math.PI * 2 * i) / armCount + (armStyle === 'wild' ? Math.random() * 0.5 : 0); + + // 分枝起始高度(根据风格调整) + let startY: number; + switch (armStyle) { + case 'low': startY = height * 0.3; break; + case 'high': startY = height * 0.7; break; + case 'goliath': startY = height * 0.4; break; + default: startY = height * 0.5; break; } - let tipX = ox + cos * (r + elbowLen), tipZ = oz + sin * (r + elbowLen); - drawStem(builder, tipX, startY, tipZ, armH, armR, C.gDark, C.gMid, false); + startY += (Math.random() - 0.5) * 4; + + // 水平肘部长度 + const elbowLen = armStyle === 'wild' ? 5 + Math.random() * 5 : 4 + Math.random() * 3; + + // 分枝向上部分高度 + const armH = armStyle === 'goliath' ? 8 : height * 0.4 + Math.random() * 5; + const armR = radius * (armStyle === 'goliath' ? 0.6 : 0.7); + + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + // 绘制水平肘部 + for (let d = radius - 0.5; d <= radius + elbowLen; d += 0.5) { + const ex = ox + cos * d; + const ez = oz + sin * d; + drawStem(builder, ex, startY, ez, 2, armR * 0.9, CACTUS_COLORS.midGreen, CACTUS_COLORS.midGreen, false); + } + + // 绘制向上的分枝 + const tipX = ox + cos * (radius + elbowLen); + const tipZ = oz + sin * (radius + elbowLen); + drawStem(builder, tipX, startY, tipZ, armH, armR, CACTUS_COLORS.darkGreen, CACTUS_COLORS.midGreen, false); + + // 分枝顶部封盖 for (let xi = -Math.ceil(armR); xi <= Math.ceil(armR); xi++) { for (let zi = -Math.ceil(armR); zi <= Math.ceil(armR); zi++) { - if (xi * xi + zi * zi <= armR * armR) builder.add(tipX + xi, startY + armH, tipZ + zi, C.gDarkest); + if (xi * xi + zi * zi <= armR * armR) { + builder.add(tipX + xi, startY + armH, tipZ + zi, CACTUS_COLORS.shadow); + } } } } }; -const createJointedClassic = (b: VoxelBuilder, x: number, z: number) => createJointedSaguaroBase(b, x, z, { h: 24, r: 2.2, armCount: 2, armStyle: 'balanced' }); -const createJointedCandelabra = (b: VoxelBuilder, x: number, z: number) => createJointedSaguaroBase(b, x, z, { h: 28, r: 3, armCount: 4, armStyle: 'low' }); -const createSaguaro = (b: VoxelBuilder, ox: number, oz: number) => { - drawStem(b, ox, 0, oz, 24, 2, C.gDark, C.gMid, true); - drawSphere(b, ox, 24, oz, 2, C.gDark, C.gMid, 0.6); + +// 仙人球(Barrel Cactus)生成器 +const createBarrelCactusNew = (builder: VoxelBuilder, ox: number, oz: number, size: 'small' | 'medium' | 'large' = 'medium') => { + // 尺寸配置(高度增加 40%) + let diameter: number, height: number, ribCount: number; + + switch (size) { + case 'small': + diameter = 6; // 原 4 -> 6 + height = 6; // 原 4 -> 6 + ribCount = 8; + break; + case 'medium': + diameter = 9; // 原 6 -> 9 + height = 9; // 原 6 -> 9 + ribCount = 12; + break; + case 'large': + diameter = 12; // 原 8 -> 12 + height = 12; // 原 8 -> 12 + ribCount = 16; + break; + } + + const radius = diameter / 2; + + // 为每条棱分配角度 + const ribAngles: number[] = []; + for (let i = 0; i < ribCount; i++) { + ribAngles.push((i / ribCount) * Math.PI * 2); + } + + // 逐层生成球形仙人球 + for (let y = 0; y < height; y++) { + const centerY = height / 2; + const dy = y - centerY + 0.5; + const layerRadiusSq = radius * radius - dy * dy; + + if (layerRadiusSq <= 0) continue; + + const layerRadius = Math.sqrt(layerRadiusSq); + + for (let x = -Math.ceil(layerRadius); x <= Math.ceil(layerRadius); x++) { + for (let z = -Math.ceil(layerRadius); z <= Math.ceil(layerRadius); z++) { + const distSq = x * x + z * z; + + if (distSq <= layerRadius * layerRadius) { + const angle = Math.atan2(z, x); + + // 找到最近的棱 + let minAngleDiff = Math.PI * 2; + for (const ribAngle of ribAngles) { + let angleDiff = Math.abs(angle - ribAngle); + if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; + minAngleDiff = Math.min(minAngleDiff, angleDiff); + } + + const ribThreshold = (Math.PI / ribCount) * 0.6; + const isOnRib = minAngleDiff < ribThreshold; + const distRatio = Math.sqrt(distSq) / layerRadius; + const shouldPlace = distRatio < 0.95 || (distRatio < 1.0 && isOnRib); + + if (shouldPlace) { + // 棱用稍暗的绿色,主体用明亮的仙人掌绿,形成自然的凹凸效果 + const color = isOnRib ? CACTUS_COLORS.rib : CACTUS_COLORS.midGreen; + builder.add(ox + x, y, oz + z, color); + } + } + } + } + } + + // 顶部装饰(花或刺) + const topY = height - 1; + const decorType = Math.random(); + + if (decorType < 0.4) { + // 小花(40%概率) + builder.add(ox, topY + 1, oz, CACTUS_COLORS.fruitYellow); + builder.add(ox + 1, topY + 1, oz, CACTUS_COLORS.flower); + builder.add(ox - 1, topY + 1, oz, CACTUS_COLORS.flower); + builder.add(ox, topY + 1, oz + 1, CACTUS_COLORS.flower); + builder.add(ox, topY + 1, oz - 1, CACTUS_COLORS.flower); + } else if (decorType < 0.7) { + // 红色果实(30%概率) + builder.add(ox, topY + 1, oz, CACTUS_COLORS.fruitRed); + } }; -// (Omitting some minor desert variations for brevity, keeping the impressive ones) + +// 双主干仙人掌 +const createDualTrunkCactusNew = (builder: VoxelBuilder, ox: number, oz: number) => { + // 主干1: 较高较粗(高度增加 40%) + const h1 = 42 + Math.floor(Math.random() * 8); // 原 10-12 -> 42-50 + const r1 = 2.5; + drawStem(builder, ox - 2, 0, oz, h1, r1, CACTUS_COLORS.darkGreen, CACTUS_COLORS.midGreen, true); + + // 顶部封盖 + for (let xi = -Math.ceil(r1); xi <= Math.ceil(r1); xi++) { + for (let zi = -Math.ceil(r1); zi <= Math.ceil(r1); zi++) { + if (xi * xi + zi * zi <= r1 * r1) { + builder.add(ox - 2 + xi, h1, oz + zi, CACTUS_COLORS.shadow); + } + } + } + + // 主干2: 较矮较细 + const h2 = 30 + Math.floor(Math.random() * 8); // 原 7-9 -> 30-38 + const r2 = 1.8; + drawStem(builder, ox + 2, 0, oz, h2, r2, CACTUS_COLORS.darkGreen, CACTUS_COLORS.midGreen, true); + + for (let xi = -Math.ceil(r2); xi <= Math.ceil(r2); xi++) { + for (let zi = -Math.ceil(r2); zi <= Math.ceil(r2); zi++) { + if (xi * xi + zi * zi <= r2 * r2) { + builder.add(ox + 2 + xi, h2, oz + zi, CACTUS_COLORS.shadow); + } + } + } +}; + +// 简单柱状仙人掌(无分枝,用于小型) +const createSimpleCactus = (builder: VoxelBuilder, ox: number, oz: number) => { + // 高度 12-18(原 3-5 * 16 网格 ≈ 0.19-0.31 Tile,现在 12-18 / 32 ≈ 0.38-0.56 Tile) + const h = 12 + Math.floor(Math.random() * 7); + const r = 1.5 + Math.random() * 0.5; + + drawStem(builder, ox, 0, oz, h, r, CACTUS_COLORS.darkGreen, CACTUS_COLORS.midGreen, true); + + // 顶部 + for (let xi = -Math.ceil(r); xi <= Math.ceil(r); xi++) { + for (let zi = -Math.ceil(r); zi <= Math.ceil(r); zi++) { + if (xi * xi + zi * zi <= r * r) { + builder.add(ox + xi, h, oz + zi, CACTUS_COLORS.shadow); + } + } + } +}; + +// ============= 参数化仙人掌变体 ============= + +// 经典 Saguaro(高度增加 35%:24 -> 32) +const createSaguaroClassic = (b: VoxelBuilder, x: number, z: number) => + createParametricSaguaro(b, x, z, { + height: 32 + Math.floor(Math.random() * 6), // 32-38 + radius: 2.5, + armCount: 2, + armStyle: 'balanced', + hasFlower: true, + flowerColor: CACTUS_COLORS.flower + }); + +// 烛台型 Saguaro(高度增加 35%:28 -> 38) +const createSaguaroCandelabra = (b: VoxelBuilder, x: number, z: number) => + createParametricSaguaro(b, x, z, { + height: 38 + Math.floor(Math.random() * 8), // 38-46 + radius: 3.2, + armCount: 4, + armStyle: 'low', + hasFlower: true, + flowerColor: CACTUS_COLORS.fruitYellow + }); + +// 巨型 Saguaro(新增,高度 50-60) +const createSaguaroGiant = (b: VoxelBuilder, x: number, z: number) => + createParametricSaguaro(b, x, z, { + height: 50 + Math.floor(Math.random() * 10), // 50-60 + radius: 3.5, + armCount: 3 + Math.floor(Math.random() * 2), // 3-4 + armStyle: 'goliath', + hasFlower: true + }); + +// 野生型 Saguaro(分枝角度随机) +const createSaguaroWild = (b: VoxelBuilder, x: number, z: number) => + createParametricSaguaro(b, x, z, { + height: 28 + Math.floor(Math.random() * 10), // 28-38 + radius: 2.2, + armCount: 2 + Math.floor(Math.random() * 2), // 2-3 + armStyle: 'wild', + hasFlower: Math.random() > 0.5 + }); + +// 仙人球变体 +const createBarrelSmall = (b: VoxelBuilder, x: number, z: number) => createBarrelCactusNew(b, x, z, 'small'); +const createBarrelMedium = (b: VoxelBuilder, x: number, z: number) => createBarrelCactusNew(b, x, z, 'medium'); +const createBarrelLarge = (b: VoxelBuilder, x: number, z: number) => createBarrelCactusNew(b, x, z, 'large'); + +// ============= 戈壁非仙人掌植物 (Non-Cactus Desert Plants) ============= + +// 戈壁植物颜色调色板 +const DESERT_PLANT_COLORS = { + woodDark: '#4A3B2A', // 深木色 + woodLight: '#8C7B58', // 浅木色/枯黄 + woodGrey: '#6E6358', // 灰木色 + leafGrey: '#6E8578', // 灰绿叶 + leafDry: '#A9A060', // 干枯黄绿 + white: '#F0F0F0', // 白色(老人柱绒毛) + pink: '#D97998', // 粉色(柽柳花) + red: '#D94141', // 红色(蜡烛木花) + crystal: '#66CCFF', // 水晶蓝 + bone: '#E3E1CD', // 骨白色 + rock: '#8A867D', // 岩石灰 +}; + +// ========== 1. 约书亚树 (Joshua Tree) ========== +// 沙漠标志性植物,分叉树干 + 球状刺叶簇 +// 现实中约书亚树 8-12m,相比 Saguaro 10-15m,约 70-80% +const createJoshuaTree = (builder: VoxelBuilder, ox: number, oz: number) => { + // 高度 24-32(约 0.75-1.0 Tile,对应经典 Saguaro 的 75-85%) + const trunkH = 24 + Math.floor(Math.random() * 9); + const trunkR = 1.8; + + // 主干(棕色,略弯曲) + for (let y = 0; y < trunkH; y++) { + const wobble = Math.sin(y * 0.3) * 0.3; + const tx = Math.round(ox + wobble); + drawCylinder(builder, tx, y, oz, 1, trunkR * (1 - y / trunkH * 0.3), DESERT_PLANT_COLORS.woodDark); + } + + // 分叉点(在 60-80% 高度) + const branchY = Math.floor(trunkH * (0.6 + Math.random() * 0.2)); + const branchCount = 2 + Math.floor(Math.random() * 2); // 2-3 个分枝 + + for (let i = 0; i < branchCount; i++) { + const angle = (i / branchCount) * Math.PI * 2 + Math.random() * 0.5; + const branchLen = 8 + Math.floor(Math.random() * 5); + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + // 斜向上的分枝 + for (let d = 0; d < branchLen; d++) { + const bx = Math.round(ox + cos * d * 0.8); + const bz = Math.round(oz + sin * d * 0.8); + const by = branchY + d; + builder.add(bx, by, bz, DESERT_PLANT_COLORS.woodDark); + if (d < branchLen * 0.5) { + builder.add(bx + 1, by, bz, DESERT_PLANT_COLORS.woodDark); + } + } + + // 顶部刺叶簇(灰绿色球) + const tipX = Math.round(ox + cos * branchLen * 0.8); + const tipZ = Math.round(oz + sin * branchLen * 0.8); + const tipY = branchY + branchLen; + drawSphere(builder, tipX, tipY, tipZ, 3 + Math.random() * 1.5, + DESERT_PLANT_COLORS.leafGrey, DESERT_PLANT_COLORS.leafDry, 1, 0.3, 0.6); + } +}; + +// ========== 2. 墨西哥蜡烛木 (Ocotillo) ========== +// 多根细长刺状茎从基部向外散开,顶部红花 +// 现实中蜡烛木 3-6m,相比 Saguaro 10-15m,约 30-40% +const createOcotillo = (builder: VoxelBuilder, ox: number, oz: number) => { + const stemCount = 8 + Math.floor(Math.random() * 4); // 8-11 根茎 + const maxHeight = 14 + Math.floor(Math.random() * 6); // 14-20 高(0.44-0.63 Tile) + + for (let i = 0; i < stemCount; i++) { + const angle = (i / stemCount) * Math.PI * 2; + const spreadRate = 0.4 + Math.random() * 0.2; // 向外散开的速率 + const stemH = maxHeight - Math.floor(Math.random() * 6); + + for (let h = 0; h < stemH; h++) { + const spread = h * spreadRate * 0.15; + const lx = Math.round(ox + Math.cos(angle) * spread); + const lz = Math.round(oz + Math.sin(angle) * spread); + + // 主茎(深绿色) + builder.add(lx, h, lz, CACTUS_COLORS.darkGreen); + + // 偶尔有小叶/刺 + if (h > 2 && Math.random() > 0.7) { + builder.add(lx + 1, h, lz, CACTUS_COLORS.midGreen); + } + } + + // 顶部红花(开花季节) + if (Math.random() > 0.3) { + const tipX = Math.round(ox + Math.cos(angle) * stemH * spreadRate * 0.15); + const tipZ = Math.round(oz + Math.sin(angle) * stemH * spreadRate * 0.15); + builder.add(tipX, stemH, tipZ, DESERT_PLANT_COLORS.red); + builder.add(tipX, stemH + 1, tipZ, DESERT_PLANT_COLORS.red); + } + } +}; + +// ========== 3. 柽柳/红柳 (Tamarisk) ========== +// 灌木状,细枝 + 粉色花球 +// 现实中柽柳是灌木 2-4m,相比 Saguaro 10-15m,约 15-30% +const createTamarisk = (builder: VoxelBuilder, ox: number, oz: number) => { + // 多个细树干 + const trunkCount = 4 + Math.floor(Math.random() * 3); + const baseH = 6 + Math.floor(Math.random() * 4); // 6-10 高(0.19-0.31 Tile) + + for (let i = 0; i < trunkCount; i++) { + const tx = ox + Math.floor((i - trunkCount / 2) * 1.5); + for (let h = 0; h < baseH; h++) { + builder.add(tx, h, oz, DESERT_PLANT_COLORS.woodDark); + } + } + + // 顶部粉色花球簇(缩小范围匹配新高度) + const flowerCount = 6 + Math.floor(Math.random() * 5); + for (let i = 0; i < flowerCount; i++) { + const fx = ox + (Math.random() - 0.5) * 6; + const fy = baseH + Math.random() * 3; + const fz = oz + (Math.random() - 0.5) * 6; + + // 小粉色花球 + const r = 1.2 + Math.random() * 0.5; + drawSphere(builder, Math.round(fx), Math.round(fy), Math.round(fz), r, + DESERT_PLANT_COLORS.pink, DESERT_PLANT_COLORS.leafGrey, 1, 0.2, 0.5); + } +}; + +// ========== 4. 梭梭树 (Saxaul) ========== +// 戈壁典型灌木,枯黄色细枝向外散开 +const createSaxaul = (builder: VoxelBuilder, ox: number, oz: number) => { + // 短粗主干 + const trunkH = 8 + Math.floor(Math.random() * 4); + for (let y = 0; y < trunkH; y++) { + const wobble = (Math.random() - 0.5) * 0.8; + builder.add(Math.round(ox + wobble), y, oz, DESERT_PLANT_COLORS.woodDark); + } + + // 大量枯黄色细枝向外散开 + const branchCount = 15 + Math.floor(Math.random() * 10); + for (let i = 0; i < branchCount; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = 2 + Math.random() * 6; + const bx = Math.round(ox + Math.cos(angle) * dist); + const bz = Math.round(oz + Math.sin(angle) * dist); + const by = trunkH - 2 + Math.random() * 6; + + builder.add(bx, Math.round(by), bz, DESERT_PLANT_COLORS.woodLight); + // 有些枝条更长 + if (Math.random() > 0.5) { + builder.add(bx, Math.round(by) + 1, bz, DESERT_PLANT_COLORS.woodLight); + } + } +}; + +// ========== 5. 枯木 (Dead Log) ========== +// 倒下的空心树干(贴地放置) +// 长度对应一棵倒下的中型树,半径适当缩小 +const createDeadLog = (builder: VoxelBuilder, ox: number, oz: number) => { + const length = 16 + Math.floor(Math.random() * 10); // 16-26 长(0.5-0.81 Tile) + const radius = 2 + Math.random() * 1.5; // 半径 2-3.5 + const angle = Math.random() * Math.PI; // 随机朝向 + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + for (let l = 0; l < length; l++) { + const cx = ox + cos * l; + const cz = oz + sin * l; + // 圆心 y = radius,这样底部 (y - radius) 刚好贴地 (y=0) + const baseY = radius + Math.sin(l * 0.2) * 0.3; // 轻微起伏 + + // 空心圆柱截面 + for (let dy = -Math.ceil(radius); dy <= Math.ceil(radius); dy++) { + for (let dr = -Math.ceil(radius); dr <= Math.ceil(radius); dr++) { + const distSq = dy * dy + dr * dr; + // 外壳(空心) + if (distSq <= radius * radius && distSq > (radius - 1.2) * (radius - 1.2)) { + const ry = baseY + dy; + // 只渲染地面以上的部分 + if (ry >= 0) { + const rx = cx - sin * dr; // 垂直于长度方向 + const rz = cz + cos * dr; + builder.add(Math.round(rx), Math.round(ry), Math.round(rz), DESERT_PLANT_COLORS.woodGrey); + } + } + } + } + } +}; + +// ========== 6. 老人柱 (Old Man Cactus) ========== +// 白色绒毛覆盖的柱状植物 +// 现实中老人柱 6-9m,相比 Saguaro 10-15m,约 50-60% +const createOldManCactus = (builder: VoxelBuilder, ox: number, oz: number) => { + const height = 18 + Math.floor(Math.random() * 8); // 18-26 高(0.56-0.81 Tile) + + for (let y = 0; y < height; y++) { + for (let x = -2; x <= 2; x++) { + for (let z = -2; z <= 2; z++) { + if (x * x + z * z <= 4) { + // 外层白色绒毛,内层灰绿色 + const isOuter = x * x + z * z > 2 || Math.random() > 0.5; + const color = isOuter ? DESERT_PLANT_COLORS.white : DESERT_PLANT_COLORS.leafGrey; + builder.add(ox + x, y, oz + z, color); + } + } + } + } + + // 顶部圆润 + drawSphere(builder, ox, height, oz, 2.5, DESERT_PLANT_COLORS.white, DESERT_PLANT_COLORS.leafGrey, 0.7, 0.1, 0.6); +}; + +// ========== 7. 象脚树 (Elephant Tree / Pachypodium) ========== +// 膨大的白色/灰色基部 + 顶部小绿叶 +const createElephantTree = (builder: VoxelBuilder, ox: number, oz: number) => { + // 膨大的基部(从下到上逐渐变细) + const baseH = 14 + Math.floor(Math.random() * 6); + + for (let y = 0; y < baseH; y++) { + const t = y / baseH; + const r = 5 * (1 - t * 0.7); // 底部宽,顶部窄 + + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + z * z <= r * r) { + builder.add(ox + x, y, oz + z, DESERT_PLANT_COLORS.white); + } + } + } + } + + // 顶部稀疏的绿叶 + const leafCount = 12 + Math.floor(Math.random() * 8); + for (let i = 0; i < leafCount; i++) { + const lx = ox + (Math.random() - 0.5) * 8; + const ly = baseH + Math.random() * 6; + const lz = oz + (Math.random() - 0.5) * 8; + builder.add(Math.round(lx), Math.round(ly), Math.round(lz), CACTUS_COLORS.midGreen); + } +}; + +// ========== 8. 沙漠水晶 (Desert Crystal) ========== +// 尖锐的蓝色/白色晶簇 +const createDesertCrystal = (builder: VoxelBuilder, ox: number, oz: number) => { + const crystalCount = 4 + Math.floor(Math.random() * 3); // 4-6 根晶体 + + for (let i = 0; i < crystalCount; i++) { + const cx = ox + (Math.random() - 0.5) * 6; + const cz = oz + (Math.random() - 0.5) * 6; + const height = 8 + Math.floor(Math.random() * 8); // 8-16 高 + + // 尖锐的晶体柱 + for (let y = 0; y < height; y++) { + const t = y / height; + // 底部宽,顶部尖 + const size = Math.max(0.5, (1 - t) * 1.5); + + builder.add(Math.round(cx), y, Math.round(cz), DESERT_PLANT_COLORS.crystal); + if (size > 1 && y < height * 0.7) { + builder.add(Math.round(cx) + 1, y, Math.round(cz), DESERT_PLANT_COLORS.crystal); + builder.add(Math.round(cx), y, Math.round(cz) + 1, DESERT_PLANT_COLORS.crystal); + } + } + + // 顶部高光 + builder.add(Math.round(cx), height, Math.round(cz), DESERT_PLANT_COLORS.white); + } +}; + +// ========== 9. 风化岩石堆 (Desert Rocks) ========== +// 圆润的岩石堆 +const createDesertRocks = (builder: VoxelBuilder, ox: number, oz: number) => { + const rockCount = 2 + Math.floor(Math.random() * 3); // 2-4 块岩石 + + for (let i = 0; i < rockCount; i++) { + const rx = ox + (Math.random() - 0.5) * 8; + const rz = oz + (Math.random() - 0.5) * 8; + const r = 2 + Math.random() * 3; // 半径 2-5 + const stretchY = 0.5 + Math.random() * 0.3; // 扁平化 + + drawSphere(builder, Math.round(rx), Math.round(r * stretchY), Math.round(rz), r, + DESERT_PLANT_COLORS.rock, DESERT_PLANT_COLORS.woodLight, stretchY, 0.1, 0.8); + } +}; + +// ========== 10. 动物骨骼 (Animal Bones) ========== +// 白色骨头散落 +const createAnimalBones = (builder: VoxelBuilder, ox: number, oz: number) => { + // 脊椎骨(一串) + const spineLen = 8 + Math.floor(Math.random() * 6); + const angle = Math.random() * Math.PI; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + for (let i = 0; i < spineLen; i++) { + const bx = Math.round(ox + cos * i * 1.2); + const bz = Math.round(oz + sin * i * 1.2); + builder.add(bx, 0, bz, DESERT_PLANT_COLORS.bone); + // 每隔几节有肋骨 + if (i % 3 === 0 && i > 0 && i < spineLen - 1) { + builder.add(bx - Math.round(sin * 2), 0, bz + Math.round(cos * 2), DESERT_PLANT_COLORS.bone); + builder.add(bx + Math.round(sin * 2), 0, bz - Math.round(cos * 2), DESERT_PLANT_COLORS.bone); + } + } + + // 头骨(椭圆形) + const headX = Math.round(ox + cos * (spineLen + 1) * 1.2); + const headZ = Math.round(oz + sin * (spineLen + 1) * 1.2); + drawSphere(builder, headX, 1, headZ, 2, DESERT_PLANT_COLORS.bone, DESERT_PLANT_COLORS.white, 0.6, 0, 0.7); +}; + +// ========== 11. 风滚草 (Tumbleweed) ========== +// 干枯的球形草团 +// 现实中风滚草直径 0.3-1m,相对于人(1.7m)很小,这里用半径 2-3 +const createTumbleweed = (builder: VoxelBuilder, ox: number, oz: number) => { + const r = 2 + Math.random() * 1.5; // 半径 2-3.5(直径 4-7) + const density = 0.45; // 稀疏的网状结构 + + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let y = -Math.ceil(r); y <= Math.ceil(r); y++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + const distSq = x * x + y * y + z * z; + if (distSq <= r * r && distSq > (r - 1.5) * (r - 1.5)) { + // 随机稀疏分布 + if (Math.random() < density) { + builder.add(ox + x, Math.round(r + y), oz + z, DESERT_PLANT_COLORS.woodLight); + } + } + } + } + } +}; + +// ============= 戈壁河岸植被 (Riparian Zone) ============= + +// 河岸灌木颜色 +const SHRUB_COLORS = { + leafDark: '#4A7A42', // 深绿色叶子 + leafMid: '#6B9B5A', // 中等绿色 + leafLight: '#8CB87A', // 浅绿色高光 + wood: '#7A6B52', // 灌木枝干 +}; + +// 戈壁灌木 - 低矮的球形灌木(适应干旱环境) +const createDesertShrub = (builder: VoxelBuilder, ox: number, oz: number) => { + // 高度 8-14(约 0.25-0.44 Tile) + const h = 8 + Math.floor(Math.random() * 7); + const crownR = 4 + Math.random() * 3; // 树冠半径 4-7 + + // 短树干(2-4格) + const trunkH = 2 + Math.floor(Math.random() * 3); + drawCylinder(builder, ox, 0, oz, trunkH, 1.2, SHRUB_COLORS.wood); + + // 球形树冠 + const crownY = trunkH + crownR * 0.5; + drawSphere(builder, ox, crownY, oz, crownR, SHRUB_COLORS.leafDark, SHRUB_COLORS.leafMid, 0.8, 0.2, 0.7); +}; + +// 绿洲灌木丛 - 多个小球形堆叠 +const createOasisBush = (builder: VoxelBuilder, ox: number, oz: number) => { + // 主球 + const mainR = 5 + Math.random() * 2; + const mainY = mainR * 0.6; + drawSphere(builder, ox, mainY, oz, mainR, SHRUB_COLORS.leafMid, SHRUB_COLORS.leafLight, 0.85, 0.15, 0.8); + + // 2-3个附属小球 + const subCount = 2 + Math.floor(Math.random() * 2); + for (let i = 0; i < subCount; i++) { + const angle = (i / subCount) * Math.PI * 2 + Math.random() * 0.5; + const dist = mainR * 0.6; + const subR = mainR * (0.5 + Math.random() * 0.2); + const subX = ox + Math.cos(angle) * dist; + const subZ = oz + Math.sin(angle) * dist; + const subY = subR * 0.5; + drawSphere(builder, Math.round(subX), subY, Math.round(subZ), subR, SHRUB_COLORS.leafDark, SHRUB_COLORS.leafMid, 0.8, 0.2, 0.75); + } +}; + +// 河岸草丛 - 密集的高草 +const createRiparianGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const clumpCount = 8 + Math.floor(Math.random() * 6); // 8-14 簇草 + + for (let i = 0; i < clumpCount; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 4; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + + // 每簇草 3-8 格高 + const grassH = 3 + Math.floor(Math.random() * 6); + for (let y = 0; y < grassH; y++) { + // 使用渐变绿色 + const col = y < grassH * 0.3 ? SHRUB_COLORS.leafDark : + y < grassH * 0.7 ? SHRUB_COLORS.leafMid : SHRUB_COLORS.leafLight; + builder.add(gx, y, gz, col); + // 偶尔有宽度 + if (Math.random() > 0.7) { + builder.add(gx + 1, y, gz, col); + } + } + } +}; + +// 芦苇 - 细长的金黄色 +const createReed = (builder: VoxelBuilder, ox: number, oz: number) => { + const reedCount = 6 + Math.floor(Math.random() * 5); // 6-10 根 + const reedColor = '#C4A35A'; // 金黄色芦苇 + const tipColor = '#E8D08A'; // 顶部更亮 + + for (let i = 0; i < reedCount; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 3; + const rx = Math.round(ox + Math.cos(angle) * dist); + const rz = Math.round(oz + Math.sin(angle) * dist); + + // 每根芦苇 6-12 格高 + const reedH = 6 + Math.floor(Math.random() * 7); + for (let y = 0; y < reedH; y++) { + const col = y > reedH * 0.8 ? tipColor : reedColor; + builder.add(rx, y, rz, col); + } + } +}; + +// 小型地被植物 - 低矮的草簇 +const createSmallGrassTuft = (builder: VoxelBuilder, ox: number, oz: number) => { + const count = 5 + Math.floor(Math.random() * 4); // 5-8 簇 + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 2.5; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 2 + Math.floor(Math.random() * 3); // 2-4 格高 + for (let y = 0; y < h; y++) { + builder.add(gx, y, gz, SHRUB_COLORS.leafMid); + } + } +}; + +// ============= 新增普通草类 ============= + +// 细叶草 - 单根细长的草叶 +const createThinGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassGreen = '#5A8A4A'; + const grassLight = '#7AAA6A'; + const count = 8 + Math.floor(Math.random() * 6); // 8-14 根 + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 3; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 3 + Math.floor(Math.random() * 4); // 3-6 高 + + for (let y = 0; y < h; y++) { + const col = y === h - 1 ? grassLight : grassGreen; + builder.add(gx, y, gz, col); + } + } +}; + +// 宽叶草 - 稍微宽一点的草 +const createWideGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassDark = '#4A7A3A'; + const grassMid = '#5A8A4A'; + const count = 5 + Math.floor(Math.random() * 4); // 5-8 簇 + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 2.5; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 2 + Math.floor(Math.random() * 3); // 2-4 高 + + for (let y = 0; y < h; y++) { + builder.add(gx, y, gz, grassMid); + // 宽叶效果 + if (y < h - 1) { + builder.add(gx + 1, y, gz, grassDark); + } + } + } +}; + +// 密集草皮 - 地面覆盖的矮草 +const createDenseGrassCover = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassColors = ['#4A7A3A', '#5A8A4A', '#4A8A3A', '#5A7A4A']; + const patchR = 2 + Math.random() * 2; + + for (let x = -Math.ceil(patchR); x <= Math.ceil(patchR); x++) { + for (let z = -Math.ceil(patchR); z <= Math.ceil(patchR); z++) { + if (x * x + z * z <= patchR * patchR && Math.random() > 0.15) { + const col = grassColors[Math.floor(Math.random() * grassColors.length)]; + builder.add(ox + x, 0, oz + z, col); + // 有些地方稍高 + if (Math.random() > 0.6) { + builder.add(ox + x, 1, oz + z, col); + } + } + } + } +}; + +// 高草丛 - 较高的草 +const createTallGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassGreen = '#5A8A4A'; + const grassYellow = '#8A9A5A'; // 略微发黄的草尖 + const count = 6 + Math.floor(Math.random() * 5); // 6-10 根 + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 2; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 5 + Math.floor(Math.random() * 4); // 5-8 高 + + for (let y = 0; y < h; y++) { + // 顶部略黄 + const col = y > h * 0.7 ? grassYellow : grassGreen; + builder.add(gx, y, gz, col); + } + } +}; + +// 水边细草 - 适应水边环境的细长草 +const createWatersideGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassGreen = '#4A8A4A'; + const grassBright = '#6AAA6A'; + const count = 10 + Math.floor(Math.random() * 8); // 10-17 根(高密度) + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 2.5; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 4 + Math.floor(Math.random() * 4); // 4-7 高 + + for (let y = 0; y < h; y++) { + const col = Math.random() > 0.7 ? grassBright : grassGreen; + builder.add(gx, y, gz, col); + } + } +}; + +// 草簇群落 - 多个小草簇组成的群落 +const createGrassCluster = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassColors = ['#4A7A3A', '#5A8A4A', '#5A9A4A', '#4A8A3A']; + const clusterCount = 3 + Math.floor(Math.random() * 3); // 3-5 个小簇 + + for (let c = 0; c < clusterCount; c++) { + const cx = ox + (Math.random() - 0.5) * 4; + const cz = oz + (Math.random() - 0.5) * 4; + const grassCount = 4 + Math.floor(Math.random() * 4); + + for (let i = 0; i < grassCount; i++) { + const gx = Math.round(cx + (Math.random() - 0.5) * 2); + const gz = Math.round(cz + (Math.random() - 0.5) * 2); + const h = 2 + Math.floor(Math.random() * 4); + const col = grassColors[Math.floor(Math.random() * grassColors.length)]; + + for (let y = 0; y < h; y++) { + builder.add(gx, y, gz, col); + } + } + } +}; + +// ============= 超小型稀疏草(2-3 体素高度) ============= + +// 微型草丝 - 单根极细的草,只有 2-3 高 +const createTinyGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const tinyGrassColors = ['#5A9A4A', '#4A8A3A', '#6AAA5A', '#5AAA4A']; + const count = 12 + Math.floor(Math.random() * 10); // 12-21 根(稀疏分布在较大范围) + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 5; // 更大的分布范围 + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 2 + Math.floor(Math.random() * 2); // 只有 2-3 高 + const col = tinyGrassColors[Math.floor(Math.random() * tinyGrassColors.length)]; + + for (let y = 0; y < h; y++) { + builder.add(gx, y, gz, col); + } + } +}; + +// 微型草皮 - 贴地的超矮草覆盖 +const createTinyGrassPatch = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassColors = ['#4A8A3A', '#5A9A4A', '#4A9A3A', '#5AAA4A']; + const patchR = 3 + Math.random() * 2; // 半径 3-5 + + for (let x = -Math.ceil(patchR); x <= Math.ceil(patchR); x++) { + for (let z = -Math.ceil(patchR); z <= Math.ceil(patchR); z++) { + if (x * x + z * z <= patchR * patchR && Math.random() > 0.4) { // 稀疏分布(60%覆盖) + const col = grassColors[Math.floor(Math.random() * grassColors.length)]; + const h = 1 + Math.floor(Math.random() * 2); // 1-2 高 + for (let y = 0; y < h; y++) { + builder.add(ox + x, y, oz + z, col); + } + } + } + } +}; + +// 散落草芽 - 极其稀疏的小草芽 +const createScatteredSprouts = (builder: VoxelBuilder, ox: number, oz: number) => { + const sproutColors = ['#5AAA4A', '#6ABA5A', '#5ABA4A']; + const count = 6 + Math.floor(Math.random() * 6); // 6-11 个草芽 + + for (let i = 0; i < count; i++) { + const gx = Math.round(ox + (Math.random() - 0.5) * 8); + const gz = Math.round(oz + (Math.random() - 0.5) * 8); + const col = sproutColors[Math.floor(Math.random() * sproutColors.length)]; + + // 只有 2-3 高 + const h = 2 + Math.floor(Math.random() * 2); + for (let y = 0; y < h; y++) { + builder.add(gx, y, gz, col); + } + } +}; + +// 河岸微草 - 专门为河岸设计的超小型草 +const createRiverbankTinyGrass = (builder: VoxelBuilder, ox: number, oz: number) => { + const grassColors = ['#4A9A4A', '#5AAA5A', '#4AAA4A', '#5ABA5A']; // 更鲜绿的颜色 + const count = 15 + Math.floor(Math.random() * 12); // 15-26 根(高密度) + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 4; + const gx = Math.round(ox + Math.cos(angle) * dist); + const gz = Math.round(oz + Math.sin(angle) * dist); + const h = 2 + Math.floor(Math.random() * 2); // 2-3 高 + const col = grassColors[Math.floor(Math.random() * grassColors.length)]; + + for (let y = 0; y < h; y++) { + builder.add(gx, y, gz, col); + } + } +}; + +// 小野花 - 带颜色的小花簇 +const createSmallWildflower = (builder: VoxelBuilder, ox: number, oz: number) => { + const flowerColors = ['#FF6B6B', '#FFE66D', '#4ECDC4', '#C44D96', '#FF8C42']; + const stemColor = '#5A8A4A'; + const count = 3 + Math.floor(Math.random() * 3); // 3-5 朵 + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 3; + const fx = Math.round(ox + Math.cos(angle) * dist); + const fz = Math.round(oz + Math.sin(angle) * dist); + const stemH = 2 + Math.floor(Math.random() * 3); // 茎 2-4 格 + + // 茎 + for (let y = 0; y < stemH; y++) { + builder.add(fx, y, fz, stemColor); + } + // 花朵 + const flowerColor = flowerColors[Math.floor(Math.random() * flowerColors.length)]; + builder.add(fx, stemH, fz, flowerColor); + // 可能有花瓣 + if (Math.random() > 0.5) { + builder.add(fx + 1, stemH, fz, flowerColor); + builder.add(fx - 1, stemH, fz, flowerColor); + } + } +}; + +// 蕨类 - 展开的叶片 +const createFernNew = (builder: VoxelBuilder, ox: number, oz: number) => { + const fernColor = '#4A7A52'; + const fernLight = '#6B9B6A'; + + // 中央茎 + const stemH = 3 + Math.floor(Math.random() * 2); + for (let y = 0; y < stemH; y++) { + builder.add(ox, y, oz, fernColor); + } + + // 向外展开的叶片(4-6个方向) + const leafCount = 4 + Math.floor(Math.random() * 3); + for (let i = 0; i < leafCount; i++) { + const angle = (i / leafCount) * Math.PI * 2; + const leafLen = 2 + Math.floor(Math.random() * 2); + for (let d = 1; d <= leafLen; d++) { + const lx = Math.round(ox + Math.cos(angle) * d); + const lz = Math.round(oz + Math.sin(angle) * d); + const ly = stemH - 1 - Math.floor(d * 0.3); // 叶片稍微下垂 + if (ly >= 0) { + builder.add(lx, ly, lz, d === leafLen ? fernLight : fernColor); + } + } + } +}; + +// ============= 新增亲水植物 ============= + +// 香蒲 (Cattail) - 水边标志性植物,高大的穗状花序 +const createCattail = (builder: VoxelBuilder, ox: number, oz: number) => { + const count = 5 + Math.floor(Math.random() * 4); // 5-8 根 + const cattailGreen = '#5A7A4A'; + const cattailBrown = '#6B4423'; // 棕色穗 + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 2.5; + const cx = Math.round(ox + Math.cos(angle) * dist); + const cz = Math.round(oz + Math.sin(angle) * dist); + + // 茎高度 8-14 + const stemH = 8 + Math.floor(Math.random() * 7); + for (let y = 0; y < stemH; y++) { + builder.add(cx, y, cz, cattailGreen); + } + + // 棕色穗(顶部 3-5 格) + const spikeH = 3 + Math.floor(Math.random() * 3); + for (let y = 0; y < spikeH; y++) { + builder.add(cx, stemH + y, cz, cattailBrown); + // 穗较粗 + if (y < spikeH - 1) { + builder.add(cx + 1, stemH + y, cz, cattailBrown); + builder.add(cx, stemH + y, cz + 1, cattailBrown); + } + } + } +}; + +// 莎草 (Sedge) - 低矮密集的湿地草 +const createSedge = (builder: VoxelBuilder, ox: number, oz: number) => { + const sedgeGreen = '#4A6A3A'; + const sedgeLight = '#6A8A5A'; + const clumpR = 3 + Math.random() * 2; + + // 密集的草丛 + for (let x = -Math.ceil(clumpR); x <= Math.ceil(clumpR); x++) { + for (let z = -Math.ceil(clumpR); z <= Math.ceil(clumpR); z++) { + if (x * x + z * z <= clumpR * clumpR && Math.random() > 0.3) { + const h = 2 + Math.floor(Math.random() * 4); // 2-5 高 + for (let y = 0; y < h; y++) { + const col = y === h - 1 ? sedgeLight : sedgeGreen; + builder.add(ox + x, y, oz + z, col); + } + } + } + } +}; + +// 沙漠柳 (Desert Willow) - 沙漠溪流边的小型柳树 +const createDesertWillow = (builder: VoxelBuilder, ox: number, oz: number) => { + const trunkH = 10 + Math.floor(Math.random() * 6); // 10-16 高 + const trunkCol = '#5A4A3A'; + const leafCol = '#5A8A5A'; + const leafLight = '#7AAA7A'; + const flowerPink = '#D98AAA'; // 粉色花 + + // 主干(略弯曲) + for (let y = 0; y < trunkH; y++) { + const wobble = Math.sin(y * 0.4) * 0.5; + builder.add(Math.round(ox + wobble), y, oz, trunkCol); + } + + // 下垂的枝条 + const branchCount = 6 + Math.floor(Math.random() * 4); + for (let i = 0; i < branchCount; i++) { + const angle = (i / branchCount) * Math.PI * 2; + const startY = trunkH - 2 - Math.floor(Math.random() * 3); + const len = 4 + Math.floor(Math.random() * 4); + + for (let d = 0; d < len; d++) { + const bx = ox + Math.cos(angle) * (d * 0.8); + const bz = oz + Math.sin(angle) * (d * 0.8); + const by = startY - d * 0.5; // 下垂 + if (by > 0) { + builder.add(Math.round(bx), Math.round(by), Math.round(bz), + Math.random() > 0.3 ? leafCol : leafLight); + // 偶尔有粉色花 + if (Math.random() > 0.85) { + builder.add(Math.round(bx), Math.round(by) + 1, Math.round(bz), flowerPink); + } + } + } + } +}; + +// 水边苔藓/地衣 - 贴地的绿色覆盖 +const createRiverMoss = (builder: VoxelBuilder, ox: number, oz: number) => { + const mossGreen = '#3A5A2A'; + const mossLight = '#5A7A4A'; + const patchR = 2 + Math.random() * 2; + + for (let x = -Math.ceil(patchR); x <= Math.ceil(patchR); x++) { + for (let z = -Math.ceil(patchR); z <= Math.ceil(patchR); z++) { + if (x * x + z * z <= patchR * patchR && Math.random() > 0.2) { + const col = Math.random() > 0.6 ? mossGreen : mossLight; + builder.add(ox + x, 0, oz + z, col); + // 偶尔有一点高度 + if (Math.random() > 0.8) { + builder.add(ox + x, 1, oz + z, col); + } + } + } + } +}; + +// 三棱草 (Bulrush) - 三角形茎的水边草 +const createBulrush = (builder: VoxelBuilder, ox: number, oz: number) => { + const rushGreen = '#4A7A4A'; + const count = 4 + Math.floor(Math.random() * 4); + + for (let i = 0; i < count; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * 2; + const rx = Math.round(ox + Math.cos(angle) * dist); + const rz = Math.round(oz + Math.sin(angle) * dist); + + const h = 6 + Math.floor(Math.random() * 5); // 6-10 高 + for (let y = 0; y < h; y++) { + builder.add(rx, y, rz, rushGreen); + // 三角形截面效果 + if (y < h - 2 && Math.random() > 0.5) { + const side = Math.floor(Math.random() * 3); + if (side === 0) builder.add(rx + 1, y, rz, rushGreen); + else if (side === 1) builder.add(rx, y, rz + 1, rushGreen); + else builder.add(rx - 1, y, rz, rushGreen); + } + } + } +}; + +// 水边石头上的青苔 +const createMossyRock = (builder: VoxelBuilder, ox: number, oz: number) => { + const rockGrey = '#7A7A72'; + const mossGreen = '#4A6A3A'; + const r = 1.5 + Math.random() * 1.5; + + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let y = 0; y <= Math.ceil(r); y++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + y * y + z * z <= r * r) { + // 上半部分有苔藓 + const hasMoss = y > r * 0.5 && Math.random() > 0.4; + builder.add(ox + x, y, oz + z, hasMoss ? mossGreen : rockGrey); + } + } + } + } +}; + +// ============= 分层河岸植被生成器列表(草类大幅增强版) ============= + +// 河床内植物(直接生长在水中/浅水区):超高密度水草和苔藓 +const INSTREAM_GENERATORS = [ + // 普通草类(超高权重) + createThinGrass, createThinGrass, createThinGrass, createThinGrass, createThinGrass, // 细叶草 x5 + createWatersideGrass, createWatersideGrass, createWatersideGrass, createWatersideGrass, // 水边细草 x4 + createDenseGrassCover, createDenseGrassCover, createDenseGrassCover, // 密集草皮 x3 + createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, // 小草簇 x4 + createGrassCluster, createGrassCluster, createGrassCluster, // 草簇群落 x3 + + // 超小型稀疏草(2-3 体素高,高权重) + createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, // 微型草丝 x6 + createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, // 微型草皮 x4 + createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, // 散落草芽 x4 + createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, // 河岸微草 x5 + + // 水生草类 + createSedge, createSedge, createSedge, createSedge, createSedge, // 莎草 x5 + createBulrush, createBulrush, createBulrush, // 三棱草 x3 + + // 苔藓 + createRiverMoss, createRiverMoss, createRiverMoss, createRiverMoss, // 苔藓 x4 + + // 高杆植物(低权重) + createReed, // 芦苇 x1 +]; + +// 水边层植物(距离 0-2):超高密度草类 + 芦苇 +const WATERSIDE_GENERATORS = [ + // 超小型稀疏草(2-3 体素高,最高权重) + createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, // 微型草丝 x7 + createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, // 微型草皮 x5 + createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, // 散落草芽 x5 + createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, // 河岸微草 x6 + + // 普通草类(高权重) + createThinGrass, createThinGrass, createThinGrass, createThinGrass, createThinGrass, // 细叶草 x5 + createWideGrass, createWideGrass, createWideGrass, createWideGrass, // 宽叶草 x4 + createWatersideGrass, createWatersideGrass, createWatersideGrass, createWatersideGrass, createWatersideGrass, // 水边细草 x5 + createTallGrass, createTallGrass, createTallGrass, // 高草丛 x3 + createDenseGrassCover, createDenseGrassCover, createDenseGrassCover, createDenseGrassCover, // 密集草皮 x4 + createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, // 小草簇 x5 + createGrassCluster, createGrassCluster, createGrassCluster, createGrassCluster, // 草簇群落 x4 + createRiparianGrass, createRiparianGrass, createRiparianGrass, createRiparianGrass, // 河岸草丛 x4 + + // 水生草类 + createSedge, createSedge, createSedge, createSedge, createSedge, // 莎草 x5 + createBulrush, createBulrush, createBulrush, createBulrush, // 三棱草 x4 + + // 苔藓 + createRiverMoss, createRiverMoss, createRiverMoss, createRiverMoss, // 苔藓 x4 + + // 高杆植物 + createCattail, createCattail, createCattail, // 香蒲 x3 + createReed, createReed, createReed, createReed, // 芦苇 x4 +]; + +// 近水层植物(距离 2-4):高密度草类 + 灌木 +const NEARWATER_GENERATORS = [ + // 超小型稀疏草(2-3 体素高,高权重) + createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, // 微型草丝 x5 + createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, // 微型草皮 x4 + createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, // 散落草芽 x3 + createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, // 河岸微草 x4 + + // 普通草类(高权重) + createThinGrass, createThinGrass, createThinGrass, createThinGrass, // 细叶草 x4 + createWideGrass, createWideGrass, createWideGrass, // 宽叶草 x3 + createTallGrass, createTallGrass, createTallGrass, // 高草丛 x3 + createDenseGrassCover, createDenseGrassCover, createDenseGrassCover, // 密集草皮 x3 + createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, // 小草簇 x4 + createGrassCluster, createGrassCluster, createGrassCluster, // 草簇群落 x3 + createRiparianGrass, createRiparianGrass, createRiparianGrass, // 河岸草丛 x3 + createSedge, createSedge, createSedge, // 莎草 x3 + createFernNew, createFernNew, createFernNew, // 蕨类 x3 + + // 苔藓 + createRiverMoss, createRiverMoss, createRiverMoss, // 苔藓 x3 + createMossyRock, createMossyRock, // 苔藓石 x2 + + // 灌木和树(较低权重) + createDesertWillow, createDesertWillow, // 沙漠柳 x2 + createTamarisk, // 柽柳 x1 + createOasisBush, // 绿洲灌木 x1 + createDesertShrub, // 戈壁灌木 x1 +]; + +// 过渡层植物(距离 4-7):中高密度草类 +const TRANSITION_GENERATORS = [ + // 超小型稀疏草(2-3 体素高,中权重) + createTinyGrass, createTinyGrass, createTinyGrass, createTinyGrass, // 微型草丝 x4 + createTinyGrassPatch, createTinyGrassPatch, createTinyGrassPatch, // 微型草皮 x3 + createScatteredSprouts, createScatteredSprouts, createScatteredSprouts, // 散落草芽 x3 + + // 普通草类(高权重) + createThinGrass, createThinGrass, createThinGrass, // 细叶草 x3 + createWideGrass, createWideGrass, // 宽叶草 x2 + createTallGrass, createTallGrass, // 高草丛 x2 + createDenseGrassCover, createDenseGrassCover, // 密集草皮 x2 + createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, createSmallGrassTuft, // 小草簇 x4 + createGrassCluster, createGrassCluster, // 草簇群落 x2 + createRiparianGrass, createRiparianGrass, createRiparianGrass, // 河岸草丛 x3 + createSmallWildflower, createSmallWildflower, createSmallWildflower, // 小野花 x3 + + // 灌木(较低权重) + createDesertShrub, createDesertShrub, // 戈壁灌木 x2 + createOasisBush, // 绿洲灌木 x1 + createSaxaul, // 梭梭树 x1 +]; + +// 兼容旧代码的通用河岸植被列表 +const RIPARIAN_GENERATORS = [ + ...WATERSIDE_GENERATORS, + ...NEARWATER_GENERATORS, +]; // --- TEMPERATE FOREST (Qiulin) - Enhanced for Mountainous Majesty --- const createLushOak = (builder: VoxelBuilder, ox: number, oz: number) => { @@ -1241,7 +2456,37 @@ const createBanana = (builder: VoxelBuilder, ox: number, oz: number) => { // --- 4. Generator Registration --- // ========================================= -const DESERT_GENERATORS = [createJointedClassic, createJointedCandelabra, createSaguaro]; +// 沙漠生成器列表(仙人掌 + 非仙人掌戈壁植物) +const DESERT_GENERATORS = [ + // === 仙人掌类 === + createSaguaroClassic, // 经典双臂 Saguaro(32-38 高) + createSaguaroCandelabra, // 烛台型四臂 Saguaro(38-46 高) + createSaguaroGiant, // 巨型 Saguaro(50-60 高) + createSaguaroWild, // 野生随机型(28-38 高) + createSimpleCactus, // 简单柱状(12-18 高) + createBarrelSmall, // 小型仙人球(6 直径) + createBarrelMedium, // 中型仙人球(9 直径) + createBarrelLarge, // 大型仙人球(12 直径) + createDualTrunkCactusNew, // 双主干仙人掌 + createOldManCactus, // 老人柱(白色绒毛) + + // === 非仙人掌植物 === + createJoshuaTree, // 约书亚树 + createOcotillo, // 墨西哥蜡烛木 + createSaxaul, // 梭梭树 + createElephantTree, // 象脚树 + + // === 装饰元素(较低概率) === + createDeadLog, // 枯木 + createDesertRocks, // 风化岩石 + createTumbleweed, // 风滚草 +]; + +// 稀有装饰元素(单独列表,用于特殊生成) +const DESERT_RARE_DECORATIONS = [ + createDesertCrystal, // 沙漠水晶 + createAnimalBones, // 动物骨骼 +]; const FOREST_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine]; const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom]; const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch]; @@ -1281,12 +2526,34 @@ export const generateVegetation = async ( let generators: any[] = []; let density = 0.05; + // ============= 戈壁场景:计算植物数量基准 ============= + // 地图面积 = mapSize² 个 Tile + const mapArea = mapSize * mapSize; + + // 河流长度(微体素数量) + const streamSize = desertContext?.streamDepthMap?.size ?? 0; + + // 非亲水植物目标数量:与地图大小正相关 + // 每 100 个 Tile 生成约 5 株仙人掌类植物 + const desertPlantDensity = 0.05; // 5% + const targetDesertPlants = Math.floor(mapArea * desertPlantDensity); + + // 亲水植物目标数量:与河流长度正相关 + // 每 10 个河流微体素生成约 3 株亲水植物 + const riparianPlantDensity = 0.3; + const targetRiparianPlants = Math.floor(streamSize * riparianPlantDensity); + + if (sceneType === 'desert') { + console.log(`Desert vegetation: map=${mapArea} tiles, stream=${streamSize} voxels`); + console.log(`Target plants: desert=${targetDesertPlants}, riparian=${targetRiparianPlants}`); + } + // Determine generators and density based on scene // Map legacy names if necessary switch (sceneType) { case 'desert': generators = DESERT_GENERATORS; - density = 0.05; // Sparse + density = 0.05; // 基础密度(用于非戈壁场景的兼容) break; case 'mountain': // Mapping Qiulin/Temperate to Mountain/Plains case 'plains': @@ -1313,64 +2580,386 @@ export const generateVegetation = async ( density = 0.1; break; } - - console.log(`Generating vegetation for scene: ${sceneType}, density: ${density}`); - - for (let lx = 0; lx < mapSize; lx++) { - for (let lz = 0; lz < mapSize; lz++) { - // Check valid placement - // For desert: check stone/gobi/stream - // For others: check if surface is appropriate (water/stone checks) - - // Retrieve context data - const stoneHeight = desertContext?.stoneHeight[lx]?.[lz] ?? 0; - const gobiHeight = desertContext?.gobiHeight[lx]?.[lz] ?? 0; - const centerMicroX = lx * 8 + 4; - const centerMicroZ = lz * 8 + 4; - const streamInfo = desertContext?.streamDepthMap.get(`${centerMicroX}|${centerMicroZ}`); - const isStream = (streamInfo?.depth ?? 0) > 0; - - // General exclusion rules - if (stoneHeight > 0 && sceneType !== 'tundra' && sceneType !== 'snowy_mountain') continue; // Allow trees on rocks in tundra sometimes - if (isStream && sceneType !== 'swamp') continue; // Swamp trees grow in water - - // Desert specific exclusion - if (sceneType === 'desert' && gobiHeight > 0) continue; // Avoid gobi tops - - // Get surface height - const terrainX = lx * 4 + 2; // Rough center in 32-grid (8*4=32) - const terrainZ = lz * 4 + 2; - // Map terrain 8x8 to logic... wait heightMap is 8x8 keys? - // heightMap keys are "ix|iz" where ix is 0..mapSize*8 - // We need to find the surface Y at this tile center - const hKey = `${lx*8+4}|${lz*8+4}`; - const terrainY = heightMap.get(hKey); - - if (terrainY === undefined) continue; - - // Convert to tree-grid height (32x32) - // 1 terrain unit (8 micro) = 4 tree units (32 micro) - // surfaceY in tree grid - let surfaceY = (terrainY + 1) * 4; - - // Swamp special: trees can sink a bit or grow on mud - if (sceneType === 'swamp') surfaceY -= 2; - - // Micro coordinates for tree grid (center of tile) - const microX = lx * 32 + 16; - const microZ = lz * 32 + 16; - - if (Math.random() < density) { - const gen = generators[Math.floor(Math.random() * generators.length)]; + + // ============= 戈壁场景专用:收集可放置位置 ============= + // 严格规则: + // 1. 水中不生成任何植物 + // 2. 地图外不生成任何植物 + // 3. 草本植物只能在距离水系 10 个体素以内(河岸区域) + // 4. 仙人掌等非亲水植物只能在河岸区域以外 + + const MAX_RIPARIAN_DIST = 10; // 草本植物最大距离水系的距离(体素) + + const desertPlantPositions: { lx: number; lz: number; surfaceY: number; microX: number; microZ: number }[] = []; + const riparianPositions: { lx: number; lz: number; surfaceY: number; microX: number; microZ: number; distToRiver: number }[] = []; + + // 创建实际有水的位置集合(只有 depth > 0 才是真正有水) + // 河床(streamDepthMap 中但 depth = 0)是可以生成植物的 + const actualWaterPositions = new Set(); + + if (sceneType === 'desert' && desertContext?.streamDepthMap) { + desertContext.streamDepthMap.forEach((value: { depth: number; waterHeight: number }, key: string) => { + // 只有实际有水深的位置才标记为水 + if (value.depth > 0) { + actualWaterPositions.add(key); + } + }); + } + + // 检查某个位置是否在实际有水的区域 + const isInWater = (microX: number, microZ: number): boolean => { + // 检查该位置是否有实际的水 + const checkRadius = 1; // 缩小检查范围,更精确 + for (let dx = -checkRadius; dx <= checkRadius; dx++) { + for (let dz = -checkRadius; dz <= checkRadius; dz++) { + const key = `${Math.floor(microX / 4) + dx}|${Math.floor(microZ / 4) + dz}`; + if (actualWaterPositions.has(key)) { + return true; + } + } + } + return false; + }; + + if (sceneType === 'desert') { + // 收集所有可用位置 + // 河床(没有实际水的区域)可以生成植物 + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const stoneHeight = desertContext?.stoneHeight[lx]?.[lz] ?? 0; + const gobiHeight = desertContext?.gobiHeight[lx]?.[lz] ?? 0; + const centerMicroX = lx * 8 + 4; + const centerMicroZ = lz * 8 + 4; + const streamInfo = desertContext?.streamDepthMap?.get(`${centerMicroX}|${centerMicroZ}`); - // 使用内置的 offset 功能,避免修改方法带来的潜在栈溢出风险 - builder.setOffset(microX, surfaceY, microZ); + // 只有实际有水(depth > 0)的位置才禁止生成植物 + // 河床(depth = 0 或无水)可以生成植物 + const hasActualWater = (streamInfo?.depth ?? 0) > 0; + // 排除条件 + if (stoneHeight > 0) continue; + if (gobiHeight > 0) continue; + if (hasActualWater) continue; // 只排除实际有水的位置,河床可以生成植物! + + const hKey = `${lx*8+4}|${lz*8+4}`; + const terrainY = heightMap.get(hKey); + if (terrainY === undefined) continue; + + const surfaceY = (terrainY + 1) * 4; + const microX = lx * 32 + 16; + const microZ = lz * 32 + 16; + + if (desertContext?.streamDepthMap && desertContext.streamDepthMap.size > 0) { + // 计算到最近水体的距离 + let minDistToRiver = MAX_RIPARIAN_DIST + 10; // 初始化为很大的值 + + for (let dx = -MAX_RIPARIAN_DIST; dx <= MAX_RIPARIAN_DIST; dx++) { + for (let dz = -MAX_RIPARIAN_DIST; dz <= MAX_RIPARIAN_DIST; dz++) { + const checkX = centerMicroX + dx; + const checkZ = centerMicroZ + dz; + if (desertContext.streamDepthMap.has(`${checkX}|${checkZ}`)) { + const dist = Math.sqrt(dx * dx + dz * dz); + if (dist < minDistToRiver) { + minDistToRiver = dist; + } + } + } + } + + if (minDistToRiver <= MAX_RIPARIAN_DIST) { + // 河岸位置(草本植物专用)- 距离水系 10 体素以内 + riparianPositions.push({ lx, lz, surfaceY, microX, microZ, distToRiver: minDistToRiver }); + } else { + // 普通沙漠位置(仙人掌等非亲水植物)- 距离水系 10 体素以外 + desertPlantPositions.push({ lx, lz, surfaceY, microX, microZ }); + } + } else { + // 无河流时,所有位置都是普通沙漠位置 + desertPlantPositions.push({ lx, lz, surfaceY, microX, microZ }); + } + } + } + + console.log(`Available positions: desert=${desertPlantPositions.length}, riparian=${riparianPositions.length} (water excluded, max dist=${MAX_RIPARIAN_DIST})`); + } + + // ============= 戈壁场景:基于数量和种群的植物放置 ============= + if (sceneType === 'desert') { + // 地图边界(微体素坐标) + const mapMinMicro = 0; + const mapMaxMicro = mapSize * 32; + + // 严格边界检查函数 + const isValidPosition = (x: number, z: number): boolean => { + // 检查地图边界 + if (x < mapMinMicro || x >= mapMaxMicro || z < mapMinMicro || z >= mapMaxMicro) { + return false; + } + // 检查是否在水中 + if (isInWater(x, z)) { + return false; + } + return true; + }; + + // 打乱位置数组 + const shuffleArray = (arr: T[]): T[] => { + const result = [...arr]; + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [result[i], result[j]] = [result[j], result[i]]; + } + return result; + }; + + // === 1. 非亲水植物(仙人掌类):只在远离河流的地方生成 === + const shuffledDesertPositions = shuffleArray(desertPlantPositions); + const actualDesertPlants = Math.min(targetDesertPlants, shuffledDesertPositions.length); + let desertPlaced = 0; + + for (let i = 0; i < actualDesertPlants && i < shuffledDesertPositions.length; i++) { + const pos = shuffledDesertPositions[i]; + + // 严格检查位置有效性 + if (!isValidPosition(pos.microX, pos.surfaceY)) continue; + + const gen = DESERT_GENERATORS[Math.floor(Math.random() * DESERT_GENERATORS.length)]; + builder.setOffset(pos.microX, pos.surfaceY, pos.microZ); + try { + gen(builder, 0, 0); + builder.commit(); + desertPlaced++; + } catch (e) { + // 忽略生成错误 + } + } + + // === 2. 河岸植物:种群聚集系统 === + // 微草是最常见的植物,紧贴河道两侧 + const COLONY_TYPES = [ + { + name: '芦苇丛', + generators: [createReed, createReed, createCattail], + maxRadius: 2, // 紧凑的种群 + density: 0.95, + minPlants: 12, maxPlants: 25, + weight: 1 // 权重 + }, + { + name: '莎草群', + generators: [createSedge, createSedge, createBulrush, createTinyGrass], + maxRadius: 2.5, + density: 0.92, + minPlants: 18, maxPlants: 35, + weight: 2 + }, + { + name: '微草地', // 最常见的植物类型 + generators: [createTinyGrass, createTinyGrass, createTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, createTinyGrassPatch, createScatteredSprouts], + maxRadius: 3, // 稍大但仍紧贴河道 + density: 0.98, // 极高密度 + minPlants: 50, maxPlants: 100, // 大量植物 + weight: 6 // 高权重,最常见 + }, + { + name: '密集微草', // 另一种微草变体 + generators: [createTinyGrass, createRiverbankTinyGrass, createScatteredSprouts, createTinyGrassPatch], + maxRadius: 2, + density: 0.95, + minPlants: 40, maxPlants: 80, + weight: 4 + }, + ]; + + // 构建加权选择数组 + const weightedColonyTypes: typeof COLONY_TYPES = []; + for (const type of COLONY_TYPES) { + for (let w = 0; w < type.weight; w++) { + weightedColonyTypes.push(type); + } + } + + // 沿河岸创建种群中心(更多种群,更密集) + const numColonies = Math.max(5, Math.floor(streamSize / 8)); // 每 8 个河流体素约 1 个种群(大幅增加) + const colonies: { x: number; z: number; type: typeof COLONY_TYPES[0]; surfaceY: number }[] = []; + + // 只选择紧贴河流的位置(距离 <= 2) + const veryCloseToRiver = riparianPositions.filter(p => p.distToRiver <= 2); + const shuffledVeryClose = shuffleArray(veryCloseToRiver); + + // 减小间距,允许更多种群(3 Tile 间距) + const minColonySpacing = 3 * 32; + + for (const pos of shuffledVeryClose) { + if (colonies.length >= numColonies) break; + + // 检查与已有种群的距离 + let tooClose = false; + for (const existing of colonies) { + const dx = pos.microX - existing.x; + const dz = pos.microZ - existing.z; + if (dx * dx + dz * dz < minColonySpacing * minColonySpacing) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + // 加权随机选择种群类型(微草概率更高) + const colonyType = weightedColonyTypes[Math.floor(Math.random() * weightedColonyTypes.length)]; + colonies.push({ + x: pos.microX, + z: pos.microZ, + type: colonyType, + surfaceY: pos.surfaceY + }); + } + + console.log(`Created ${colonies.length} plant colonies (target: ${numColonies})`); + + // 为每个种群生成植物(高密度聚集) + let totalColonyPlants = 0; + for (const colony of colonies) { + const plantCount = colony.type.minPlants + Math.floor(Math.random() * (colony.type.maxPlants - colony.type.minPlants)); + + for (let p = 0; p < plantCount; p++) { + // 在种群半径内随机位置(使用高斯分布使中心更密集) + const angle = Math.random() * Math.PI * 2; + // 高斯分布:大部分植物靠近中心 + const gaussianDist = Math.abs((Math.random() + Math.random() + Math.random()) / 3 - 0.5) * 2; + const dist = gaussianDist * colony.type.maxRadius * 32; + + const px = colony.x + Math.cos(angle) * dist; + const pz = colony.z + Math.sin(angle) * dist; + + // 严格检查:地图边界 + 水体 + if (!isValidPosition(px, pz)) continue; + + // 随机选择该种群的生成器 + const gen = colony.type.generators[Math.floor(Math.random() * colony.type.generators.length)]; + + if (Math.random() < colony.type.density) { + builder.setOffset(px, colony.surfaceY, pz); + try { + gen(builder, 0, 0); + builder.commit(); + totalColonyPlants++; + } catch (e) { + // 忽略 + } + } + } + } + + // === 3. 种群之间的填充:靠近水的地方更密集 === + // 微草填充河岸空隙,越靠近水越密 + let scatteredPlants = 0; + + // 按距离排序,优先处理靠近水的位置 + const sortedRiparian = [...riparianPositions].sort((a, b) => a.distToRiver - b.distToRiver); + + for (const pos of sortedRiparian) { + // 检查是否在任何种群范围内 + let inColony = false; + for (const colony of colonies) { + const dx = (pos.microX - colony.x); + const dz = (pos.microZ - colony.z); + const colonyRadiusMicro = colony.type.maxRadius * 32 * 1.5; + if (dx * dx + dz * dz < colonyRadiusMicro * colonyRadiusMicro) { + inColony = true; + break; + } + } + + if (inColony) continue; + + // 距离衰减:紧贴河道的密度很高,远离则快速降低 + // distToRiver = 1 -> 密度 90% + // distToRiver = 3 -> 密度 50% + // distToRiver = 6 -> 密度 15% + // distToRiver = 10 -> 密度 5% + const distFactor = Math.exp(-pos.distToRiver * 0.35); + const baseDensity = 0.95; // 高基础密度 + const scatterDensity = baseDensity * distFactor; + + if (Math.random() < scatterDensity) { + // 微草为主(更多种类) + const scatterGenerators = [ + createTinyGrass, createTinyGrass, createTinyGrass, // 高权重 + createRiverbankTinyGrass, createRiverbankTinyGrass, + createScatteredSprouts, createScatteredSprouts, + createTinyGrassPatch + ]; + const gen = scatterGenerators[Math.floor(Math.random() * scatterGenerators.length)]; + + // 严格检查位置 + if (!isValidPosition(pos.microX, pos.microZ)) continue; + + builder.setOffset(pos.microX, pos.surfaceY, pos.microZ); try { gen(builder, 0, 0); - builder.commit(); // Commit and cull hidden voxels + builder.commit(); + scatteredPlants++; } catch (e) { - console.warn('Tree generation failed at', lx, lz, e); + // 忽略 + } + } + } + + // === 4. 稀有装饰(水晶、骨骼)=== + const targetRareDecorations = Math.floor(mapArea * 0.003); // 降低到 0.3% + const shuffledForRare = shuffleArray(desertPlantPositions); + let rarePlaced = 0; + + for (let i = 0; i < targetRareDecorations && i < shuffledForRare.length; i++) { + const pos = shuffledForRare[i]; + if (!isValidPosition(pos.microX, pos.microZ)) continue; + + const rareGen = DESERT_RARE_DECORATIONS[Math.floor(Math.random() * DESERT_RARE_DECORATIONS.length)]; + builder.setOffset(pos.microX, pos.surfaceY, pos.microZ); + try { + rareGen(builder, 0, 0); + builder.commit(); + rarePlaced++; + } catch (e) { + // 忽略 + } + } + + console.log(`Placed: desert=${desertPlaced}, colonies=${totalColonyPlants}, scattered=${scatteredPlants}, rare=${rarePlaced}`); + + } else { + // ============= 非戈壁场景:使用原有概率生成逻辑 ============= + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const stoneHeight = desertContext?.stoneHeight[lx]?.[lz] ?? 0; + const centerMicroX = lx * 8 + 4; + const centerMicroZ = lz * 8 + 4; + const streamInfo = desertContext?.streamDepthMap.get(`${centerMicroX}|${centerMicroZ}`); + const isStream = (streamInfo?.depth ?? 0) > 0; + + if (stoneHeight > 0 && sceneType !== 'tundra' && sceneType !== 'snowy_mountain') continue; + if (isStream && sceneType !== 'swamp') continue; + + const hKey = `${lx*8+4}|${lz*8+4}`; + const terrainY = heightMap.get(hKey); + if (terrainY === undefined) continue; + + let surfaceY = (terrainY + 1) * 4; + if (sceneType === 'swamp') surfaceY -= 2; + + const microX = lx * 32 + 16; + const microZ = lz * 32 + 16; + + if (Math.random() < density) { + const gen = generators[Math.floor(Math.random() * generators.length)]; + builder.setOffset(microX, surfaceY, microZ); + try { + gen(builder, 0, 0); + builder.commit(); + } catch (e) { + console.warn('Tree generation failed at', lx, lz, e); + } } } } diff --git a/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts index 8263454..507780f 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts @@ -135,8 +135,6 @@ export const generateRockClusters = (params: RockClusterGenerationParams): numbe ); const attemptLimit = maxAttempts ?? Math.max(25, targetClusters * 40); - console.log(`[RockGen] Target clusters: ${targetClusters}, Profile:`, profile); - let placed = 0; let attempts = 0; @@ -179,8 +177,6 @@ export const generateRockClusters = (params: RockClusterGenerationParams): numbe const variant = Math.floor(rand() * 2); - console.log(`[RockGen] Placed cluster ${placed + 1} at (${startX},${startZ}): cells=${clusterCells.length}, above=${aboveGround}, below=${belowGround}`); - for (const cell of clusterCells) { field.stoneHeight[cell.x][cell.z] = aboveGround; field.stoneDepth[cell.x][cell.z] = belowGround; @@ -190,8 +186,6 @@ export const generateRockClusters = (params: RockClusterGenerationParams): numbe placed++; } - console.log(`[RockGen] Finished: ${placed}/${targetClusters} clusters placed after ${attempts} attempts`); - return placed; }; diff --git a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts index 49fdd98..9db213a 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -75,7 +75,7 @@ export type ProgressCallback = (progress: TerrainGenerationProgress) => void; // ============= 地形生成配置 ============= // 地形生成版本号 - 每次修改时改变这个数字,触发地图重新生成 -export const TERRAIN_VERSION = 86; // 优化:河流在同一平面时避开高度边界,保持在平台中心 +export const TERRAIN_VERSION = 110; // 优化:河流转弯处圆角插值,更自然 // 逻辑层面的网格大小(战棋移动格子) export const TILE_SIZE = 1; @@ -100,6 +100,9 @@ export interface VoxelData { heightScale: number; isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素 isRock?: boolean; // 标记是否为石块体素(不可剔除) + isMergedWater?: boolean; // 标记是否为合并的水体几何体 + mergedPositions?: Array<{ ix: number; iy: number; iz: number; flowDirection?: { dx: number; dz: number } }>; // 合并的体素位置集合 + flowDirection?: { dx: number; dz: number }; // 水流方向 } export const MAP_SIZES = { @@ -133,9 +136,159 @@ const buildRockOnlyContext = ( stoneHeight: rockField.stoneHeight, stoneDepth: rockField.stoneDepth, stoneVariant: rockField.stoneVariant, - streamDepthMap: new Map(), + streamDepthMap: new Map(), }); +/** + * 水体素临时数据结构(用于合并前的收集) + */ +interface WaterVoxelTemp { + ix: number; + iy: number; + iz: number; + x: number; + y: number; + z: number; + color: string; + heightScale: number; + flowDirection?: { dx: number; dz: number }; // 水流方向 +} + +/** + * 使用 BFS 找到所有 3D 连通的水体素区域 + * @param waterVoxels 所有水体素的位置集合 + * @returns 连通区域数组,每个区域包含该区域的所有体素 + */ +const findConnectedWaterRegions = (waterVoxels: WaterVoxelTemp[]): WaterVoxelTemp[][] => { + if (waterVoxels.length === 0) return []; + + // 构建快速查找表 + const voxelMap = new Map(); + const keyOf = (ix: number, iy: number, iz: number) => `${ix}|${iy}|${iz}`; + + waterVoxels.forEach(v => { + voxelMap.set(keyOf(v.ix, v.iy, v.iz), v); + }); + + const visited = new Set(); + const regions: WaterVoxelTemp[][] = []; + + // 6个方向的邻居偏移(3D连通性) + const neighborOffsets = [ + [1, 0, 0], [-1, 0, 0], + [0, 1, 0], [0, -1, 0], + [0, 0, 1], [0, 0, -1], + ]; + + // BFS 遍历 + const bfs = (startVoxel: WaterVoxelTemp): WaterVoxelTemp[] => { + const region: WaterVoxelTemp[] = []; + const queue: WaterVoxelTemp[] = [startVoxel]; + const startKey = keyOf(startVoxel.ix, startVoxel.iy, startVoxel.iz); + visited.add(startKey); + + while (queue.length > 0) { + const current = queue.shift()!; + region.push(current); + + // 检查6个邻居 + for (const [dx, dy, dz] of neighborOffsets) { + const neighborKey = keyOf( + current.ix + dx, + current.iy + dy, + current.iz + dz + ); + + if (!visited.has(neighborKey) && voxelMap.has(neighborKey)) { + visited.add(neighborKey); + queue.push(voxelMap.get(neighborKey)!); + } + } + } + + return region; + }; + + // 对每个未访问的体素启动 BFS + for (const voxel of waterVoxels) { + const key = keyOf(voxel.ix, voxel.iy, voxel.iz); + if (!visited.has(key)) { + const region = bfs(voxel); + if (region.length > 0) { + regions.push(region); + } + } + } + + return regions; +}; + +/** + * 将连通的水体素区域合并为单个 VoxelData 对象 + * @param region 连通区域内的所有体素 + * @returns 合并后的 VoxelData 对象 + */ +const mergeWaterRegion = (region: WaterVoxelTemp[]): VoxelData => { + if (region.length === 0) { + throw new Error('Cannot merge empty water region'); + } + + // 如果只有一个体素,直接返回(不需要合并) + if (region.length === 1) { + const single = region[0]; + return { + x: single.x, + y: single.y, + z: single.z, + type: 'water', + color: single.color, + ix: single.ix, + iy: single.iy, + iz: single.iz, + heightScale: single.heightScale, + isMergedWater: false, // 单个体素不需要标记为合并 + }; + } + + // 计算区域的中心位置(用作代表位置) + const avgX = region.reduce((sum, v) => sum + v.x, 0) / region.length; + const avgY = region.reduce((sum, v) => sum + v.y, 0) / region.length; + const avgZ = region.reduce((sum, v) => sum + v.z, 0) / region.length; + + // 选择最接近中心的体素作为代表体素 + const representative = region.reduce((closest, v) => { + const distCurrent = Math.sqrt( + Math.pow(v.x - avgX, 2) + + Math.pow(v.y - avgY, 2) + + Math.pow(v.z - avgZ, 2) + ); + const distClosest = Math.sqrt( + Math.pow(closest.x - avgX, 2) + + Math.pow(closest.y - avgY, 2) + + Math.pow(closest.z - avgZ, 2) + ); + return distCurrent < distClosest ? v : closest; + }, region[0]); + + // 使用代表体素的平均颜色(混合所有颜色) + const avgColor = representative.color; // 简化:使用代表体素的颜色 + + return { + x: representative.x, + y: representative.y, + z: representative.z, + type: 'water', + color: avgColor, + ix: representative.ix, + iy: representative.iy, + iz: representative.iz, + heightScale: representative.heightScale, + isMergedWater: true, + mergedPositions: region.map(v => ({ ix: v.ix, iy: v.iy, iz: v.iz, flowDirection: v.flowDirection })), + flowDirection: representative.flowDirection, // 代表体素的流向 + }; +}; + export const generateTerrain = async ( mapSize: number, seed: string = 'default', @@ -327,8 +480,6 @@ export const generateTerrain = async ( seededRandom, MICRO_SCALE ); - - console.log(`[Terrain] 山地溪流已生成,包含 ${mountainStreamMap.size} 个微体素`); } // ===== 戈壁和巨石风化预处理 ===== @@ -372,7 +523,7 @@ export const generateTerrain = async ( await new Promise(resolve => setTimeout(resolve, 0)); } - const voxels: VoxelData[] = []; + let voxels: VoxelData[] = []; const occupancy = new Set(); const keyOf = (x: number, y: number, z: number) => `${x}|${y}|${z}`; const topColumns = new Map(); @@ -380,6 +531,9 @@ export const generateTerrain = async ( // 记录树木位置 const treePositions: Array<{ x: number, z: number }> = []; + // 临时收集所有水体素(用于后续合并) + const tempWaterVoxels: WaterVoxelTemp[] = []; + // 定义添加体素的函数类型 type AddVoxelFn = ( ix: number, @@ -390,7 +544,31 @@ export const generateTerrain = async ( heightScale?: number ) => void; + // 【新增】记录需要移除的体素位置(用于侧面瀑布蚀刻) + const voxelsToRemove = new Set(); + + // 当前正在处理的微体素的流向(用于水体素) + let currentFlowDirection: { dx: number; dz: number } | undefined = undefined; + const addVoxel: AddVoxelFn = (ix, iy, iz, type, color, heightScale = 1) => { + // 如果是水体素,收集到临时数组而不是立即添加 + if (type === 'water') { + tempWaterVoxels.push({ + ix, + iy, + iz, + x: ix * VOXEL_SIZE, + y: iy * VOXEL_SIZE, + z: iz * VOXEL_SIZE, + color, + heightScale, + flowDirection: currentFlowDirection, // 保存当前流向 + }); + occupancy.add(keyOf(ix, iy, iz)); + return; + } + + // 非水体素正常添加 const voxel: VoxelData = { x: ix * VOXEL_SIZE, y: iy * VOXEL_SIZE, @@ -619,9 +797,17 @@ export const generateTerrain = async ( // 记录原始地表位置(用于石块范围判断) const groundLevelY = worldY; + // 获取溪流信息(戈壁场景或山地场景),用于判断是否在河流区域 + const streamInfo = isDesertScene && desertContext + ? desertContext.streamDepthMap.get(`${ix}|${iz}`) + : undefined; + const mountainStreamInfo = mountainStreamMap?.get(`${ix}|${iz}`); + const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0; + const isInRiverArea = streamDepthMicro > 0; + // SMOOTHING LOGIC: - // 只对非石块区域应用 smoothing,保持石块稳固 - if (!hasRockColumn) { + // 只对非石块、非河流区域应用 smoothing,保持石块和河床稳固对齐 + if (!hasRockColumn && !isInRiverArea) { // Only apply significant noise to Stone/Snow // Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally) if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') { @@ -647,13 +833,6 @@ 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 mountainStreamInfo = mountainStreamMap?.get(`${ix}|${iz}`); - const streamDepthMicro = streamInfo?.depth ?? mountainStreamInfo?.depth ?? 0; - // 只有非河流区域才生成沙丘 if (streamDepthMicro === 0 && isDesertScene && logicType === 'sand' && gobiLogicHeight === 0 && stoneLogicHeight === 0) { // Check if we are near a Gobi hill @@ -706,16 +885,10 @@ export const generateTerrain = async ( // Apply sand mound height to worldY worldY += sandMoundHeight; - // 河流生成:强制使用绝对高度 - // 【方案A + B】如果河流存储了绝对表面高度,直接使用,完全忽略 logicHeight + // 【修复】河床蚀刻:从地表减去河床深度,形成凹陷 if (streamDepthMicro > 0) { - // 保持原有计算逻辑 - worldY = logicHeight * MICRO_SCALE; - } else if (streamDepthMicro > 0) { - // 降级方案(如果没有绝对高度,使用旧逻辑) - const baseWorldY = logicHeight * MICRO_SCALE; - worldY = baseWorldY; - worldY -= streamDepthMicro; + // 河流区域:worldY 是基础地形高度,减去蚀刻深度 + worldY = logicHeight * MICRO_SCALE - streamDepthMicro; } const surfaceY = worldY; @@ -739,31 +912,16 @@ export const generateTerrain = async ( const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange && depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight; - // 【图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'; // 碎石 - } + // 【简化】河床内部渲染:统一泥土材质 + if (mountainStreamInfo && streamDepthMicro > 0) { + // 河床蚀刻深度内 + 下方填充层:统一使用泥土 + if (depth < streamDepthMicro + 5) { + type = 'dirt'; } } - // 戈壁溪流的原有逻辑 + // 【简化】戈壁溪流:统一使用 etched_sand(蚀刻沙) else if (shouldApplyStreamEtching({ streamDepthMicro, depth })) { type = 'etched_sand'; - // CENTER OF STREAM = DARK STONE / GRAVEL - if (streamInfo && streamInfo.isCenter) { - // Simple noise for gravel patchiness - if (pseudoRandom(ix * 0.3 + iz * 0.3) > 0.4) { - type = 'dark_stone'; - } - } } else if (isInStoneRange) { // 巨石风化检查:如果该体素被风化移除,跳过生成 if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) { @@ -917,9 +1075,13 @@ export const generateTerrain = async ( } } + // 【重新启用】水体生成 - 河床调试已完成 if (streamDepthMicro > 0) { // 戈壁溪流水体填充 if (streamInfo?.waterHeight && streamInfo.waterHeight > 0) { + // 设置当前流向(用于水体素) + currentFlowDirection = streamInfo.flowDirection; + for (let h = 1; h <= streamInfo.waterHeight; h++) { // Ensure water doesn't exceed surface (should not happen with waterH calculation, but safe) if (surfaceY + h > surfaceY + streamDepthMicro) break; @@ -939,6 +1101,9 @@ export const generateTerrain = async ( if (mountainStreamInfo?.waterHeight && mountainStreamInfo.waterHeight > 0) { const waterHeight = mountainStreamInfo.waterHeight; + // 设置当前流向(用于水体素) + currentFlowDirection = mountainStreamInfo.flowDirection; + // 普通水面填充 for (let h = 1; h <= waterHeight; h++) { if (surfaceY + h > surfaceY + streamDepthMicro) break; @@ -953,112 +1118,184 @@ export const generateTerrain = async ( ); } - // 瀑布效果:基于相邻微体素的真实高度差填充 - // 【修复】检测相邻河流微体素的绝对高度,填充高度差 - if (mountainStreamInfo && waterHeight > 0) { - const waterTopY = surfaceY + Math.floor(waterHeight); + // 【新增】侧面瀑布填充逻辑 + // 检测相邻微体素,如果有高度落差,在侧面填充水体素 + const checkDirections = [ + { dx: 1, dz: 0 }, + { dx: -1, dz: 0 }, + { dx: 0, dz: 1 }, + { dx: 0, dz: -1 }, + ]; + + for (const dir of checkDirections) { + const nx = ix + dir.dx; + const nz = iz + dir.dz; - // 检测4个方向的相邻微体素 - const checkDirections = [ - { dx: 1, dz: 0 }, - { dx: -1, dz: 0 }, - { dx: 0, dz: 1 }, - { dx: 0, dz: -1 }, - ]; + // 获取相邻微体素的河流信息 + const neighborStreamInfo = mountainStreamMap?.get(`${nx}|${nz}`); - let maxHeightDiff = 0; - - for (const dir of checkDirections) { - const nx = ix + dir.dx; - const nz = iz + dir.dz; + if (neighborStreamInfo && neighborStreamInfo.depth > 0) { + // 计算相邻微体素的逻辑坐标 + const neighborLx = Math.floor(nx / MICRO_SCALE); + const neighborLz = Math.floor(nz / MICRO_SCALE); - const neighborStreamInfo = mountainStreamMap?.get(`${nx}|${nz}`); - - if (neighborStreamInfo) { - // 简化:使用cascadeHeight判断 - const cascadeH = neighborStreamInfo.cascadeHeight || 0; - - if (cascadeH > 0) { - maxHeightDiff = Math.max(maxHeightDiff, cascadeH); - } + // 边界检查 + if (neighborLx < 0 || neighborLx >= mapSize || neighborLz < 0 || neighborLz >= mapSize) { + continue; } - } - - // 如果检测到高度差 > 2,填充侧面瀑布 - if (maxHeightDiff >= 2) { - for (let dy = 1; dy < maxHeightDiff; dy++) { - const cascadeY = waterTopY - dy; + + // 获取相邻的逻辑高度 + const neighborLogicHeight = terrainHeightMap[neighborLx][neighborLz]; + + // 计算相邻的河床表面高度 + const neighborSurfaceY = neighborLogicHeight * MICRO_SCALE - neighborStreamInfo.depth; + + // 如果相邻更低,说明有落差 + if (neighborSurfaceY < surfaceY) { + const heightDiff = surfaceY - neighborSurfaceY; - 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( + // 只处理明显的落差(至少2微体素) + if (heightDiff >= 2) { + // 在当前位置垂直填充水体素,覆盖侧面 + // 从相邻表面+1 到当前表面 + for (let y = Math.floor(neighborSurfaceY) + 1; y <= surfaceY; y++) { + if (y <= MIN_WORLD_Y) { + continue; + } + + const posKey = keyOf(ix, y, iz); + + // 【修改】如果位置已被占用(泥土),标记为需要移除(蚀刻掉) + if (occupancy.has(posKey)) { + occupancy.delete(posKey); + voxelsToRemove.add(posKey); // 标记为需要移除 + } + + // 【修复】移除噪声检查,填满所有侧面瀑布位置 + // 添加水体素 (会重新添加到occupancy中) + addVoxel( ix, - cascadeY, + y, 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 - ); - } - } - } - } + 1 + ); + } + } } } } - // 激流效果:非瀑布段但有一定流速(流向明显) - 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 // 很透明的水花 - ); - } - } + // 【临时注释】瀑布效果 - 用于调试 + /* + 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); + } + } + } + + // 垂直瀑布填充 + 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 // 很透明的水花 + ); + } + } + */ } } @@ -1102,24 +1339,85 @@ export const generateTerrain = async ( reportProgress('postprocessing', 0, '开始地貌后处理'); await new Promise(resolve => setTimeout(resolve, 0)); + // 【新增】移除被侧面瀑布蚀刻掉的泥土体素 + if (voxelsToRemove.size > 0) { + console.log(`[侧面瀑布蚀刻] 移除 ${voxelsToRemove.size} 个被覆盖的泥土体素`); + const originalLength = voxels.length; + voxels = voxels.filter(v => { + const key = keyOf(v.ix, v.iy, v.iz); + return !voxelsToRemove.has(key); + }); + console.log(`[侧面瀑布蚀刻] 实际移除 ${originalLength - voxels.length} 个体素`); + + // 【修复】重建 topColumns 映射,因为 voxels 数组已改变,旧的索引失效 + // 使用临时 Map 存储每个位置的最高点信息 + const heightTracker = new Map(); + voxels.forEach((v, index) => { + const key = `${v.ix}|${v.iz}`; + const existing = heightTracker.get(key); + if (!existing || v.iy > existing.maxY) { + heightTracker.set(key, { maxY: v.iy, index, baseType: v.type }); + } + }); + // 重建 topColumns + topColumns.clear(); + heightTracker.forEach((data, key) => { + topColumns.set(key, { index: data.index, baseType: data.baseType }); + }); + } + + // ===== 水体合并处理:将连通的水体素合并为单一几何体 ===== + reportProgress('postprocessing', 5, '开始合并水体区域'); + await new Promise(resolve => setTimeout(resolve, 0)); + + console.log(`[水体合并] 收集到 ${tempWaterVoxels.length} 个水体素`); + + if (tempWaterVoxels.length > 0) { + // 使用 BFS 找到所有连通区域 + const waterRegions = findConnectedWaterRegions(tempWaterVoxels); + console.log(`[水体合并] 检测到 ${waterRegions.length} 个连通水体区域`); + + // 统计信息 + let totalOriginalVoxels = 0; + let totalMergedVoxels = 0; + + // 合并每个区域并添加到 voxels 数组 + waterRegions.forEach((region, index) => { + totalOriginalVoxels += region.length; + + const mergedVoxel = mergeWaterRegion(region); + voxels.push(mergedVoxel); + totalMergedVoxels++; + + if (index < 5 || region.length > 100) { + // 只打印前5个区域或大型区域的信息 + console.log(`[水体合并] 区域 ${index + 1}: ${region.length} 个体素 -> 1 个合并体素`); + } + }); + + const reductionPercent = ((1 - totalMergedVoxels / totalOriginalVoxels) * 100).toFixed(1); + console.log(`[水体合并] 优化完成: ${totalOriginalVoxels} 个体素 -> ${totalMergedVoxels} 个合并体素 (减少 ${reductionPercent}%)`); + } + + reportProgress('postprocessing', 10, '水体合并完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + // ===== 水体颜色后处理:基于离陆地距离 ===== - // 1. 构建逻辑地形类型映射(基于 topColumns 的 baseType) + // 注意:合并后的水体素已经包含了所有子体素的颜色信息 + // 颜色渲染将在 WaterFlowMesh 中基于 mergedPositions 进行 + // 这里保留逻辑以兼容未合并的水体素(如果有的话) const logicalTerrainMap = new Map(); topColumns.forEach(({ baseType }, key) => { logicalTerrainMap.set(key, baseType); }); - // 2. 为每个水体方块计算到最近陆地的距离 voxels.forEach(voxel => { - if (voxel.type === 'water') { - // 转换到逻辑坐标 + if (voxel.type === 'water' && !voxel.isMergedWater) { + // 只处理未合并的水体素(正常情况下不应该有) const logicX = Math.floor(voxel.ix / TILE_SIZE); const logicZ = Math.floor(voxel.iz / TILE_SIZE); - // BFS 查找最近的陆地(使用逻辑坐标) let minDistToLand = Infinity; - - // 搜索半径(逻辑坐标)- 增加到 12 格 const searchRadius = 12; for (let dx = -searchRadius; dx <= searchRadius; dx++) { for (let dz = -searchRadius; dz <= searchRadius; dz++) { @@ -1127,7 +1425,6 @@ export const generateTerrain = async ( const checkZ = logicZ + dz; const checkType = logicalTerrainMap.get(`${checkX}|${checkZ}`); - // 如果是陆地(非水体) if (checkType && checkType !== 'water') { const dist = Math.sqrt(dx * dx + dz * dz); if (dist < minDistToLand) { @@ -1137,13 +1434,8 @@ export const generateTerrain = async ( } } - // 根据距离重新计算颜色 - // 使用平滑的平方根函数:让浅色区域更长,深色只在远处出现 - // sqrt(x) 函数增长缓慢,适合创建平滑渐变 - const normalizedDist = Math.min(minDistToLand / 12, 1); // 归一化到 0-1 - const effectiveDepth = Math.sqrt(normalizedDist) * 10; // 平方根映射到 0-10 - - // 降低颜色噪声,让渐变更平滑 + const normalizedDist = Math.min(minDistToLand / 12, 1); + const effectiveDepth = Math.sqrt(normalizedDist) * 10; voxel.color = varyColor('water', Math.random() * 0.1, effectiveDepth); } }); @@ -1177,6 +1469,9 @@ export const generateTerrain = async ( // 保留石块体素(不可剔除) if (voxel.isRock) return true; + + // 保留合并的水体素(已经过优化,不需要剔除) + if (voxel.isMergedWater) return true; for (const [dx, dy, dz] of neighborOffsets) { let neighborKey = keyOf(voxel.ix + dx, voxel.iy + dy, voxel.iz + dz); diff --git a/voxel-tactics-horizon/src/features/Map/logic/vegetation.ts b/voxel-tactics-horizon/src/features/Map/logic/vegetation.ts deleted file mode 100644 index d566146..0000000 --- a/voxel-tactics-horizon/src/features/Map/logic/vegetation.ts +++ /dev/null @@ -1,2420 +0,0 @@ -/** - * 植物系统:包含所有植物相关的结构和生成函数 - * - 树木(阔叶树、针叶树、果树) - * - 草类(草丛、高草、蕨类、芦苇) - * - 花朵(小花、中花、大花、花丛) - * - 仙人掌(柱状、L形分枝、仙人球等) - */ - -import type { VoxelType } from './voxelStyles'; - -// ============= 工具函数 ============= - -// 伪随机函数 -export function pseudoRandom(seed: number): number { - const x = Math.sin(seed) * 10000; - return x - Math.floor(x); -} - -// RGB转十六进制 -export const rgbToHex = (r: number, g: number, b: number): string => { - return '#' + [r, g, b].map(x => { - const hex = Math.round(x).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }).join(''); -}; - -// 十六进制转RGB -export const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null; -}; - -// 随机选择 -export const pickRandom = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; - -// ============= 类型定义 ============= - -export interface StructureTemplate { - name: string; - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }>; -} - -export type AddTreeVoxelFn = ( - fineX: number, - fineY: number, - fineZ: number, - type: VoxelType, - color: string, - heightScale?: number -) => void; - - -// Helpers for procedural tree generation -const createTrunk = (height: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - for (let y = 0; y < height; y++) { - blocks.push({ dx: 0, dy: y, dz: 0, type: 'wood' }); - blocks.push({ dx: 1, dy: y, dz: 0, type: 'wood' }); - blocks.push({ dx: 0, dy: y, dz: 1, type: 'wood' }); - blocks.push({ dx: 1, dy: y, dz: 1, type: 'wood' }); - } - return blocks; -}; - -const createSphereCrown = (radius: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 改进的蘑菇伞形树冠 - // 使用更大的高度比例,让顶部更圆润 - const crownHeight = radius * 1.8; // 高度是半径的1.8倍,更高更圆润 - const centerY = yStart + crownHeight * 0.45; // 球心位置稍微靠下,让顶部更圆 - - const rSq = radius * radius; - const range = Math.ceil(radius); - - // 生成球形树冠 - for (let y = -range; y <= Math.ceil(crownHeight); y++) { - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dy = y; - const dz = z - centerZ; - const distSq = dx*dx + dy*dy + dz*dz; - - if (distSq <= rSq) { - const actualY = Math.round(centerY + y); - - // 避免在树干位置生成(底部中心区域) - if (actualY < yStart && Math.abs(x-0.5)<1 && Math.abs(z-0.5)<1) continue; - - // 底部稍微收窄,形成蘑菇伞的效果 - const heightFromCenter = y; - if (heightFromCenter < -radius * 0.5) { - // 底部区域,需要收窄 - const bottomShrink = 0.7; // 底部收窄到70% - const adjustedDistSq = (dx*dx + dz*dz) / (bottomShrink * bottomShrink) + dy*dy; - if (adjustedDistSq > rSq) continue; - } - - blocks.push({ dx: x, dy: actualY, dz: z, type: 'leaves' }); - } - } - } - } - - return blocks; -}; - -const createConeCrown = (radiusBase: number, height: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - for (let y = 0; y < height; y++) { - const progress = y / height; - - // 平滑曲线,顶部1格 - const radiusFactor = Math.pow(1 - progress, 0.75); - - // 顶部强制1格 - const minRadius = progress > 0.92 ? 0.3 : 1; // 0.3确保只有1格 - const currentRadius = Math.max(minRadius, radiusBase * radiusFactor); - const rSq = currentRadius * currentRadius; - - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - // 顶部只保留中心1格 - if (progress > 0.92) { - if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } else { - const edgeFactor = distSq / rSq; - const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; - - if (shouldPlace) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - } - } - return blocks; -}; - -// 创建双层锥形树冠:圣诞树风格,两层紧密相连 -const createDoubleConeCrown = (radiusBase: number, totalHeight: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 两层紧密相连:下层50%,上层50%,无间隙 - const lowerHeight = Math.floor(totalHeight * 0.5); - const upperHeight = totalHeight - lowerHeight; - - // === 下层:大锥形 === - for (let y = 0; y < lowerHeight; y++) { - const progress = y / lowerHeight; - const radiusFactor = Math.pow(1 - progress, 0.7); - const currentRadius = Math.max(1, radiusBase * radiusFactor); - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - const edgeFactor = distSq / rSq; - const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; - - if (shouldPlace) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - } - - // === 上层:小锥形(底部稍大,顶部1格)=== - const upperBaseRadius = radiusBase * 0.6; // 上层底部稍大(从0.45改为0.5) - for (let y = 0; y < upperHeight; y++) { - const progress = y / upperHeight; - const radiusFactor = Math.pow(1 - progress, 0.8); - - // 顶部强制1格 - const minRadius = progress > 0.92 ? 0.3 : 1; // 0.3确保只有1格 - const currentRadius = Math.max(minRadius, upperBaseRadius * radiusFactor); - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - // 顶部只保留中心1格 - if (progress > 0.92) { - if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { - blocks.push({ dx: x, dy: yStart + lowerHeight + y, dz: z, type: 'leaves' }); - } - } else { - const edgeFactor = distSq / rSq; - const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; - - if (shouldPlace) { - blocks.push({ dx: x, dy: yStart + lowerHeight + y, dz: z, type: 'leaves' }); - } - } - } - } - } - } - - return blocks; -}; - -// 创建圆润树冠:顶部自然圆润,不再是平的 -const createRoundedCrown = (radius: number, height: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 蘑菇伞形树冠:底部收窄,顶部圆润 - for (let y = 0; y < height; y++) { - const progress = y / height; - - let layerR: number; - if (progress < 0.3) { - // 底部30%:从小开始,蘑菇柄效果 - layerR = radius * (0.3 + progress * 0.4); // 从半径的30%增长到42% - } else if (progress < 0.7) { - // 中部40%:快速扩大到最大,蘑菇伞的外沿 - const midProgress = (progress - 0.3) / 0.4; - layerR = radius * (0.42 + midProgress * 0.58); // 从42%增长到100% - } else { - // 顶部30%:圆润的球形顶部 - const topProgress = (progress - 0.7) / 0.3; - const sphereFactor = Math.sqrt(1 - topProgress * topProgress); // 球形曲线 - layerR = radius * sphereFactor; - } - - const rSq = layerR * layerR; - const range = Math.ceil(layerR); - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - if (dx*dx + dz*dz <= rSq) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - return blocks; -}; - -// 创建带果实的球形树冠(蘑菇伞形) -const createFruitSphereCrown = (radius: number, yStart: number, fruitColor: 'red' | 'yellow' | 'orange') => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 改进的蘑菇伞形树冠 - const crownHeight = radius * 1.8; - const centerY = yStart + crownHeight * 0.45; - - const rSq = radius * radius; - const range = Math.ceil(radius); - - // 用于存储所有叶子的位置 - const leafPositions: Array<{dx: number, dy: number, dz: number}> = []; - - // 生成球形树冠 - for (let y = -range; y <= Math.ceil(crownHeight); y++) { - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dy = y; - const dz = z - centerZ; - const distSq = dx*dx + dy*dy + dz*dz; - - if (distSq <= rSq) { - const actualY = Math.round(centerY + y); - - if (actualY < yStart && Math.abs(x-0.5)<1 && Math.abs(z-0.5)<1) continue; - - // 底部收窄 - const heightFromCenter = y; - if (heightFromCenter < -radius * 0.5) { - const bottomShrink = 0.7; - const adjustedDistSq = (dx*dx + dz*dz) / (bottomShrink * bottomShrink) + dy*dy; - if (adjustedDistSq > rSq) continue; - } - - blocks.push({ dx: x, dy: actualY, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: actualY, dz: z }); - } - } - } - } - - // 果实颜色 - const fruitRGB = fruitColor === 'red' ? { r: 220, g: 50, b: 50 } - : fruitColor === 'yellow' ? { r: 255, g: 220, b: 50 } - : { r: 255, g: 165, b: 50 }; - const fruitColorHex = rgbToHex(fruitRGB.r, fruitRGB.g, fruitRGB.b); - - // 从叶子中筛选出适合变成果实的位置: - // 1. 在树冠外围(距离中心轴较远) - // 2. 高度在20%-65%之间(避免顶部和底部) - const candidateLeaves = leafPositions.filter(leaf => { - const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); - const relativeHeight = (leaf.dy - yStart) / (crownHeight + 5); - return distFromCenter > radius * 0.6 && relativeHeight >= 0.2 && relativeHeight <= 0.65; - }); - - // 定义8个方向 - const directions = [ - { angle: 0 }, - { angle: Math.PI / 4 }, - { angle: Math.PI / 2 }, - { angle: Math.PI * 3/4 }, - { angle: Math.PI }, - { angle: Math.PI * 5/4 }, - { angle: Math.PI * 3/2 }, - { angle: Math.PI * 7/4 } - ]; - - // 在每个方向选择1-2个叶子变成果实 - const selectedFruits: Array<{dx: number, dy: number, dz: number}> = []; - - directions.forEach(dir => { - const fruitCount = 1 + Math.floor(pseudoRandom(dir.angle * 100) * 2); // 1-2个 - - for (let attempt = 0; attempt < fruitCount; attempt++) { - // 找到该方向附近的候选叶子 - const directionLeaves = candidateLeaves.filter(leaf => { - const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); - let angleDiff = Math.abs(leafAngle - dir.angle); - if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; - return angleDiff < Math.PI / 6; // 30度范围内 - }); - - if (directionLeaves.length === 0) continue; - - // 随机选择一个 - const randomIndex = Math.floor(pseudoRandom(dir.angle * 200 + attempt * 50) * directionLeaves.length); - const selectedLeaf = directionLeaves[randomIndex]; - - // 检查与已选果实的距离 - const tooClose = selectedFruits.some(fruit => { - const dx = fruit.dx - selectedLeaf.dx; - const dy = fruit.dy - selectedLeaf.dy; - const dz = fruit.dz - selectedLeaf.dz; - return dx*dx + dy*dy + dz*dz < 9; // 距离至少3格 - }); - - if (!tooClose) { - selectedFruits.push(selectedLeaf); - } - } - }); - - // 将选中的叶子位置改为果实 - selectedFruits.forEach(fruit => { - // 找到对应的叶子块并修改颜色 - const leafBlock = blocks.find(b => b.dx === fruit.dx && b.dy === fruit.dy && b.dz === fruit.dz); - if (leafBlock) { - leafBlock.type = 'flower'; - leafBlock.color = fruitColorHex; - } - }); - - return blocks; -}; - -// 创建带果实的圆润树冠(蘑菇伞形) -const createFruitRoundedCrown = (radius: number, height: number, yStart: number, fruitColor: 'red' | 'yellow' | 'orange') => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 用于存储所有叶子的位置 - const leafPositions: Array<{dx: number, dy: number, dz: number}> = []; - - // 蘑菇伞形树冠:底部收窄,顶部圆润 - for (let y = 0; y < height; y++) { - const progress = y / height; - - let layerR: number; - if (progress < 0.3) { - layerR = radius * (0.3 + progress * 0.4); - } else if (progress < 0.7) { - const midProgress = (progress - 0.3) / 0.4; - layerR = radius * (0.42 + midProgress * 0.58); - } else { - const topProgress = (progress - 0.7) / 0.3; - const sphereFactor = Math.sqrt(1 - topProgress * topProgress); - layerR = radius * sphereFactor; - } - - const rSq = layerR * layerR; - const range = Math.ceil(layerR); - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - if (dx*dx + dz*dz <= rSq) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: yStart + y, dz: z }); - } - } - } - } - - // 果实颜色 - const fruitRGB = fruitColor === 'red' ? { r: 220, g: 50, b: 50 } - : fruitColor === 'yellow' ? { r: 255, g: 220, b: 50 } - : { r: 255, g: 165, b: 50 }; - const fruitColorHex = rgbToHex(fruitRGB.r, fruitRGB.g, fruitRGB.b); - - // 从叶子中筛选出适合变成果实的位置 - const candidateLeaves = leafPositions.filter(leaf => { - const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); - const relativeHeight = (leaf.dy - yStart) / height; - return distFromCenter > radius * 0.6 && relativeHeight >= 0.25 && relativeHeight <= 0.65; - }); - - // 定义8个方向 - const directions = [ - { angle: 0 }, - { angle: Math.PI / 4 }, - { angle: Math.PI / 2 }, - { angle: Math.PI * 3/4 }, - { angle: Math.PI }, - { angle: Math.PI * 5/4 }, - { angle: Math.PI * 3/2 }, - { angle: Math.PI * 7/4 } - ]; - - // 在每个方向选择1-2个叶子变成果实 - const selectedFruits: Array<{dx: number, dy: number, dz: number}> = []; - - directions.forEach(dir => { - const fruitCount = 1 + Math.floor(pseudoRandom(dir.angle * 100) * 2); - - for (let attempt = 0; attempt < fruitCount; attempt++) { - const directionLeaves = candidateLeaves.filter(leaf => { - const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); - let angleDiff = Math.abs(leafAngle - dir.angle); - if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; - return angleDiff < Math.PI / 6; - }); - - if (directionLeaves.length === 0) continue; - - const randomIndex = Math.floor(pseudoRandom(dir.angle * 200 + attempt * 50) * directionLeaves.length); - const selectedLeaf = directionLeaves[randomIndex]; - - const tooClose = selectedFruits.some(fruit => { - const dx = fruit.dx - selectedLeaf.dx; - const dy = fruit.dy - selectedLeaf.dy; - const dz = fruit.dz - selectedLeaf.dz; - return dx*dx + dy*dy + dz*dz < 9; - }); - - if (!tooClose) { - selectedFruits.push(selectedLeaf); - } - } - }); - - // 将选中的叶子改为果实 - selectedFruits.forEach(fruit => { - const leafBlock = blocks.find(b => b.dx === fruit.dx && b.dy === fruit.dy && b.dz === fruit.dz); - if (leafBlock) { - leafBlock.type = 'flower'; - leafBlock.color = fruitColorHex; - } - }); - - return blocks; -}; - -// 创建带果实的锥形树冠(针叶树 - 褐色松果) -const createFruitConeCrown = (radiusBase: number, height: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 用于存储所有叶子位置 - const leafPositions: Array<{dx: number, dy: number, dz: number}> = []; - - // 首先生成树冠 - for (let y = 0; y < height; y++) { - const progress = y / height; - const radiusFactor = Math.pow(1 - progress, 0.75); - const minRadius = progress > 0.92 ? 0.3 : 1; - const currentRadius = Math.max(minRadius, radiusBase * radiusFactor); - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - if (progress > 0.92) { - if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: yStart + y, dz: z }); - } - } else { - const edgeFactor = distSq / rSq; - const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; - - if (shouldPlace) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: yStart + y, dz: z }); - } - } - } - } - } - } - - // 褐色松果颜色 - const brownColor = rgbToHex(139, 90, 43); - - // 从叶子中筛选出适合变成松果的位置 - const candidateLeaves = leafPositions.filter(leaf => { - const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); - const relativeHeight = (leaf.dy - yStart) / height; - // 计算该高度的树冠半径 - const progress = relativeHeight; - const radiusFactor = Math.pow(1 - progress, 0.75); - const currentRadius = Math.max(1, radiusBase * radiusFactor); - return distFromCenter > currentRadius * 0.6 && relativeHeight >= 0.25 && relativeHeight <= 0.65; - }); - - // 定义8个方向 - const directions = [ - { angle: 0 }, - { angle: Math.PI / 4 }, - { angle: Math.PI / 2 }, - { angle: Math.PI * 3/4 }, - { angle: Math.PI }, - { angle: Math.PI * 5/4 }, - { angle: Math.PI * 3/2 }, - { angle: Math.PI * 7/4 } - ]; - - // 在每个方向选择1个叶子变成松果 - const selectedCones: Array<{dx: number, dy: number, dz: number}> = []; - - directions.forEach(dir => { - // 找到该方向附近的候选叶子 - const directionLeaves = candidateLeaves.filter(leaf => { - const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); - let angleDiff = Math.abs(leafAngle - dir.angle); - if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; - return angleDiff < Math.PI / 6; // 30度范围内 - }); - - if (directionLeaves.length === 0) return; - - // 随机选择一个 - const randomIndex = Math.floor(pseudoRandom(dir.angle * 200) * directionLeaves.length); - const selectedLeaf = directionLeaves[randomIndex]; - - // 检查与已选松果的距离 - const tooClose = selectedCones.some(cone => { - const dx = cone.dx - selectedLeaf.dx; - const dy = cone.dy - selectedLeaf.dy; - const dz = cone.dz - selectedLeaf.dz; - return dx*dx + dy*dy + dz*dz < 9; // 距离至少3格 - }); - - if (!tooClose) { - selectedCones.push(selectedLeaf); - } - }); - - // 将选中的叶子改为松果 - selectedCones.forEach(cone => { - const leafBlock = blocks.find(b => b.dx === cone.dx && b.dy === cone.dy && b.dz === cone.dz); - if (leafBlock) { - leafBlock.type = 'flower'; - leafBlock.color = brownColor; - } - }); - - return blocks; -}; - -// 创建带果实的双层锥形树冠(针叶树 - 褐色松果) -const createFruitDoubleConeCrown = (radiusBase: number, totalHeight: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - const lowerHeight = Math.floor(totalHeight * 0.5); - const upperHeight = totalHeight - lowerHeight; - - // 用于存储所有叶子位置 - const leafPositions: Array<{dx: number, dy: number, dz: number, layer: 'lower' | 'upper'}> = []; - - // === 下层:大锥形 === - for (let y = 0; y < lowerHeight; y++) { - const progress = y / lowerHeight; - const radiusFactor = Math.pow(1 - progress, 0.7); - const currentRadius = Math.max(1, radiusBase * radiusFactor); - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - const edgeFactor = distSq / rSq; - const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; - - if (shouldPlace) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: yStart + y, dz: z, layer: 'lower' }); - } - } - } - } - } - - // === 上层:小锥形 === - const upperStartY = yStart + lowerHeight; - const upperRadiusBase = radiusBase * 0.65; - - for (let y = 0; y < upperHeight; y++) { - const progress = y / upperHeight; - const radiusFactor = Math.pow(1 - progress, 0.8); - const minRadius = progress > 0.9 ? 0.3 : 1; - const currentRadius = Math.max(minRadius, upperRadiusBase * radiusFactor); - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - if (progress > 0.9) { - if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { - blocks.push({ dx: x, dy: upperStartY + y, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: upperStartY + y, dz: z, layer: 'upper' }); - } - } else { - const edgeFactor = distSq / rSq; - const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; - - if (shouldPlace) { - blocks.push({ dx: x, dy: upperStartY + y, dz: z, type: 'leaves' }); - leafPositions.push({ dx: x, dy: upperStartY + y, dz: z, layer: 'upper' }); - } - } - } - } - } - } - - // 褐色松果颜色 - const brownColor = rgbToHex(139, 90, 43); - - // 从下层叶子中筛选候选位置 - const lowerLeaves = leafPositions.filter(leaf => leaf.layer === 'lower'); - const lowerCandidates = lowerLeaves.filter(leaf => { - const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); - const relativeHeight = (leaf.dy - yStart) / lowerHeight; - const progress = relativeHeight; - const radiusFactor = Math.pow(1 - progress, 0.7); - const currentRadius = Math.max(1, radiusBase * radiusFactor); - return distFromCenter > currentRadius * 0.6 && relativeHeight >= 0.3 && relativeHeight <= 0.65; - }); - - // 从上层叶子中筛选候选位置 - const upperLeaves = leafPositions.filter(leaf => leaf.layer === 'upper'); - const upperCandidates = upperLeaves.filter(leaf => { - const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); - const relativeHeight = (leaf.dy - upperStartY) / upperHeight; - const progress = relativeHeight; - const radiusFactor = Math.pow(1 - progress, 0.8); - const currentRadius = Math.max(1, upperRadiusBase * radiusFactor); - return distFromCenter > currentRadius * 0.6 && relativeHeight >= 0.3 && relativeHeight <= 0.65; - }); - - // 定义8个方向 - const directions = [ - { angle: 0 }, - { angle: Math.PI / 4 }, - { angle: Math.PI / 2 }, - { angle: Math.PI * 3/4 }, - { angle: Math.PI }, - { angle: Math.PI * 5/4 }, - { angle: Math.PI * 3/2 }, - { angle: Math.PI * 7/4 } - ]; - - // 在每个方向选择1个叶子变成松果(下层和上层各一个) - const selectedCones: Array<{dx: number, dy: number, dz: number}> = []; - - // 下层松果 - directions.forEach(dir => { - const directionLeaves = lowerCandidates.filter(leaf => { - const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); - let angleDiff = Math.abs(leafAngle - dir.angle); - if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; - return angleDiff < Math.PI / 6; - }); - - if (directionLeaves.length === 0) return; - - const randomIndex = Math.floor(pseudoRandom(dir.angle * 200) * directionLeaves.length); - const selectedLeaf = directionLeaves[randomIndex]; - - const tooClose = selectedCones.some(cone => { - const dx = cone.dx - selectedLeaf.dx; - const dy = cone.dy - selectedLeaf.dy; - const dz = cone.dz - selectedLeaf.dz; - return dx*dx + dy*dy + dz*dz < 9; - }); - - if (!tooClose) { - selectedCones.push({dx: selectedLeaf.dx, dy: selectedLeaf.dy, dz: selectedLeaf.dz}); - } - }); - - // 上层松果 - directions.forEach(dir => { - const directionLeaves = upperCandidates.filter(leaf => { - const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); - let angleDiff = Math.abs(leafAngle - dir.angle); - if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; - return angleDiff < Math.PI / 6; - }); - - if (directionLeaves.length === 0) return; - - const randomIndex = Math.floor(pseudoRandom(dir.angle * 250) * directionLeaves.length); - const selectedLeaf = directionLeaves[randomIndex]; - - const tooClose = selectedCones.some(cone => { - const dx = cone.dx - selectedLeaf.dx; - const dy = cone.dy - selectedLeaf.dy; - const dz = cone.dz - selectedLeaf.dz; - return dx*dx + dy*dy + dz*dz < 9; - }); - - if (!tooClose) { - selectedCones.push({dx: selectedLeaf.dx, dy: selectedLeaf.dy, dz: selectedLeaf.dz}); - } - }); - - // 将选中的叶子改为松果 - selectedCones.forEach(cone => { - const leafBlock = blocks.find(b => b.dx === cone.dx && b.dy === cone.dy && b.dz === cone.dz); - if (leafBlock) { - leafBlock.type = 'flower'; - leafBlock.color = brownColor; - } - }); - - return blocks; -}; - -// 创建细树干(1x1,用于椰树等) -const createThinTrunk = (height: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - for (let y = 0; y < height; y++) { - blocks.push({ dx: 0, dy: y, dz: 0, type: 'wood' }); - } - return blocks; -}; - -// 创建双细树干(并排,用于椰树) -const createDoubleThinTrunk = (height: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - for (let y = 0; y < height; y++) { - // 左侧树干 - blocks.push({ dx: 0, dy: y, dz: 0, type: 'wood' }); - // 右侧树干(并排) - blocks.push({ dx: 1, dy: y, dz: 0, type: 'wood' }); - } - return blocks; -}; - -// 创建椰树树冠:顶部有多个扇形叶片(双树干版本,中心在两个树干之间) -const createCoconutPalmCrown = (yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 1.0; // 两个树干之间:(0,0) 和 (1,0) 的中心 - const centerZ = 0.5; - - // 创建6-8个扇形叶片,从中心向外辐射 - const frondCount = 6 + Math.floor(Math.random() * 3); // 6-8片 - const baseY = yStart; - - for (let i = 0; i < frondCount; i++) { - const angle = (i / frondCount) * Math.PI * 2; - const frondLength = 5 + Math.random() * 2; // 5-7格长度 - - // 每个叶片是一个扇形,从中心向外延伸 - for (let dist = 0; dist < frondLength; dist++) { - const width = Math.max(1, Math.floor(3 - dist * 0.3)); // 宽度随距离递减 - - // 在垂直于叶片方向创建宽度 - for (let w = -width; w <= width; w++) { - const perpAngle = angle + Math.PI / 2; - const dx = Math.round(centerX + dist * Math.cos(angle) + w * Math.cos(perpAngle) * 0.3); - const dz = Math.round(centerZ + dist * Math.sin(angle) + w * Math.sin(perpAngle) * 0.3); - - // 叶片有轻微的高度变化 - const heightOffset = Math.floor(Math.abs(Math.sin(dist * 0.5)) * 2); - blocks.push({ dx, dy: baseY + heightOffset, dz, type: 'leaves' }); - } - } - } - - // 中心区域填充一些叶子 - for (let x = -1; x <= 1; x++) { - for (let z = -1; z <= 1; z++) { - if (Math.abs(x) + Math.abs(z) <= 1) { - blocks.push({ dx: Math.round(centerX + x), dy: baseY, dz: Math.round(centerZ + z), type: 'leaves' }); - } - } - } - - return blocks; -}; - -// 创建小球形树冠:紧凑的小球 -const createSmallSphereCrown = (radius: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const rSq = radius * radius; - const centerX = 0.5; - const centerZ = 0.5; - const centerY = yStart + radius * 0.6; - - const range = Math.ceil(radius); - for (let y = -range; y <= range; y++) { - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dy = y; - const dz = z - centerZ; - if (dx*dx + dy*dy + dz*dz <= rSq) { - if (y + centerY < yStart && Math.abs(x-0.5)<1 && Math.abs(z-0.5)<1) continue; - blocks.push({ dx: x, dy: Math.round(centerY + y), dz: z, type: 'leaves' }); - } - } - } - } - return blocks; -}; - -// 创建分散的松树树冠:稀疏的针叶,可以看到树干 -const createSparsePineCrown = (radiusBase: number, height: number, yStart: number, density: number = 0.4) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - for (let y = 0; y < height; y++) { - const progress = y / height; - const currentRadius = Math.max(0, radiusBase * (1 - progress * 0.8)); - const rSq = currentRadius * currentRadius; - - const range = Math.ceil(currentRadius); - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - // 根据密度随机决定是否放置叶子 - // 中心区域密度更高,边缘更稀疏 - const distFromCenter = Math.sqrt(distSq); - const centerDensity = distFromCenter < currentRadius * 0.3 ? density * 1.5 : density; - - if (Math.random() < centerDensity) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - } - return blocks; -}; - -// 创建伞形树冠:顶部大而圆润(不再是平的) -const createUmbrellaCrown = (radius: number, height: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 从树干到顶部的过渡,顶部使用球形曲线 - for (let y = 0; y < height; y++) { - const progress = y / height; - - // 底部到中部:快速扩大 - let currentRadius: number; - if (progress < 0.7) { - // 底部到中部:二次增长 - currentRadius = 1 + (radius - 1) * progress * progress / 0.49; - } else { - // 顶部:使用球形曲线,让顶部圆润而不是平的 - const topProgress = (progress - 0.7) / 0.3; // 0-1 - const sphereFactor = Math.sqrt(1 - topProgress * topProgress); // 球形曲线 - currentRadius = radius * sphereFactor; - } - - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - if (dx*dx + dz*dz <= rSq && dx*dx + dz*dz > 1) { // 排除树干区域 - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - - return blocks; -}; - -// 创建圆柱形树冠:垂直的圆柱体,顶部圆润 -const createCylindricalCrown = (radius: number, height: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - for (let y = 0; y < height; y++) { - const progress = y / height; - - // 顶部使用球形曲线,让顶部圆润 - let currentRadius: number; - if (progress < 0.8) { - // 底部到中部:保持圆柱形 - currentRadius = radius; - } else { - // 顶部:使用球形曲线 - const topProgress = (progress - 0.8) / 0.2; // 0-1 - const sphereFactor = Math.sqrt(1 - topProgress * topProgress); - currentRadius = radius * sphereFactor; - } - - const rSq = currentRadius * currentRadius; - const range = Math.ceil(currentRadius); - - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - if (dx*dx + dz*dz <= rSq) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - return blocks; -}; - -// 创建多层球形树冠:多个球形堆叠 -const createMultiSphereCrown = (baseRadius: number, sphereCount: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - let currentY = yStart; - - for (let i = 0; i < sphereCount; i++) { - const radius = baseRadius * (1 - i * 0.15); // 每层稍微小一点 - const rSq = radius * radius; - const centerY = currentY + radius * 0.7; - - const range = Math.ceil(radius); - for (let y = -range; y <= range; y++) { - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dy = y; - const dz = z - centerZ; - if (dx*dx + dy*dy + dz*dz <= rSq) { - const finalY = Math.round(centerY + y); - if (finalY >= currentY) { // 避免与下层重叠太多 - blocks.push({ dx: x, dy: finalY, dz: z, type: 'leaves' }); - } - } - } - } - } - - currentY += radius * 1.2; // 移动到下一层 - } - - return blocks; -}; - -// 创建不规则树冠:随机形状,更自然 -const createIrregularCrown = (baseRadius: number, height: number, yStart: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 使用噪声创建不规则形状 - const noiseSeed = Math.random() * 1000; - const noise = (x: number, y: number, z: number) => { - const val = Math.sin(x * 0.5 + noiseSeed) * Math.cos(y * 0.3) * Math.sin(z * 0.5 + noiseSeed); - return (val + 1) * 0.5; // 归一化到 0-1 - }; - - for (let y = 0; y < height; y++) { - const progress = y / height; - const baseR = baseRadius * (1 - progress * 0.6); - - const range = Math.ceil(baseR * 1.5); - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const dist = Math.sqrt(dx*dx + dz*dz); - - if (dist <= baseR * 1.2) { - // 使用噪声决定是否放置叶子 - const noiseVal = noise(x, y, z); - const threshold = 0.3 + progress * 0.3; // 底部更密集 - - if (noiseVal > threshold && dist < baseR * (0.7 + noiseVal * 0.5)) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); - } - } - } - } - } - - return blocks; -}; - -// 创建草丛:统一的草类生成函数 -const createGrassTuft = (count: number) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - for (let i = 0; i < count; i++) { - const r = Math.random() * 2.5; - const angle = Math.random() * Math.PI * 2; - const dx = Math.round(r * Math.cos(angle)); - const dz = Math.round(r * Math.sin(angle)); - - const height = Math.floor(Math.random() * 3) + 2; - - for (let y = 0; y < height; y++) { - blocks.push({ dx, dy: y, dz, type: 'dark_grass' }); - } - } - return blocks; -}; - -// 创建高草丛:细长的矩形块,高度不同 -const createTallGrass = (count: number = 8) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - for (let i = 0; i < count; i++) { - const r = Math.random() * 2.0; - const angle = Math.random() * Math.PI * 2; - const dx = Math.round(r * Math.cos(angle)); - const dz = Math.round(r * Math.sin(angle)); - - // 高度变化:3-8 个体素 - const height = Math.floor(Math.random() * 5) + 3; - const width = Math.random() < 0.5 ? 1 : 0; // 细长型,偶尔有宽度 - - for (let y = 0; y < height; y++) { - if (width === 0) { - // 单个体素宽 - blocks.push({ dx, dy: y, dz, type: 'dark_grass' }); - } else { - // 偶尔有2个体素宽 - blocks.push({ dx, dy: y, dz, type: 'dark_grass' }); - if (Math.random() < 0.3) { - const offset = Math.random() < 0.5 ? 1 : -1; - blocks.push({ dx: dx + offset, dy: y, dz, type: 'dark_grass' }); - } - } - } - } - return blocks; -}; - -// 创建蕨类植物:中央茎 + 横向分支的叶子 -const createFern = () => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0; - const centerZ = 0; - - // 中央茎:高度 2-4 - const stemHeight = Math.floor(Math.random() * 3) + 2; - for (let y = 0; y < stemHeight; y++) { - blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'dark_grass' }); - } - - // 横向分支:从不同高度向两侧延伸 - const branchCount = Math.floor(Math.random() * 3) + 2; // 2-4 个分支 - for (let i = 0; i < branchCount; i++) { - const branchY = Math.floor(Math.random() * stemHeight) + 1; - const direction = Math.random() * Math.PI * 2; - const branchLength = Math.floor(Math.random() * 2) + 1; // 1-2 个体素长 - - for (let j = 0; j < branchLength; j++) { - const offsetX = Math.round(Math.cos(direction) * j); - const offsetZ = Math.round(Math.sin(direction) * j); - blocks.push({ dx: centerX + offsetX, dy: branchY, dz: centerZ + offsetZ, type: 'dark_grass' }); - } - } - - return blocks; -}; - -// ========== 重写的花朵系统:严格对称 ========== - -// 1. 小花:4片花瓣十字形围绕1格花蕊,无茎干 -const createSmallFlower = (petalColor: string, centerColor: string) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; - const centerX = 0; - const centerZ = 0; - const y = 0; // 直接贴地 - - // 中心花蕊(1格) - blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'flower', color: centerColor }); - - // 四片花瓣:上下左右四个方向(十字形) - blocks.push({ dx: centerX + 1, dy: y, dz: centerZ, type: 'flower', color: petalColor }); // 右 - blocks.push({ dx: centerX - 1, dy: y, dz: centerZ, type: 'flower', color: petalColor }); // 左 - blocks.push({ dx: centerX, dy: y, dz: centerZ + 1, type: 'flower', color: petalColor }); // 前 - blocks.push({ dx: centerX, dy: y, dz: centerZ - 1, type: 'flower', color: petalColor }); // 后 - - return blocks; -}; - -// 2. 中花:8片花瓣围绕1格花蕊(9格正方形),可选2格高度茎干 -const createMediumFlower = (petalColor: string, centerColor: string, hasStem: boolean = true) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; - const centerX = 0; - const centerZ = 0; - let flowerY = 0; - - // 可选茎干(2格高度) - if (hasStem) { - const stemHeight = 2; - for (let y = 0; y < stemHeight; y++) { - blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'flower', color: rgbToHex(100, 200, 100) }); // 恢复原来的亮绿色茎 - } - flowerY = stemHeight; - } - - // 中心花蕊(1格) - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ, type: 'flower', color: centerColor }); - - // 8片花瓣:上下左右 + 四个对角线方向(正方形) - blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ, type: 'flower', color: petalColor }); // 右 - blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ, type: 'flower', color: petalColor }); // 左 - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ + 1, type: 'flower', color: petalColor }); // 前 - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ - 1, type: 'flower', color: petalColor }); // 后 - blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: petalColor }); // 右前 - blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: petalColor }); // 左后 - blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: petalColor }); // 右后 - blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: petalColor }); // 左前 - - return blocks; -}; - -// 3. 小花丛:由两个小花组成,其中一个带3格高度茎干 -const createSmallFlowerCluster = (petalColor1: string, centerColor1: string, petalColor2: string, centerColor2: string) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; - - // 第一朵花:带3格高度茎干 - const stemHeight = 3; - for (let y = 0; y < stemHeight; y++) { - blocks.push({ dx: 0, dy: y, dz: 0, type: 'flower', color: rgbToHex(100, 200, 100) }); // 恢复原来的亮绿色茎 - } - // 花蕊 - blocks.push({ dx: 0, dy: stemHeight, dz: 0, type: 'flower', color: centerColor1 }); - // 花瓣 - blocks.push({ dx: 1, dy: stemHeight, dz: 0, type: 'flower', color: petalColor1 }); - blocks.push({ dx: -1, dy: stemHeight, dz: 0, type: 'flower', color: petalColor1 }); - blocks.push({ dx: 0, dy: stemHeight, dz: 1, type: 'flower', color: petalColor1 }); - blocks.push({ dx: 0, dy: stemHeight, dz: -1, type: 'flower', color: petalColor1 }); - - // 第二朵花:无茎干,贴地,位置偏移 - const offset = 2; - blocks.push({ dx: offset, dy: 0, dz: 0, type: 'flower', color: centerColor2 }); - blocks.push({ dx: offset + 1, dy: 0, dz: 0, type: 'flower', color: petalColor2 }); - blocks.push({ dx: offset - 1, dy: 0, dz: 0, type: 'flower', color: petalColor2 }); - blocks.push({ dx: offset, dy: 0, dz: 1, type: 'flower', color: petalColor2 }); - blocks.push({ dx: offset, dy: 0, dz: -1, type: 'flower', color: petalColor2 }); - - return blocks; -}; - -// 4. 大花:4格高度茎干,多层花瓣 -const createLargeFlower = (innerPetalColor: string, outerPetalColor: string, centerColor: string) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; - const centerX = 0; - const centerZ = 0; - - // 茎干(4格高度) - const stemHeight = 4; - for (let y = 0; y < stemHeight; y++) { - blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'flower', color: rgbToHex(100, 200, 100) }); // 恢复原来的亮绿色茎 - } - - const flowerY = stemHeight; - - // 中心花蕊(1格) - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ, type: 'flower', color: centerColor }); - - // 内层花瓣(4片,十字形) - blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ, type: 'flower', color: innerPetalColor }); - blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ, type: 'flower', color: innerPetalColor }); - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ + 1, type: 'flower', color: innerPetalColor }); - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ - 1, type: 'flower', color: innerPetalColor }); - - // 外层花瓣(8片,包围内层) - blocks.push({ dx: centerX + 2, dy: flowerY, dz: centerZ, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX - 2, dy: flowerY, dz: centerZ, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ + 2, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX, dy: flowerY, dz: centerZ - 2, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: outerPetalColor }); - blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: outerPetalColor }); - - return blocks; -}; - -// 创建芦苇:金黄色细长矩形块 -const createReed = (count: number = 8) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - for (let i = 0; i < count; i++) { - const r = Math.random() * 2.0; - const angle = Math.random() * Math.PI * 2; - const dx = Math.round(r * Math.cos(angle)); - const dz = Math.round(r * Math.sin(angle)); - - // 高度变化:3-8 个体素,细长型 - const height = Math.floor(Math.random() * 5) + 3; - - for (let y = 0; y < height; y++) { - blocks.push({ dx, dy: y, dz, type: 'reed' }); - } - } - return blocks; -}; - -// 辅助函数:添加圆柱形节段(带肋状结构) -const addCylindricalSegment = ( - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, - centerX: number, - centerZ: number, - yStart: number, - height: number, - radius: number, - ribCount: number = 8 -) => { - const rSq = radius * radius; - - for (let y = 0; y < height; y++) { - const range = Math.ceil(radius); - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dz = z - centerZ; - const distSq = dx*dx + dz*dz; - - if (distSq <= rSq) { - // 添加肋状结构:在特定角度创建凹陷 - const angle = Math.atan2(dz, dx); - const ribAngle = (Math.PI * 2) / ribCount; - const ribIndex = Math.floor((angle + Math.PI) / ribAngle); - const ribCenterAngle = ribIndex * ribAngle - Math.PI; - const angleDiff = Math.abs(angle - ribCenterAngle); - - // 肋的凹陷效果:在肋的中心位置稍微缩小半径 - const ribEffect = Math.cos(angleDiff * ribCount / 2) * 0.15; - const effectiveRadius = radius * (1 - ribEffect); - - if (distSq <= effectiveRadius * effectiveRadius) { - blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'cactus' }); - } - } - } - } - } -}; - -// 辅助函数:添加分支(从主干的特定位置) -const addBranch = ( - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, - startX: number, - startZ: number, - startY: number, - direction: number, // 角度(弧度) - length: number, - radius: number, - ribCount: number = 6 -) => { - for (let i = 0; i < length; i++) { - const progress = i / length; - const currentY = startY + i; - const currentX = startX + Math.cos(direction) * progress * 0.3; - const currentZ = startZ + Math.sin(direction) * progress * 0.3; - const currentRadius = radius * (1 - progress * 0.1); // 逐渐变细 - - addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, currentRadius, ribCount); - } -}; - -// 辅助函数:添加L形分枝(先水平延伸,然后向上弯曲) -const addLBranch = ( - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, - startX: number, - startZ: number, - startY: number, - horizontalDirection: number, // 水平方向角度(弧度) - horizontalLength: number, // 水平段长度 - verticalLength: number, // 垂直段长度 - radius: number, - ribCount: number = 6 -) => { - // 水平段:从主干向外水平延伸 - for (let i = 0; i < horizontalLength; i++) { - const progress = i / horizontalLength; - const currentX = startX + Math.cos(horizontalDirection) * progress * 0.5; - const currentZ = startZ + Math.sin(horizontalDirection) * progress * 0.5; - const currentY = startY; - const currentRadius = radius * (1 - progress * 0.05); - - addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, currentRadius, ribCount); - } - - // 垂直段(向上弯曲):从水平段末端开始向上 - const verticalStartX = startX + Math.cos(horizontalDirection) * horizontalLength * 0.5; - const verticalStartZ = startZ + Math.sin(horizontalDirection) * horizontalLength * 0.5; - - for (let i = 0; i < verticalLength; i++) { - const progress = i / verticalLength; - // 向上弯曲:使用正弦函数创建平滑的向上弯曲效果 - const curveOffset = Math.sin(progress * Math.PI * 0.5) * 0.4; // 向上弯曲 - const currentY = startY + i + Math.round(curveOffset); - const currentX = verticalStartX; - const currentZ = verticalStartZ; - const currentRadius = radius * (1 - progress * 0.1); - - addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, currentRadius, ribCount); - } -}; - -// 辅助函数:添加倾斜的圆柱形节段 -const addTiltedCylindricalSegment = ( - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, - startX: number, - startZ: number, - startY: number, - height: number, - radius: number, - tiltAngle: number, // 倾斜角度(弧度) - tiltDirection: number, // 倾斜方向(弧度) - ribCount: number = 8 -) => { - for (let i = 0; i < height; i++) { - const progress = i / height; - const tiltOffset = Math.sin(progress * Math.PI) * tiltAngle; - const currentX = startX + Math.cos(tiltDirection) * tiltOffset; - const currentZ = startZ + Math.sin(tiltDirection) * tiltOffset; - const currentY = startY + i; - - addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, radius, ribCount); - } -}; - -// 辅助函数:添加不规则的圆柱形节段(半径变化) -const addIrregularCylindricalSegment = ( - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, - centerX: number, - centerZ: number, - yStart: number, - height: number, - baseRadius: number, - variation: number = 0.2, // 半径变化幅度 - ribCount: number = 8 -) => { - for (let y = 0; y < height; y++) { - const progress = y / height; - // 使用正弦波创建不规则的半径变化 - const radiusVariation = Math.sin(progress * Math.PI * 3) * variation; - const currentRadius = baseRadius + radiusVariation; - - addCylindricalSegment(blocks, centerX, centerZ, yStart + y, 1, currentRadius, ribCount); - } -}; - -// 辅助函数:添加球形节段 -const addSphericalSegment = ( - blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, - centerX: number, - centerZ: number, - centerY: number, - radius: number, - ribCount: number = 8 -) => { - const rSq = radius * radius; - const range = Math.ceil(radius); - - for (let y = -range; y <= range; y++) { - for (let x = -range; x <= range + 1; x++) { - for (let z = -range; z <= range + 1; z++) { - const dx = x - centerX; - const dy = y; - const dz = z - centerZ; - const distSq = dx*dx + dy*dy + dz*dz; - - if (distSq <= rSq) { - // 添加垂直肋状结构 - const angle = Math.atan2(dz, dx); - const ribAngle = (Math.PI * 2) / ribCount; - const ribIndex = Math.floor((angle + Math.PI) / ribAngle); - const ribCenterAngle = ribIndex * ribAngle - Math.PI; - const angleDiff = Math.abs(angle - ribCenterAngle); - - const ribEffect = Math.cos(angleDiff * ribCount / 2) * 0.12; - const effectiveRadius = radius * (1 - ribEffect); - - if (distSq <= effectiveRadius * effectiveRadius) { - blocks.push({ dx: x, dy: Math.round(centerY + y), dz: z, type: 'cactus' }); - } - } - } - } - } -}; - -// 创建小型仙人掌:简单的柱状,有节段和肋 -const createSmallCactus = () => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 参考图:左侧小型仙人掌,高度约3-5格,1-2个L形分枝 - const mainHeight = 3 + Math.floor(Math.random() * 3); // 3-5格 - const branchCount = Math.random() < 0.5 ? 1 : 2; // 1-2个分枝 - - // 主干:简单的柱状,略微变细 - for (let i = 0; i < mainHeight; i++) { - const progress = i / mainHeight; - const currentRadius = 1.0 - progress * 0.1; // 轻微变细 - addCylindricalSegment(blocks, centerX, centerZ, i, 1, currentRadius, 8); - } - - // 添加L形分枝 - if (branchCount >= 1) { - // 第一个分枝:从主干中部偏下位置 - const branch1Height = Math.floor(mainHeight * 0.4); - const branch1Direction = Math.random() * Math.PI * 2; - const branch1Length = 2 + Math.floor(Math.random() * 2); // 2-3格 - addLBranch(blocks, centerX, centerZ, branch1Height, branch1Direction, 2, branch1Length, 0.8, 6); - } - - if (branchCount >= 2) { - // 第二个分枝:从主干中部偏上位置,方向与第一个相反 - const branch2Height = Math.floor(mainHeight * 0.6); - const branch2Direction = Math.random() * Math.PI * 2; - const branch2Length = 2 + Math.floor(Math.random() * 2); // 2-3格 - addLBranch(blocks, centerX, centerZ, branch2Height, branch2Direction, 2, branch2Length, 0.8, 6); - } - - return blocks; -}; - -// 创建中型仙人掌:参考图中间,高度约6-8格,2-3个分枝 -const createMediumCactus = () => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - const mainHeight = 6 + Math.floor(Math.random() * 3); // 6-8格 - const branchCount = 2 + Math.floor(Math.random() * 2); // 2-3个分枝 - - // 主干:分段,逐渐变细 - for (let i = 0; i < mainHeight; i++) { - const progress = i / mainHeight; - const currentRadius = 1.2 - progress * 0.2; - addCylindricalSegment(blocks, centerX, centerZ, i, 1, currentRadius, 8); - } - - // 添加L形分枝,位置错落有致 - const branchHeights = []; - for (let i = 0; i < branchCount; i++) { - const heightProgress = (i + 1) / (branchCount + 1); // 均匀分布 - branchHeights.push(Math.floor(mainHeight * heightProgress)); - } - - branchHeights.forEach((height, index) => { - const direction = (index * Math.PI * 2) / branchCount + Math.random() * 0.5; - const horizontalLength = 2 + Math.floor(Math.random() * 2); - const verticalLength = 3 + Math.floor(Math.random() * 2); // 3-4格 - addLBranch(blocks, centerX, centerZ, height, direction, horizontalLength, verticalLength, 0.9, 6); - }); - - return blocks; -}; - -// 创建大型仙人掌:参考图右侧,高度约10-12格,3-4个分枝 -const createLargeCactus = () => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - const mainHeight = 10 + Math.floor(Math.random() * 3); // 10-12格 - const branchCount = 3 + Math.floor(Math.random() * 2); // 3-4个分枝 - - // 主干:多段,逐渐变细 - for (let i = 0; i < mainHeight; i++) { - const progress = i / mainHeight; - const currentRadius = 1.4 - progress * 0.3; - addCylindricalSegment(blocks, centerX, centerZ, i, 1, currentRadius, 10); - } - - // 添加L形分枝,位置、长度变化更大 - const branchHeights = []; - for (let i = 0; i < branchCount; i++) { - // 分枝主要集中在上半部分 - const heightProgress = 0.4 + (i / branchCount) * 0.5; - branchHeights.push(Math.floor(mainHeight * heightProgress)); - } - - branchHeights.forEach((height, index) => { - const direction = (index * Math.PI * 2) / branchCount + Math.random() * 0.5; - const horizontalLength = 2 + Math.floor(Math.random() * 2); - const verticalLength = 4 + Math.floor(Math.random() * 3); // 4-6格,长度变化更大 - addLBranch(blocks, centerX, centerZ, height, direction, horizontalLength, verticalLength, 1.0, 8); - }); - - return blocks; -}; - -// ========== 新的仙人掌类型 ========== - -// 创建L形分枝仙人掌:一个主干 + 1-3个L形分枝 -const createLBranchCactus = (branchCount: 1 | 2 | 3 = 2) => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 根据分支数量决定主干大小 - const trunkHeight = branchCount === 1 ? 8 : branchCount === 2 ? 10 : 12; - const trunkRadius = branchCount === 1 ? 1.2 : branchCount === 2 ? 1.3 : 1.4; - - // 主干:多个节段 - const segmentHeight = Math.floor(trunkHeight / 3); - addCylindricalSegment(blocks, centerX, centerZ, 0, segmentHeight, trunkRadius, 8); - addCylindricalSegment(blocks, centerX, centerZ, segmentHeight, segmentHeight, trunkRadius * 0.95, 8); - addCylindricalSegment(blocks, centerX, centerZ, segmentHeight * 2, trunkHeight - segmentHeight * 2, trunkRadius * 0.9, 8); - - // 添加L形分枝 - const branchStartY = Math.floor(trunkHeight * 0.4); // 从主干40%高度开始 - const branchSpacing = Math.floor(trunkHeight * 0.25); // 分枝之间的间距 - - const directions = [ - Math.PI, // 左 - 0, // 右 - Math.PI / 2, // 前 - -Math.PI / 2, // 后 - ]; - - for (let i = 0; i < branchCount; i++) { - const branchY = branchStartY + (i * branchSpacing); - const direction = directions[i % directions.length]; - const horizontalLength = 3 + Math.floor(Math.random() * 2); // 3-4 - const verticalLength = 4 + Math.floor(Math.random() * 2); // 4-5 - const branchRadius = trunkRadius * 0.7; - - addLBranch(blocks, centerX, centerZ, branchY, direction, horizontalLength, verticalLength, branchRadius, 6); - } - - return blocks; -}; - -// 创建双主干仙人掌:两根主干,粗细和高度不同 -const createDualTrunkCactus = () => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 第一根主干:较粗较高 - const trunk1Height = 10 + Math.floor(Math.random() * 3); // 10-12 - const trunk1Radius = 1.3; - const trunk1OffsetX = -0.3; - - addCylindricalSegment(blocks, centerX + trunk1OffsetX, centerZ, 0, Math.floor(trunk1Height / 3), trunk1Radius, 8); - addCylindricalSegment(blocks, centerX + trunk1OffsetX, centerZ, Math.floor(trunk1Height / 3), Math.floor(trunk1Height / 3), trunk1Radius * 0.95, 8); - addCylindricalSegment(blocks, centerX + trunk1OffsetX, centerZ, Math.floor(trunk1Height / 3) * 2, trunk1Height - Math.floor(trunk1Height / 3) * 2, trunk1Radius * 0.9, 8); - - // 第二根主干:较细较矮 - const trunk2Height = 7 + Math.floor(Math.random() * 3); // 7-9 - const trunk2Radius = 1.0; - const trunk2OffsetX = 0.3; - - addCylindricalSegment(blocks, centerX + trunk2OffsetX, centerZ, 0, Math.floor(trunk2Height / 2), trunk2Radius, 6); - addCylindricalSegment(blocks, centerX + trunk2OffsetX, centerZ, Math.floor(trunk2Height / 2), trunk2Height - Math.floor(trunk2Height / 2), trunk2Radius * 0.9, 6); - - return blocks; -}; - -// 改进的仙人球:根据大小在顶部添加装饰 -const createBarrelCactus = (size: 'small' | 'medium' | 'large' = 'medium') => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; - - // 根据大小决定直径(必须是偶数:4、6、8) - let diameter: number; - let height: number; - let ribCount: number; // 垂直棱的数量 - - if (size === 'small') { - diameter = 4; // 直径4格 - height = 4; // 高度4格(球形) - ribCount = 8; // 8条棱 - } else if (size === 'medium') { - diameter = 6; // 直径6格 - height = 6; // 高度6格(球形) - ribCount = 12; // 12条棱 - } else { - diameter = 8; // 直径8格 - height = 8; // 高度8格(球形) - ribCount = 16; // 16条棱 - } - - const radius = diameter / 2; - const centerX = 0; - const centerZ = 0; - - // 为每条棱分配一个角度 - const ribAngles: number[] = []; - for (let i = 0; i < ribCount; i++) { - ribAngles.push((i / ribCount) * Math.PI * 2); - } - - // 逐层生成球形仙人球 - for (let y = 0; y < height; y++) { - // 计算当前层的半径(使用球形公式) - const centerY = height / 2; // 球心在中间 - const dy = y - centerY + 0.5; // 到球心的垂直距离 - const layerRadiusSq = radius * radius - dy * dy; - - if (layerRadiusSq <= 0) continue; // 超出球体范围 - - const layerRadius = Math.sqrt(layerRadiusSq); - - // 生成当前层的所有体素 - for (let x = -Math.ceil(layerRadius); x <= Math.ceil(layerRadius); x++) { - for (let z = -Math.ceil(layerRadius); z <= Math.ceil(layerRadius); z++) { - const distSq = x * x + z * z; - - if (distSq <= layerRadius * layerRadius) { - // 计算该点的角度 - const angle = Math.atan2(z, x); - - // 找到最近的棱 - let minAngleDiff = Math.PI * 2; - for (const ribAngle of ribAngles) { - let angleDiff = Math.abs(angle - ribAngle); - // 处理角度环绕(-π 到 π) - if (angleDiff > Math.PI) { - angleDiff = Math.PI * 2 - angleDiff; - } - minAngleDiff = Math.min(minAngleDiff, angleDiff); - } - - // 根据到最近棱的距离决定是否生成体素 - // 棱附近的概率更高,形成凸起的棱 - const ribThreshold = (Math.PI / ribCount) * 0.6; // 棱的宽度 - const isOnRib = minAngleDiff < ribThreshold; - - // 边缘收缩效果 - const distRatio = Math.sqrt(distSq) / layerRadius; - const shouldPlace = distRatio < 0.95 || (distRatio < 1.0 && isOnRib); - - if (shouldPlace) { - let color: string | undefined; - - // 中型和大型仙人球添加棱的色彩效果 - if (size !== 'small' && isOnRib) { - // 棱上使用稍微深一点的绿色 - color = rgbToHex(90, 140, 70); // 深绿色的棱 - } - - blocks.push({ - dx: centerX + x, - dy: y, - dz: centerZ + z, - type: 'cactus', - ...(color && { color }) - }); - } - } - } - } - } - - // 在顶部中心添加装饰 - const topY = height - 1; - const decorationType = Math.random(); - - if (decorationType < 0.33) { - // 小白点 - blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 255, 255) }); - } else if (decorationType < 0.66) { - // 小红点 - blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 80, 80) }); - } else { - // 小花(对称的4瓣花) - blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 220, 0) }); // 花心 - // 4个对称花瓣 - blocks.push({ dx: centerX + 1, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 192, 203) }); - blocks.push({ dx: centerX - 1, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 192, 203) }); - blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ + 1, type: 'flower', color: rgbToHex(255, 192, 203) }); - blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ - 1, type: 'flower', color: rgbToHex(255, 192, 203) }); - } - - return blocks; -}; - -// 创建不规则矮胖仙人球:不对称形状,红色装饰块嵌入表面 -const createIrregularBarrelCactus = () => { - const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; - const centerX = 0.5; - const centerZ = 0.5; - - // 创建不规则形状:宽度大于高度,不对称 - // 使用多个层创建不规则的矮胖形状 - const layers = [ - { y: 0, radiusX: 2.5, radiusZ: 2.5, offsetX: 0, offsetZ: 0 }, - { y: 1, radiusX: 2.8, radiusZ: 2.6, offsetX: 0.2, offsetZ: -0.1 }, - { y: 2, radiusX: 2.6, radiusZ: 2.7, offsetX: -0.1, offsetZ: 0.15 }, - { y: 3, radiusX: 2.4, radiusZ: 2.5, offsetX: 0.1, offsetZ: -0.1 }, - { y: 4, radiusX: 2.0, radiusZ: 2.2, offsetX: -0.15, offsetZ: 0.1 }, - { y: 5, radiusX: 1.5, radiusZ: 1.8, offsetX: 0.1, offsetZ: 0 }, - ]; - - const cactusPositions = new Set(); - - layers.forEach(layer => { - const rXSq = layer.radiusX * layer.radiusX; - const rZSq = layer.radiusZ * layer.radiusZ; - const rangeX = Math.ceil(layer.radiusX); - const rangeZ = Math.ceil(layer.radiusZ); - - for (let x = -rangeX; x <= rangeX; x++) { - for (let z = -rangeZ; z <= rangeZ; z++) { - const dx = x - centerX - layer.offsetX; - const dz = z - centerZ - layer.offsetZ; - const ellipse = (dx*dx) / rXSq + (dz*dz) / rZSq; - - // 添加一些随机性,使边缘更不规则 - if (ellipse <= 1 && (ellipse < 0.7 || Math.random() > 0.15)) { - const blockX = Math.round(centerX + x); - const blockZ = Math.round(centerZ + z); - blocks.push({ dx: blockX, dy: layer.y, dz: blockZ, type: 'cactus' }); - cactusPositions.add(`${blockX}|${layer.y}|${blockZ}`); - } - } - } - }); - - // 在表面不同位置嵌入红色装饰块(2-3个) - const redBlockCount = 2 + Math.floor(Math.random() * 2); // 2-3个 - const surfacePositions: Array<{ dx: number; dy: number; dz: number }> = []; - - // 收集表面位置 - cactusPositions.forEach(pos => { - const [dx, dy, dz] = pos.split('|').map(Number); - const neighbors = [ - { dx: dx + 1, dy, dz }, - { dx: dx - 1, dy, dz }, - { dx, dy: dy + 1, dz }, - { dx, dy: dy - 1, dz }, - { dx, dy, dz: dz + 1 }, - { dx, dy, dz: dz - 1 }, - ]; - - // 检查是否是表面位置 - let isSurface = false; - for (const neighbor of neighbors) { - if (!cactusPositions.has(`${neighbor.dx}|${neighbor.dy}|${neighbor.dz}`)) { - isSurface = true; - break; - } - } - - if (isSurface) { - surfacePositions.push({ dx, dy, dz }); - } - }); - - // 随机选择表面位置添加红色块 - const selectedPositions = new Set(); - const positionsToReplace: Array<{ dx: number; dy: number; dz: number }> = []; - - for (let i = 0; i < redBlockCount && i < surfacePositions.length; i++) { - let attempts = 0; - let pos; - do { - pos = surfacePositions[Math.floor(Math.random() * surfacePositions.length)]; - attempts++; - } while (selectedPositions.has(`${pos.dx}|${pos.dy}|${pos.dz}`) && attempts < 50); - - if (pos) { - selectedPositions.add(`${pos.dx}|${pos.dy}|${pos.dz}`); - positionsToReplace.push(pos); - } - } - - // 替换选中的位置为红色装饰块 - positionsToReplace.forEach(pos => { - for (let i = 0; i < blocks.length; i++) { - if (blocks[i].dx === pos.dx && blocks[i].dy === pos.dy && blocks[i].dz === pos.dz && blocks[i].type === 'cactus') { - blocks[i] = { dx: pos.dx, dy: pos.dy, dz: pos.dz, type: 'flower', color: rgbToHex(255, 0, 0) }; - break; - } - } - }); - - return blocks; -}; - -const TREE_VARIANTS: StructureTemplate[] = [ - // 1. 灌木 (Size 2x) - { - name: 'shrub', - blocks: [ - ...createTrunk(3), - ...createSphereCrown(3, 2) - ], - }, - // 2. 球形橡树 (Size 2x) - { - name: 'spherical_oak', - blocks: [ - ...createTrunk(8), - ...createSphereCrown(5.5, 6) - ], - }, - // 3. 矮针叶树 (Size 2x) - 单锥形,底部收窄,使用1x1细树干 - { - name: 'short_pine', - blocks: [ - ...createThinTrunk(4), - ...createConeCrown(2.5, 8, 3) // 底部半径从3改为2.5(收窄),顶部1格 - ], - }, - // 4. 高大针叶树 (Size 2x) - 圣诞树风格双层,协调美观 - { - name: 'tall_pine', - blocks: [ - ...createTrunk(10), // 树干10格 - ...createDoubleConeCrown(7, 16, 6) // 底部半径6,树冠16格高,从第6格开始 → 总高22 - ], - }, - // 5. 桦木 (Size 2x) - { - name: 'birch', - blocks: [ - ...createTrunk(12), - ...createRoundedCrown(5, 7, 9) - ], - }, - // 6. 阔叶大树 (Size 2x) - { - name: 'wide_canopy_short', - blocks: [ - ...createTrunk(5), - ...createSphereCrown(7, 4) - ], - }, - // 7. 针叶大树 (Size 2x) - 双层锥形树冠 - { - name: 'wide_conifer_short', - blocks: [ - ...createTrunk(6), - ...createDoubleConeCrown(5, 12, 3) - ], - }, - // 8. 深色草丛装饰 (Size 2x) - { - name: 'grass_tuft', - blocks: createGrassTuft(12), - }, - // 8.1. 高草丛 - { - name: 'tall_grass', - blocks: createTallGrass(8), - }, - // 8.2. 蕨类植物 - { - name: 'fern', - blocks: createFern(), - }, - // 8.3. 芦苇 - { - name: 'reed', - blocks: createReed(8), - }, - // 8.4-8.7. 新的花朵类型:严格对称 - // 小花:红色花瓣 + 黄色花蕊 - { - name: 'small_flower_red_yellow', - blocks: createSmallFlower(rgbToHex(255, 0, 0), rgbToHex(255, 220, 0)), - }, - // 小花:白色花瓣 + 黄色花蕊 - { - name: 'small_flower_white_yellow', - blocks: createSmallFlower(rgbToHex(255, 255, 255), rgbToHex(255, 220, 0)), - }, - // 小花:粉色花瓣 + 白色花蕊 - { - name: 'small_flower_pink_white', - blocks: createSmallFlower(rgbToHex(255, 192, 203), rgbToHex(255, 255, 255)), - }, - // 小花:紫色花瓣 + 黄色花蕊 - { - name: 'small_flower_purple_yellow', - blocks: createSmallFlower(rgbToHex(180, 100, 200), rgbToHex(255, 220, 0)), - }, - // 中花(带茎干):粉色花瓣 + 黄色花蕊 - { - name: 'medium_flower_pink', - blocks: createMediumFlower(rgbToHex(255, 192, 203), rgbToHex(255, 220, 0), true), - }, - // 中花(带茎干):红色花瓣 + 黄色花蕊 - { - name: 'medium_flower_red', - blocks: createMediumFlower(rgbToHex(255, 0, 0), rgbToHex(255, 220, 0), true), - }, - // 中花(带茎干):蓝色花瓣 + 白色花蕊 - { - name: 'medium_flower_blue', - blocks: createMediumFlower(rgbToHex(100, 150, 255), rgbToHex(255, 255, 255), true), - }, - // 中花(带茎干):橙色花瓣 + 黄色花蕊 - { - name: 'medium_flower_orange', - blocks: createMediumFlower(rgbToHex(255, 165, 0), rgbToHex(255, 220, 0), true), - }, - // 小花丛:红+白组合 - { - name: 'flower_cluster_red_white', - blocks: createSmallFlowerCluster( - rgbToHex(255, 0, 0), rgbToHex(255, 220, 0), - rgbToHex(255, 255, 255), rgbToHex(255, 220, 0) - ), - }, - // 小花丛:粉+紫组合 - { - name: 'flower_cluster_pink_purple', - blocks: createSmallFlowerCluster( - rgbToHex(255, 192, 203), rgbToHex(255, 220, 0), - rgbToHex(180, 100, 200), rgbToHex(255, 255, 255) - ), - }, - // 大花:蓝色外层 + 白色内层 + 红色花蕊 - { - name: 'large_flower_blue_white_red', - blocks: createLargeFlower(rgbToHex(255, 255, 255), rgbToHex(100, 150, 255), rgbToHex(255, 0, 0)), - }, - // 大花:粉色外层 + 白色内层 + 黄色花蕊 - { - name: 'large_flower_pink_white_yellow', - blocks: createLargeFlower(rgbToHex(255, 255, 255), rgbToHex(255, 192, 203), rgbToHex(255, 220, 0)), - }, - // 9. 小型仙人掌 - { - name: 'small_cactus', - blocks: createSmallCactus(), - }, - // 10. 中型仙人掌 - { - name: 'medium_cactus', - blocks: createMediumCactus(), - }, - // 11. 大型仙人掌 - { - name: 'large_cactus', - blocks: createLargeCactus(), - }, - // 12. L形分枝仙人掌(1个分支) - { - name: 'l_branch_cactus_1', - blocks: createLBranchCactus(1), - }, - // 13. L形分枝仙人掌(2个分支) - { - name: 'l_branch_cactus_2', - blocks: createLBranchCactus(2), - }, - // 14. L形分枝仙人掌(3个分支) - { - name: 'l_branch_cactus_3', - blocks: createLBranchCactus(3), - }, - // 15. 双主干仙人掌 - { - name: 'dual_trunk_cactus', - blocks: createDualTrunkCactus(), - }, - // 16. 小型仙人球 - { - name: 'small_barrel_cactus', - blocks: createBarrelCactus('small'), - }, - // 17. 中型仙人球 - { - name: 'medium_barrel_cactus', - blocks: createBarrelCactus('medium'), - }, - // 18. 大型仙人球 - { - name: 'large_barrel_cactus', - blocks: createBarrelCactus('large'), - }, - // 19. 不规则矮胖仙人球 - { - name: 'irregular_barrel_cactus', - blocks: createIrregularBarrelCactus(), - }, - // 20. 椰树:双细树干,顶部有扇形叶片 - { - name: 'coconut_palm', - blocks: [ - ...createDoubleThinTrunk(14), - ...createCoconutPalmCrown(12) - ], - }, - // 21. 苹果树:球形树冠,红色果实 - { - name: 'apple_tree', - blocks: [ - ...createTrunk(6), - ...createFruitSphereCrown(5, 5, 'red') - ], - }, - // 22. 橙树:圆润树冠,橙色果实 - { - name: 'orange_tree', - blocks: [ - ...createTrunk(7), - ...createFruitRoundedCrown(4.5, 8, 6, 'orange') - ], - }, - // 23. 柠檬树:球形树冠,黄色果实 - { - name: 'lemon_tree', - blocks: [ - ...createTrunk(5), - ...createFruitSphereCrown(4.5, 4, 'yellow') - ], - }, - // 24. 樱桃树:圆润树冠,红色果实 - { - name: 'cherry_tree', - blocks: [ - ...createTrunk(8), - ...createFruitRoundedCrown(5.5, 7, 7, 'red') - ], - }, - // 25. 桃树:球形树冠,橙色果实 - { - name: 'peach_tree', - blocks: [ - ...createTrunk(6), - ...createFruitSphereCrown(5, 5, 'orange') - ], - }, - // 26. 松果树(矮):单锥形,褐色松果,1x1细树干 - { - name: 'pine_cone_tree_short', - blocks: [ - ...createThinTrunk(6), - ...createFruitConeCrown(3, 10, 5) - ], - }, - // 27. 松果树(高):双层锥形,褐色松果 - { - name: 'pine_cone_tree_tall', - blocks: [ - ...createTrunk(12), - ...createFruitDoubleConeCrown(6, 18, 10) - ], - }, - // 28. 小球形树:紧凑的小球树冠 - { - name: 'small_spherical', - blocks: [ - ...createTrunk(6), - ...createSmallSphereCrown(3.5, 5) - ], - }, - // 27. 分散的松树:稀疏的针叶,可以看到树干 - { - name: 'sparse_pine', - blocks: [ - ...createTrunk(12), - ...createSparsePineCrown(5, 10, 10, 0.35) - ], - }, - // 28. 伞形树:顶部大而扁平 - { - name: 'umbrella_tree', - blocks: [ - ...createTrunk(8), - ...createUmbrellaCrown(6, 5, 6) - ], - }, - // 29. 圆柱形树:垂直的圆柱体树冠 - { - name: 'cylindrical_tree', - blocks: [ - ...createTrunk(10), - ...createCylindricalCrown(4, 8, 8) - ], - }, - // 30. 多层球形树:多个球形堆叠 - { - name: 'multi_sphere_tree', - blocks: [ - ...createTrunk(9), - ...createMultiSphereCrown(4.5, 3, 7) - ], - }, - // 31. 不规则树:随机形状,更自然 - { - name: 'irregular_tree', - blocks: [ - ...createTrunk(7), - ...createIrregularCrown(5, 9, 5) - ], - }, -]; - -// 专门用于添加树木体素的函数,使用16x16高分辨率网格 -// type AddTreeVoxelFn = ... (Removed duplicate definition) - -const placeStructure = ( - template: StructureTemplate, - addTreeVoxel: AddTreeVoxelFn, - baseX: number, - baseY: number, - baseZ: number -) => { - // baseX/baseZ 是地形的微体素坐标 (8x8 grid) - // 转换为高分辨率坐标 (16x16 grid) - // 一个地形微体素 = 2x2 高分辨率微体素 - // 我们将树木生成的原点对齐到这个地形微体素的左下角(0,0) - // 模板内的偏移量 (dx, dy, dz) 现在直接代表高分辨率网格的单位 - const fineBaseX = baseX * 2; - const fineBaseY = baseY * 2; - const fineBaseZ = baseZ * 2; - - template.blocks.forEach(({ dx, dy, dz, type, color: customColor }) => { - const fineX = fineBaseX + dx; - const fineY = fineBaseY + dy; - const fineZ = fineBaseZ + dz; - - // 如果提供了自定义颜色(如花朵),使用自定义颜色;否则使用默认颜色变化 - const color = customColor || '#00FF00'; // 默认绿色,实际颜色会在addTreeVoxel中通过varyColor处理 - - addTreeVoxel(fineX, fineY, fineZ, type, color); - }); -}; - -// ============= 植物分类数组 ============= - -// 阔叶树:包括各种形状的阔叶树和果树 -const LUSH_TREES = TREE_VARIANTS.filter((t) => [ - 'spherical_oak', - 'birch', - 'wide_canopy_short', - 'small_spherical', - 'umbrella_tree', - 'cylindrical_tree', - 'multi_sphere_tree', - 'irregular_tree', - 'coconut_palm', // 椰树 - 'apple_tree', // 苹果树(红色果实) - 'orange_tree', // 橙树(橙色果实) - 'lemon_tree', // 柠檬树(黄色果实) - 'cherry_tree', // 樱桃树(红色果实) - 'peach_tree' // 桃树(橙色果实) -].includes(t.name)); - -// 针叶树:包括各种松树和针叶树及松果树 -const CONIFER_TREES = TREE_VARIANTS.filter((t) => [ - 'short_pine', - 'tall_pine', - 'wide_conifer_short', - 'sparse_pine', // 分散的松树 - 'pine_cone_tree_short', // 矮松果树(1x1树干) - 'pine_cone_tree_tall' // 高松果树 -].includes(t.name)); - -// 草类植物:包括草丛、高草丛、蕨类 -const GRASS_DECO = TREE_VARIANTS.filter((t) => [ - 'grass_tuft', - 'tall_grass', - 'fern' -].includes(t.name)); - -// 花朵植物:包括所有新的严格对称花朵类型 -const FLOWER_STRUCTS = TREE_VARIANTS.filter((t) => [ - 'small_flower_red_yellow', - 'small_flower_white_yellow', - 'small_flower_pink_white', - 'small_flower_purple_yellow', - 'medium_flower_pink', - 'medium_flower_red', - 'medium_flower_blue', - 'medium_flower_orange', - 'flower_cluster_red_white', - 'flower_cluster_pink_purple', - 'large_flower_blue_white_red', - 'large_flower_pink_white_yellow' -].includes(t.name)); - -// 芦苇植物 -const REED_STRUCTS = TREE_VARIANTS.filter((t) => t.name === 'reed'); - -// 仙人掌:包括所有仙人掌变体和带花的仙人掌(移除掌形和球形仙人掌) -const CACTUS_STRUCTS = TREE_VARIANTS.filter((t) => [ - // 核心柱状仙人掌(3种尺寸,带L形分支) - 'small_cactus', // 小型仙人掌 - 'medium_cactus', // 中型仙人掌 - 'large_cactus', // 大型仙人掌 - - // L形分枝仙人掌(经典造型) - 'l_branch_cactus_1', // 1个L形分支 - 'l_branch_cactus_2', // 2个L形分支 - 'l_branch_cactus_3', // 3个L形分支 - - // 双主干(变化造型) - 'dual_trunk_cactus', // 两根不同高度的主干 - - // 仙人球系列(3种尺寸) - 'small_barrel_cactus', // 小型仙人球(带白点或小花) - 'medium_barrel_cactus', // 中型仙人球 - 'large_barrel_cactus', // 大型仙人球 - - // 特殊造型 - 'irregular_barrel_cactus' // 不规则矮胖仙人球(红色装饰) -].includes(t.name)); - - -/** - * 在戈壁地形上生成植被 - * 根据与水源的距离决定植被分布 - * - * Zone 1: 河岸区域 (Riparian Zone) - 靠近河流 (d < 3) - * - 密度:较高 - * - 类型:草丛 (GrassClump) 和 芦苇 (Reed) - * - * Zone 2: 干旱戈壁 (Arid Gobi) - 远离水源 - * - 密度:稀疏 - * - 类型:仙人掌 (Cactus) 和 仙人球 (BarrelCactus) - */ -const placeGobiVegetation = ( - addTreeVoxel: AddTreeVoxelFn, - ix: number, - iy: number, - iz: number, - streamDepthMap: Map | undefined, - densityRoll: number -) => { - // 1. 检测是否靠近河流 (Zone 1) - let isNearRiver = false; - - if (streamDepthMap) { - // 扩大检测范围,确保河岸带更宽,形成明显的绿带 - const checkRange = 7; // 约1个Tile的宽度 - // 检查中心及周边 - for (let dx = -checkRange; dx <= checkRange; dx++) { - for (let dz = -checkRange; dz <= checkRange; dz++) { - // 使用欧几里得距离判断,使分布更自然 (圆形而不是方形) - if (dx*dx + dz*dz <= checkRange*checkRange) { - if (streamDepthMap.has(`${ix + dx}|${iz + dz}`)) { - isNearRiver = true; - break; - } - } - } - if (isNearRiver) break; - } - } - - // 使用坐标生成确定的随机数,用于选择植被类型 - const typeSeed = ix * 12.9898 + iz * 78.233 + iy * 37.719; - const typeRoll = pseudoRandom(typeSeed); - - if (isNearRiver) { - // Zone 1: 河岸区域 (Riparian) - // 密度:高 (30% 概率) - 略微降低密度避免过于拥挤,但保持高密度特征 - if (densityRoll > 0.70) { - // 生成类型:草丛 (60%) 或 芦苇 (40%) - if (typeRoll < 0.6) { - placeStructure(pickRandom(GRASS_DECO), addTreeVoxel, ix, iy, iz); - } else { - placeStructure(pickRandom(REED_STRUCTS), addTreeVoxel, ix, iy, iz); - } - } - } else { - // Zone 2: 干旱戈壁 (Arid) - // 密度:低 (3.5% 概率) - 大幅降低密度以突显荒漠感,与河岸形成鲜明对比 - if (densityRoll > 0.965) { - // 生成类型:主要是仙人掌和仙人球 - placeStructure(pickRandom(CACTUS_STRUCTS), addTreeVoxel, ix, iy, iz); - } - } -}; - -// ============= 导出所有植物相关内容 ============= - -export { - // 树木创建函数 - createTrunk, - createSphereCrown, - createConeCrown, - createDoubleConeCrown, - createRoundedCrown, - createFruitSphereCrown, - createFruitRoundedCrown, - createFruitConeCrown, - createFruitDoubleConeCrown, - createThinTrunk, - createDoubleThinTrunk, - createCoconutPalmCrown, - createSmallSphereCrown, - createSparsePineCrown, - createUmbrellaCrown, - createCylindricalCrown, - createMultiSphereCrown, - createIrregularCrown, - - // 草类函数 - createGrassTuft, - createTallGrass, - createFern, - createReed, - - // 花朵函数 - createSmallFlower, - createMediumFlower, - createSmallFlowerCluster, - createLargeFlower, - - // 仙人掌函数 - addCylindricalSegment, - addBranch, - addLBranch, - addTiltedCylindricalSegment, - addIrregularCylindricalSegment, - addSphericalSegment, - createSmallCactus, - createMediumCactus, - createLargeCactus, - createLBranchCactus, - createDualTrunkCactus, - createBarrelCactus, - createIrregularBarrelCactus, - - // 模板和分类 - TREE_VARIANTS, - LUSH_TREES, - CONIFER_TREES, - GRASS_DECO, - FLOWER_STRUCTS, - REED_STRUCTS, - CACTUS_STRUCTS, - - // 放置函数 - placeStructure, - placeGobiVegetation, -}; - diff --git a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts index 9cbe4d6..cc97ec8 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts @@ -7,6 +7,7 @@ export interface StreamVoxel { depth: number; waterHeight: number; isCenter: boolean; + flowDirection?: { dx: number; dz: number }; // 水流方向 } interface Point { @@ -261,10 +262,39 @@ export const generateDesertRiver = ( const bankDepth = 1; // 浅河床深度 const waterFillHeight = 2; // 水深 - for (const node of bestPath) { + // bestPath 是从终点回溯到起点的,所以反转它得到从起点到终点的顺序 + const forwardPath = [...bestPath].reverse(); + + for (let i = 0; i < forwardPath.length; i++) { + const node = forwardPath[i]; const cx = node.x; const cy = node.y; // cy is Z + // 计算流向:当前节点到下一个节点的方向 + let flowDx = 0; + let flowDz = 0; + if (i < forwardPath.length - 1) { + const nextNode = forwardPath[i + 1]; + flowDx = nextNode.x - node.x; + flowDz = nextNode.y - node.y; // y 是 Z 坐标 + // 归一化 + const flowLen = Math.sqrt(flowDx * flowDx + flowDz * flowDz); + if (flowLen > 0.01) { + flowDx /= flowLen; + flowDz /= flowLen; + } + } else if (i > 0) { + // 最后一个节点,使用前一个节点的方向 + const prevNode = forwardPath[i - 1]; + flowDx = node.x - prevNode.x; + flowDz = node.y - prevNode.y; + const flowLen = Math.sqrt(flowDx * flowDx + flowDz * flowDz); + if (flowLen > 0.01) { + flowDx /= flowLen; + flowDz /= flowLen; + } + } + const scanRad = Math.ceil(bankRadius + 2); for (let dx = -scanRad; dx <= scanRad; dx++) { for (let dy = -scanRad; dy <= scanRad; dy++) { @@ -301,13 +331,77 @@ export const generateDesertRiver = ( depth: newDepth, // 只有深水道才放水,且水位设置为2 (离地表差1格) waterHeight: newDepth === channelDepth ? waterFillHeight : 0, - isCenter + isCenter, + flowDirection: { dx: flowDx, dz: flowDz }, // 添加流向 }); } } } } } + + // 【修复】延伸终点侵蚀:沿着最后一个节点的流向额外渲染 3 个体素,避免最后一排体素未侵蚀 + if (forwardPath.length > 1) { + const lastNode = forwardPath[forwardPath.length - 1]; + const prevNode = forwardPath[forwardPath.length - 2]; + + // 计算终点流向 + let endFlowDx = lastNode.x - prevNode.x; + let endFlowDz = lastNode.y - prevNode.y; + const endFlowLen = Math.sqrt(endFlowDx * endFlowDx + endFlowDz * endFlowDz); + if (endFlowLen > 0.01) { + endFlowDx /= endFlowLen; + endFlowDz /= endFlowLen; + } + + // 沿流向额外延伸 3 个体素 + const extraExtension = 3; + for (let ext = 1; ext <= extraExtension; ext++) { + const extCx = Math.round(lastNode.x + endFlowDx * ext); + const extCy = Math.round(lastNode.y + endFlowDz * ext); + + const scanRad = Math.ceil(bankRadius + 2); + for (let dx = -scanRad; dx <= scanRad; dx++) { + for (let dy = -scanRad; dy <= scanRad; dy++) { + const tx = extCx + dx; + const ty = extCy + dy; + + if (tx < 0 || tx >= width || ty < 0 || ty >= height) continue; + if (getGrid(tx, ty) === 2) continue; + + const key = `${tx}|${ty}`; + const dist = Math.sqrt(dx*dx + dy*dy); + + const noise = noise2D(tx * 0.08, ty * 0.08) * 0.8; + const noisyChannelRad = channelRadius + noise * 0.5; + const noisyBankRad = bankRadius + noise * 1.0; + + let newDepth = 0; + let isCenter = false; + + if (dist < noisyChannelRad) { + newDepth = channelDepth; + isCenter = true; + } else if (dist < noisyBankRad) { + newDepth = bankDepth; + } + + if (newDepth > 0) { + const existing = streamMap.get(key); + if (!existing || newDepth > existing.depth) { + streamMap.set(key, { + depth: newDepth, + waterHeight: newDepth === channelDepth ? waterFillHeight : 0, + isCenter, + flowDirection: { dx: endFlowDx, dz: endFlowDz }, + }); + } + } + } + } + } + } + console.log(`River generated successfully with length ${bestPath.length}`); } else { console.warn("Failed to generate river path after all attempts."); @@ -317,37 +411,123 @@ export const generateDesertRiver = ( }; /** - * 山地溪流生成系统 V1 - * 特点: - * 1. 从高处山泉起点流向低处,符合自然规律 - * 2. 河流宽度 3-6 微体素,河床 8-9 微体素 - * 3. 深度根据宽度动态调整(1.5-3 微体素) - * 4. 严格避开石头方块(stoneHeight > 0 或 stoneDepth > 0) - * 5. 高度落差处生成瀑布效果(密集水体素 + 水花) - * 6. 支持 1-2 条支流系统 - * 7. 起点可以是山泉洞口效果 + * 山地溪流生成系统 V2 - 正交方块河流 + * + * 核心特性: + * 1. 从地图边缘高处流向对向边缘,营造"穿过地图"的视觉效果 + * 2. 严格90度正交方向(沿方块边界),避免斜向 + * 3. 动态宽度:起始8微体素,每下降2格减少2微体素,最小4微体素 + * 4. 碰撞处理:遇到更高方块时旋转90度寻找出路 + * 5. 高度连续性:下降时在侧面填充水体素(瀑布效果) + * 6. 智能支流:碰撞时可能生成支流(最多2条,有生成间隔) + * 7. 严格避障:避开所有石头方块 * * @param mapSize 逻辑地图大小 * @param terrainHeightMap 地形高度图(逻辑坐标) * @param rockContext 山地巨石上下文(用于避障) * @param rng 随机数生成器 * @param microScale 微缩比例(默认 8) - * @returns 溪流深度Map,包含河床、水面、瀑布等信息 + * @returns 溪流体素Map */ export interface MountainStreamVoxel extends StreamVoxel { isCascade?: boolean; // 是否瀑布段 cascadeHeight?: number; // 瀑布落差(微体素) - isSpring?: boolean; // 是否山泉起点 + isSpring?: boolean; // 是否山泉起点(已废弃,保留兼容性) isTributary?: boolean; // 是否支流 flowDirection?: { dx: number; dz: number }; // 流向(未来用于动画) } +/** + * 正交方向(只允许4个方向) + */ +type Direction = 0 | 1 | 2 | 3; +const Direction = { + NORTH: 0 as Direction, // -Z 方向 + EAST: 1 as Direction, // +X 方向 + SOUTH: 2 as Direction, // +Z 方向 + WEST: 3 as Direction, // -X 方向 +} as const; + +/** + * 地图边缘 + */ +type Edge = 0 | 1 | 2 | 3; +const Edge = { + TOP: 0 as Edge, // Z = 0 + RIGHT: 1 as Edge, // X = max + BOTTOM: 2 as Edge, // Z = max + LEFT: 3 as Edge, // X = 0 +} as const; + interface MountainRockContext { stoneHeight: number[][]; stoneDepth: number[][]; stoneVariant: number[][]; } +/** + * 辅助函数:方向转90度 + */ +function rotate90(dir: Direction, clockwise: boolean): Direction { + if (clockwise) { + return ((dir + 1) % 4) as Direction; + } else { + return ((dir + 3) % 4) as Direction; // (dir - 1 + 4) % 4 + } +} + +/** + * 辅助函数:获取方向的单位向量 + */ +function getDirectionVector(dir: Direction): { dx: number; dz: number } { + switch (dir) { + case Direction.NORTH: return { dx: 0, dz: -1 }; + case Direction.EAST: return { dx: 1, dz: 0 }; + case Direction.SOUTH: return { dx: 0, dz: 1 }; + case Direction.WEST: return { dx: -1, dz: 0 }; + default: return { dx: 0, dz: 0 }; + } +} + +/** + * 辅助函数:获取对向边缘 + */ +function getOppositeEdge(edge: Edge): Edge { + switch (edge) { + case Edge.TOP: return Edge.BOTTOM; + case Edge.RIGHT: return Edge.LEFT; + case Edge.BOTTOM: return Edge.TOP; + case Edge.LEFT: return Edge.RIGHT; + default: return Edge.BOTTOM; + } +} + +/** + * 辅助函数:根据边缘确定初始方向 + */ +function getInitialDirectionFromEdge(edge: Edge): Direction { + switch (edge) { + case Edge.TOP: return Direction.SOUTH; // 从上边缘向下流 + case Edge.RIGHT: return Direction.WEST; // 从右边缘向左流 + case Edge.BOTTOM: return Direction.NORTH; // 从下边缘向上流 + case Edge.LEFT: return Direction.EAST; // 从左边缘向右流 + default: return Direction.SOUTH; + } +} + +/** + * 辅助函数:获取边缘名称(用于日志) + */ +function getEdgeName(edge: Edge): string { + switch (edge) { + case Edge.TOP: return 'TOP'; + case Edge.RIGHT: return 'RIGHT'; + case Edge.BOTTOM: return 'BOTTOM'; + case Edge.LEFT: return 'LEFT'; + default: return 'UNKNOWN'; + } +} + export const generateMountainStream = ( mapSize: number, terrainHeightMap: number[][], @@ -358,8 +538,6 @@ export const generateMountainStream = ( const width = mapSize * microScale; const height = mapSize * microScale; - console.log('[MountainStream] 开始生成山地溪流系统...'); - // 1. 初始化噪声生成器 let seedVal = rng() * 10000; const noise2D = createNoise2D(() => { @@ -367,487 +545,1329 @@ export const generateMountainStream = ( return seedVal / 233280; }); - // 2. 构建障碍物网格 + 高度图(微体素精度) - const grid = new Uint8Array(width * height).fill(0); // 0: 可行走, 1: 障碍物 - const heightGrid = new Float32Array(width * height); // 每个微体素的高度 + // 2. 构建逻辑网格(方块级别,而非微体素级别) + // 障碍物网格:0=可通行, 1=有石头障碍 + const grid = new Uint8Array(mapSize * mapSize).fill(0); + // 高度图:逻辑坐标的高度 + const heightMap = terrainHeightMap; - 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 setGrid = (lx: number, lz: number, val: number) => { + if (lx >= 0 && lx < mapSize && lz >= 0 && lz < mapSize) { + grid[lz * mapSize + lx] = Math.max(grid[lz * mapSize + lx], val); } }; - const getGrid = (x: number, z: number): number => { - if (x >= 0 && x < width && z >= 0 && z < height) { - return grid[z * width + x]; + const getGrid = (lx: number, lz: number): number => { + if (lx >= 0 && lx < mapSize && lz >= 0 && lz < mapSize) { + return grid[lz * mapSize + lx]; } 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]; + const getHeight = (lx: number, lz: number): number => { + if (lx >= 0 && lx < mapSize && lz >= 0 && lz < mapSize) { + return heightMap[lx][lz]; } return 0; }; - // 3. 填充障碍物数据和高度图 + // 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); - } + if (hasStone) { + setGrid(lx, lz, 1); } } } - // 4. 选择起点(山泉位置) - // 策略:在高度 > 平均高度 + 阈值 的非石头区域随机选择 + // 4. 选择起点边缘(高处) + // 策略:扫描4条边缘,找到平均高度+阈值以上的位置 const avgHeight = terrainHeightMap.flat().reduce((a, b) => a + b, 0) / (mapSize * mapSize); - const heightThreshold = avgHeight + Math.max(1, avgHeight * 0.3); + const heightThreshold = avgHeight + 2; // 高于平均高度2格 - 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 }); - } - } + interface EdgeCandidate { + edge: Edge; + lx: number; + lz: number; + height: number; } - 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]); - } + const edgeCandidates: EdgeCandidate[] = []; + + // 【修改】定义边缘中间区域:排除角落区域 + // 角落排除范围:边缘的前25%和后25%(例如32格地图,排除前8格和后8格) + const cornerMargin = Math.floor(mapSize * 0.25); + const middleStart = cornerMargin; + const middleEnd = mapSize - cornerMargin; + + console.log(`[起点选择] 地图大小: ${mapSize}, 排除角落范围: ${cornerMargin}, 中间区域: [${middleStart}, ${middleEnd})`); + + // 扫描4条边缘的中间区域 + for (let i = middleStart; i < middleEnd; i++) { + // 上边缘 (Z = 0) + const topH = getHeight(i, 0); + if (topH >= heightThreshold && getGrid(i, 0) === 0) { + edgeCandidates.push({ edge: Edge.TOP, lx: i, lz: 0, height: topH }); } - 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 }); + // 下边缘 (Z = mapSize-1) + const bottomH = getHeight(i, mapSize - 1); + if (bottomH >= heightThreshold && getGrid(i, mapSize - 1) === 0) { + edgeCandidates.push({ edge: Edge.BOTTOM, lx: i, lz: mapSize - 1, height: bottomH }); + } + + // 左边缘 (X = 0) + const leftH = getHeight(0, i); + if (leftH >= heightThreshold && getGrid(0, i) === 0) { + edgeCandidates.push({ edge: Edge.LEFT, lx: 0, lz: i, height: leftH }); + } + + // 右边缘 (X = mapSize-1) + const rightH = getHeight(mapSize - 1, i); + if (rightH >= heightThreshold && getGrid(mapSize - 1, i) === 0) { + edgeCandidates.push({ edge: Edge.RIGHT, lx: mapSize - 1, lz: i, height: rightH }); } } - 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 (edgeCandidates.length === 0) { + console.warn('[MountainStream V2] 未找到合适的边缘起点,降低高度阈值重试(仍限制在中间区域)'); + // 降级:放宽高度条件,但仍然限制在中间区域 + for (let i = middleStart; i < middleEnd; i++) { + const topH = getHeight(i, 0); + if (topH >= avgHeight && getGrid(i, 0) === 0) { + edgeCandidates.push({ edge: Edge.TOP, lx: i, lz: 0, height: topH }); + } + const bottomH = getHeight(i, mapSize - 1); + if (bottomH >= avgHeight && getGrid(i, mapSize - 1) === 0) { + edgeCandidates.push({ edge: Edge.BOTTOM, lx: i, lz: mapSize - 1, height: bottomH }); + } + const leftH = getHeight(0, i); + if (leftH >= avgHeight && getGrid(0, i) === 0) { + edgeCandidates.push({ edge: Edge.LEFT, lx: 0, lz: i, height: leftH }); + } + const rightH = getHeight(mapSize - 1, i); + if (rightH >= avgHeight && getGrid(mapSize - 1, i) === 0) { + edgeCandidates.push({ edge: Edge.RIGHT, lx: mapSize - 1, lz: i, height: rightH }); } } } - if (candidateEnds.length === 0) { - console.error('[MountainStream] 无法找到有效终点,溪流生成失败'); + // 【新增】如果中间区域仍然找不到,再扩大到整个边缘(最后的备用方案) + if (edgeCandidates.length === 0) { + console.warn('[MountainStream V2] 中间区域无合适起点,扩大到整个边缘(排除极端角落)'); + const minMargin = Math.max(3, Math.floor(mapSize * 0.1)); // 至少排除10%的角落 + + for (let i = minMargin; i < mapSize - minMargin; i++) { + const topH = getHeight(i, 0); + if (topH >= avgHeight * 0.8 && getGrid(i, 0) === 0) { + edgeCandidates.push({ edge: Edge.TOP, lx: i, lz: 0, height: topH }); + } + const bottomH = getHeight(i, mapSize - 1); + if (bottomH >= avgHeight * 0.8 && getGrid(i, mapSize - 1) === 0) { + edgeCandidates.push({ edge: Edge.BOTTOM, lx: i, lz: mapSize - 1, height: bottomH }); + } + const leftH = getHeight(0, i); + if (leftH >= avgHeight * 0.8 && getGrid(0, i) === 0) { + edgeCandidates.push({ edge: Edge.LEFT, lx: 0, lz: i, height: leftH }); + } + const rightH = getHeight(mapSize - 1, i); + if (rightH >= avgHeight * 0.8 && getGrid(mapSize - 1, i) === 0) { + edgeCandidates.push({ edge: Edge.RIGHT, lx: mapSize - 1, lz: i, height: rightH }); + } + } + } + + if (edgeCandidates.length === 0) { + console.error('[MountainStream V2] 无法找到任何有效边缘起点'); return new Map(); } - // 选择最低的终点 - candidateEnds.sort((a, b) => a.h - b.h); - const endPoint = candidateEnds[Math.floor(rng() * Math.min(3, candidateEnds.length))]; + // 【修改】选择最高的候选点,并优先选择更接近边缘中心的点 + edgeCandidates.sort((a, b) => { + // 1. 首先按高度排序 + const heightDiff = b.height - a.height; + if (Math.abs(heightDiff) > 1) return heightDiff; + + // 2. 高度相近时,优先选择更接近边缘中心的点 + const centerPos = Math.floor(mapSize / 2); + let aDist = 0, bDist = 0; + + if (a.edge === Edge.TOP || a.edge === Edge.BOTTOM) { + aDist = Math.abs(a.lx - centerPos); + bDist = Math.abs(b.lx - centerPos); + } else { + aDist = Math.abs(a.lz - centerPos); + bDist = Math.abs(b.lz - centerPos); + } + + return aDist - bDist; // 距离中心越近越好 + }); - console.log(`[MountainStream] 选择终点: (${endPoint.x}, ${endPoint.z}), 高度: ${endPoint.h.toFixed(2)}`); + const topCandidates = edgeCandidates.slice(0, Math.min(10, edgeCandidates.length)); + const startCandidate = topCandidates[Math.floor(rng() * topCandidates.length)]; - // 6. 执行 A* 寻路(优先选择高度下降方向) - interface PathNode { - x: number; - z: number; - f: number; - g: number; - h: number; - parent: PathNode | null; - } + const startEdge = startCandidate.edge; + const startPos = { lx: startCandidate.lx, lz: startCandidate.lz }; + const startHeight = startCandidate.height; + const targetEdge = getOppositeEdge(startEdge); + const initialDirection = getInitialDirectionFromEdge(startEdge); - const minPathLength = mapSize * microScale * 0.5; // 最小路径长度 50% - let mainPath: PathNode[] | null = null; + console.log(`[起点选择] 选择边缘: ${getEdgeName(startEdge)}, 位置: (${startPos.lx}, ${startPos.lz}), 高度: ${startHeight}`); - const startNode: PathNode = { - x: startPoint.x, - z: startPoint.z, - f: 0, - g: 0, - h: 0, - parent: null, + // 【新增】验证起点到其他边缘的最短距离 + const distToOtherEdges = { + toTop: startPos.lz, + toBottom: mapSize - 1 - startPos.lz, + toLeft: startPos.lx, + toRight: mapSize - 1 - startPos.lx, }; - const openList = new PriorityQueue() as any; - openList.enqueue(startNode); - const closedSet = new Set(); - const nodeKey = (x: number, z: number) => `${x}|${z}`; + // 排除当前边缘 + if (startEdge === Edge.TOP) delete distToOtherEdges.toTop; + if (startEdge === Edge.BOTTOM) delete distToOtherEdges.toBottom; + if (startEdge === Edge.LEFT) delete distToOtherEdges.toLeft; + if (startEdge === Edge.RIGHT) delete distToOtherEdges.toRight; - // 启发式函数:优先选择高度下降 + 接近终点 - 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; // 强烈倾向于下坡 - }; + const minDistToOtherEdge = Math.min(...Object.values(distToOtherEdges)); + const requiredMinDist = Math.floor(mapSize * 0.3); // 至少30%的地图宽度 - let finalNode: PathNode | null = null; - let iterations = 0; - const maxIterations = width * height * 2; + console.log(`[起点验证] 到其他边缘最短距离: ${minDistToOtherEdge}, 要求最小距离: ${requiredMinDist}`); - 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 (minDistToOtherEdge < requiredMinDist) { + console.warn(`[起点验证] 警告:起点距离其他边缘过近 (${minDistToOtherEdge} < ${requiredMinDist}),河流可能会很短`); + // 注意:这里不强制中断,只是警告,因为在某些地形下可能无法避免 } - 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(); + // 5. 河流延伸主循环 + interface RiverNode { + lx: number; + lz: number; + height: number; + width: number; // 微体素单位 + direction: Direction; + accumulatedDrop: number; // 累计下降高度 } - // 7. 生成支流(1-2条) - const tributaries: PathNode[][] = []; - const numTributaries = Math.floor(rng() * 2) + 1; // 1-2条支流 + const mainPath: RiverNode[] = []; + const tributaries: RiverNode[][] = []; // 支流集合 + let tributaryCount = 0; + const MAX_TRIBUTARIES = 2; + const TRIBUTARY_COOLDOWN = 15; // 【修改】增加冷却距离:15格(避免支流太密集) + let stepsSinceLastTributary = 999; // 初始值很大,允许第一次碰撞就能生成支流 + let lastTributaryDirection: Direction | null = null; // 【新增】记录上一个支流的方向 - 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}`); - } - } + let currentPos = { lx: startPos.lx, lz: startPos.lz }; + let currentHeight = startHeight; + let currentDirection = initialDirection; + let currentWidth = 6; // 起始宽度 6 微体素 + let accumulatedDrop = 0; // 累计下降高度 + let straightSteps = 0; // 【新增】直行步数计数器 + let lastHeight = startHeight; // 【新增】上一步的高度,用于检测高度变化 - // 8. 河床雕刻(主河流 + 支流) + const MAX_STEPS = mapSize * 3; // 防止无限循环 + let steps = 0; + + // 【新增】计算全局最低高度 + const mapMinHeight = Math.min(...terrainHeightMap.flat()); + + // 【新增】访问记录,防止重复访问形成循环 + const visitedPositions = new Set(); + const posKey = (lx: number, lz: number) => `${lx}|${lz}`; + + // 【修复】提前定义 streamMap,避免在辅助函数中使用时出现初始化顺序错误 const streamMap = new Map(); - const carveRiverBed = (path: PathNode[], isTributary: boolean = false) => { + // 添加起点 + mainPath.push({ + lx: currentPos.lx, + lz: currentPos.lz, + height: currentHeight, + width: currentWidth, + direction: currentDirection, + accumulatedDrop: 0, + }); + + // 辅助函数:检查是否到达目标边缘 + const hasReachedTargetEdge = (lx: number, lz: number, edge: Edge): boolean => { + switch (edge) { + case Edge.TOP: return lz === 0; + case Edge.BOTTOM: return lz === mapSize - 1; + case Edge.LEFT: return lx === 0; + case Edge.RIGHT: return lx === mapSize - 1; + default: return false; + } + }; + + // 【修正】辅助函数:严格检测是否在盆地中(真正的低洼处) + // 盆地定义:四个正交方向的邻居都比当前高或有障碍物 + const isInBasin = (pos: { lx: number; lz: number }, currentH: number): boolean => { + const checkDirs = [ + { dx: 1, dz: 0 }, // 东 + { dx: -1, dz: 0 }, // 西 + { dx: 0, dz: 1 }, // 南 + { dx: 0, dz: -1 }, // 北 + ]; + + let blockedCount = 0; // 被阻挡的方向数量(更高或有障碍) + + for (const dir of checkDirs) { + const checkLx = pos.lx + dir.dx; + const checkLz = pos.lz + dir.dz; + + // 边界检查 - 边界视为阻挡 + if (checkLx < 0 || checkLx >= mapSize || checkLz < 0 || checkLz >= mapSize) { + blockedCount++; + continue; + } + + // 障碍物检查 - 有石头视为阻挡 + if (getGrid(checkLx, checkLz) === 1) { + blockedCount++; + continue; + } + + const neighborH = getHeight(checkLx, checkLz); + + // 只有更高的邻居才算阻挡,同高不算 + if (neighborH > currentH) { + blockedCount++; + } + } + + // 严格盆地判定:四个方向全部被阻挡(都更高或有障碍) + return blockedCount >= 4; + }; + + // 【重构】辅助函数:生成自然形状的湖泊 + // 使用极坐标+噪声生成平滑、自然的湖泊形状 + // 特性: + // 1. 微体素级别操作,避免锯齿边界 + // 2. 极坐标 + 噪声生成圆润但不规则的形状 + // 3. 与地形约束取交集 + // 4. 边界有 3-4 微体素的自然抖动,但整体平滑 + // 5. 最大尺寸 24x24 微体素 + // 返回值:{ size: 湖泊逻辑格数量, cells: 湖泊覆盖的逻辑格集合 } + const fillBasinLake = ( + startPos: { lx: number; lz: number }, + basinHeight: number, + streamMap: Map, + microScale: number, + _maxArea: number = 12 // 保留参数兼容性,但内部使用新逻辑 + ): { size: number; cells: Set } => { + // ===== 配置参数 ===== + const MAX_LAKE_SIZE = 24; // 最大尺寸:24x24 微体素 + const BASE_RADIUS_MIN = 6; // 基础半径最小值 + const BASE_RADIUS_MAX = 10; // 基础半径最大值 + const SHAPE_VARIATION = 3; // 形状变化幅度(噪声影响) + const BOUNDARY_JITTER = 3.5; // 边界抖动幅度(3-4 微体素) + const MIN_LAKE_AREA = 30; // 最小湖泊面积(微体素²) + const LAKE_DEPTH = 2; // 湖泊深度 + + // ===== 计算湖泊中心(微体素坐标)===== + // 以河流终点为基础,略微偏移到逻辑格中心 + const centerMx = startPos.lx * microScale + Math.floor(microScale / 2); + const centerMz = startPos.lz * microScale + Math.floor(microScale / 2); + + // ===== 生成基础形状参数 ===== + // 使用噪声生成器创建形状 + const shapeNoiseSeed = (startPos.lx * 1000 + startPos.lz) * 0.1; + + // 随机选择基础半径和椭圆比例 + const baseRadiusX = BASE_RADIUS_MIN + noise2D(shapeNoiseSeed, 0) * (BASE_RADIUS_MAX - BASE_RADIUS_MIN); + const baseRadiusZ = BASE_RADIUS_MIN + noise2D(0, shapeNoiseSeed) * (BASE_RADIUS_MAX - BASE_RADIUS_MIN); + + // 椭圆旋转角度(增加形状多样性) + const rotationAngle = noise2D(shapeNoiseSeed * 2, shapeNoiseSeed * 2) * Math.PI; + + // ===== 辅助函数:检查微体素是否在湖泊形状内 ===== + const isInLakeShape = (mx: number, mz: number): boolean => { + // 相对于中心的坐标 + const dx = mx - centerMx; + const dz = mz - centerMz; + + // 应用旋转变换 + const cos = Math.cos(rotationAngle); + const sin = Math.sin(rotationAngle); + const rotatedDx = dx * cos + dz * sin; + const rotatedDz = -dx * sin + dz * cos; + + // 计算到中心的角度(用于噪声采样) + const angle = Math.atan2(rotatedDz, rotatedDx); + + // 使用低频噪声生成边界变化(保证平滑) + // 噪声频率较低,确保相邻角度的变化连续 + const noiseVal = noise2D( + Math.cos(angle) * 2 + shapeNoiseSeed, + Math.sin(angle) * 2 + shapeNoiseSeed + ); + + // 计算当前角度的半径(椭圆 + 噪声扰动) + const ellipseRadius = 1 / Math.sqrt( + Math.pow(Math.cos(angle) / baseRadiusX, 2) + + Math.pow(Math.sin(angle) / baseRadiusZ, 2) + ); + + // 应用形状变化(噪声)和边界抖动 + const jitterNoise = noise2D(mx * 0.3, mz * 0.3); // 高频噪声用于边界抖动 + const finalRadius = ellipseRadius + noiseVal * SHAPE_VARIATION + jitterNoise * BOUNDARY_JITTER; + + // 计算实际距离 + const distance = Math.sqrt(rotatedDx * rotatedDx + rotatedDz * rotatedDz); + + return distance <= finalRadius; + }; + + // ===== 辅助函数:检查微体素是否满足地形约束 ===== + const meetsTerrainConstraint = (mx: number, mz: number): boolean => { + // 转换为逻辑格坐标 + const lx = Math.floor(mx / microScale); + const lz = Math.floor(mz / microScale); + + // 边界检查 + if (lx < 0 || lx >= mapSize || lz < 0 || lz >= mapSize) { + return false; + } + + // 高度约束:只能在同高度或更低的位置 + const terrainHeight = getHeight(lx, lz); + if (terrainHeight > basinHeight) { + return false; + } + + // 障碍物约束:不能在石头上 + if (getGrid(lx, lz) === 1) { + return false; + } + + return true; + }; + + // ===== 生成湖泊形状(微体素级别)===== + const lakeVoxels = new Set(); + + // 扫描边界框内的所有微体素 + const halfSize = Math.floor(MAX_LAKE_SIZE / 2); + for (let offsetX = -halfSize; offsetX <= halfSize; offsetX++) { + for (let offsetZ = -halfSize; offsetZ <= halfSize; offsetZ++) { + const mx = centerMx + offsetX; + const mz = centerMz + offsetZ; + + // 检查是否在形状内 + if (!isInLakeShape(mx, mz)) { + continue; + } + + // 检查地形约束 + if (!meetsTerrainConstraint(mx, mz)) { + continue; + } + + // 添加到湖泊 + lakeVoxels.add(`${mx}|${mz}`); + } + } + + // ===== 边界平滑处理 ===== + // 移除孤立的单个体素,保持形状连贯 + const smoothedVoxels = new Set(); + const dirs8 = [ + { dx: 1, dz: 0 }, { dx: -1, dz: 0 }, + { dx: 0, dz: 1 }, { dx: 0, dz: -1 }, + { dx: 1, dz: 1 }, { dx: -1, dz: 1 }, + { dx: 1, dz: -1 }, { dx: -1, dz: -1 }, + ]; + + lakeVoxels.forEach(key => { + const [mxStr, mzStr] = key.split('|'); + const mx = parseInt(mxStr); + const mz = parseInt(mzStr); + + // 计算相邻湖泊体素数量 + let neighborCount = 0; + for (const dir of dirs8) { + const neighborKey = `${mx + dir.dx}|${mz + dir.dz}`; + if (lakeVoxels.has(neighborKey)) { + neighborCount++; + } + } + + // 保留至少有 2 个相邻体素的点(避免孤立点) + if (neighborCount >= 2) { + smoothedVoxels.add(key); + } + }); + + // ===== 检查最小面积 ===== + if (smoothedVoxels.size < MIN_LAKE_AREA) { + console.log(`[盆地湖泊] 湖泊面积过小 (${smoothedVoxels.size} < ${MIN_LAKE_AREA}),不生成`); + return { size: 0, cells: new Set() }; + } + + // ===== 写入 streamMap ===== + smoothedVoxels.forEach(key => { + const [mxStr, mzStr] = key.split('|'); + const mx = parseInt(mxStr); + const mz = parseInt(mzStr); + + // 计算到中心的距离(用于深度渐变,可选) + const distToCenter = Math.sqrt( + Math.pow(mx - centerMx, 2) + Math.pow(mz - centerMz, 2) + ); + const maxDist = Math.max(baseRadiusX, baseRadiusZ); + + // 湖泊中心稍深,边缘稍浅(自然过渡) + const depthFactor = 1 - (distToCenter / maxDist) * 0.3; + const actualDepth = Math.max(1, Math.floor(LAKE_DEPTH * depthFactor)); + const waterDepth = actualDepth * 0.9; + + streamMap.set(key, { + depth: actualDepth, + waterHeight: waterDepth, + isCenter: mx === centerMx && mz === centerMz, + isCascade: false, + cascadeHeight: 0, + isSpring: false, + isTributary: false, + flowDirection: { dx: 0, dz: 0 }, + }); + }); + + // 计算实际覆盖的逻辑格数量(用于日志) + const coveredLogicCells = new Set(); + smoothedVoxels.forEach(key => { + const [mxStr, mzStr] = key.split('|'); + const mx = parseInt(mxStr); + const mz = parseInt(mzStr); + const lx = Math.floor(mx / microScale); + const lz = Math.floor(mz / microScale); + coveredLogicCells.add(`${lx}|${lz}`); + }); + + console.log(`[盆地湖泊] 在位置 (${startPos.lx}, ${startPos.lz}) 生成自然湖泊`); + console.log(` - 微体素数量: ${smoothedVoxels.size}`); + console.log(` - 覆盖逻辑格: ${coveredLogicCells.size}`); + console.log(` - 半径: X=${baseRadiusX.toFixed(1)}, Z=${baseRadiusZ.toFixed(1)}`); + console.log(` - 盆地高度: ${basinHeight}`); + + return { size: coveredLogicCells.size, cells: coveredLogicCells }; + }; + + // 【修改】辅助函数:计算到最低且最远边缘的方向 + const getDirectionToLowestFarthestEdge = (pos: { lx: number; lz: number }): Direction => { + // 计算到4个边缘的距离 + const distToTop = pos.lz; + const distToBottom = mapSize - 1 - pos.lz; + const distToLeft = pos.lx; + const distToRight = mapSize - 1 - pos.lx; + + // 计算4个边缘的平均高度(采样边缘中心区域) + const sampleSize = Math.min(10, Math.floor(mapSize / 4)); + + // 顶部边缘平均高度 + let topHeight = 0; + for (let i = -sampleSize; i <= sampleSize; i++) { + const lx = Math.max(0, Math.min(mapSize - 1, pos.lx + i)); + topHeight += getHeight(lx, 0); + } + topHeight /= (sampleSize * 2 + 1); + + // 底部边缘平均高度 + let bottomHeight = 0; + for (let i = -sampleSize; i <= sampleSize; i++) { + const lx = Math.max(0, Math.min(mapSize - 1, pos.lx + i)); + bottomHeight += getHeight(lx, mapSize - 1); + } + bottomHeight /= (sampleSize * 2 + 1); + + // 左侧边缘平均高度 + let leftHeight = 0; + for (let i = -sampleSize; i <= sampleSize; i++) { + const lz = Math.max(0, Math.min(mapSize - 1, pos.lz + i)); + leftHeight += getHeight(0, lz); + } + leftHeight /= (sampleSize * 2 + 1); + + // 右侧边缘平均高度 + let rightHeight = 0; + for (let i = -sampleSize; i <= sampleSize; i++) { + const lz = Math.max(0, Math.min(mapSize - 1, pos.lz + i)); + rightHeight += getHeight(mapSize - 1, lz); + } + rightHeight /= (sampleSize * 2 + 1); + + // 构建边缘候选列表 + const edges = [ + { dir: Direction.NORTH, height: topHeight, dist: distToTop }, + { dir: Direction.SOUTH, height: bottomHeight, dist: distToBottom }, + { dir: Direction.WEST, height: leftHeight, dist: distToLeft }, + { dir: Direction.EAST, height: rightHeight, dist: distToRight }, + ]; + + // 排序:优先选择高度最低的,高度相同则选择距离最远的 + edges.sort((a, b) => { + if (Math.abs(a.height - b.height) > 0.5) { + return a.height - b.height; // 高度低的优先 + } + return b.dist - a.dist; // 高度相同,距离远的优先 + }); + + return edges[0].dir; + }; + + // 辅助函数:选择最优转向(优先向低处转+朝向目标) + // 【严格模式】绝对禁止上坡,水只能往下流或保持同高度 + const chooseBestTurn = ( + pos: { lx: number; lz: number }, + currentDir: Direction, + currentH: number, + targetEdge: Edge + ): Direction | null => { + const options: Array<{ dir: Direction; score: number }> = []; + + // 尝试左右两个方向 + const leftDir = rotate90(currentDir, false); + const rightDir = rotate90(currentDir, true); + + for (const testDir of [leftDir, rightDir]) { + const vec = getDirectionVector(testDir); + const nextLx = pos.lx + vec.dx; + const nextLz = pos.lz + vec.dz; + + // 越界检查 + if (nextLx < 0 || nextLx >= mapSize || nextLz < 0 || nextLz >= mapSize) { + continue; + } + + // 障碍物检查 + if (getGrid(nextLx, nextLz) === 1) { + continue; + } + + // 访问检查,防止选择已经走过的路径(避免循环) + if (visitedPositions.has(posKey(nextLx, nextLz))) { + continue; + } + + const nextHeight = getHeight(nextLx, nextLz); + const heightDiff = currentH - nextHeight; + + // 【严格模式】禁止任何上坡,水只能往下流 + if (heightDiff < 0) { + continue; // 上坡,跳过 + } + + let score = 0; + + // 1. 高度优势(下坡优先) + if (heightDiff > 0) { + score += heightDiff * 20; // 下坡有高分 + } else { + score += 5; // 同高度也有一定优势 + } + + // 2. 朝向目标边缘(距离优势) + let currentDist = 0; + let nextDist = 0; + switch (targetEdge) { + case Edge.TOP: + currentDist = pos.lz; + nextDist = nextLz; + break; + case Edge.BOTTOM: + currentDist = mapSize - 1 - pos.lz; + nextDist = mapSize - 1 - nextLz; + break; + case Edge.LEFT: + currentDist = pos.lx; + nextDist = nextLx; + break; + case Edge.RIGHT: + currentDist = mapSize - 1 - pos.lx; + nextDist = mapSize - 1 - nextLx; + break; + } + + if (nextDist < currentDist) { + score += 15; // 朝向目标加分 + } + + // 3. 随机微调 + score += rng() * 5; + + options.push({ dir: testDir, score }); + } + + if (options.length === 0) { + return null; // 无路可走 + } + + // 选择分数最高的方向 + options.sort((a, b) => b.score - a.score); + return options[0].dir; + }; + + // 跟踪湖泊覆盖的逻辑格(用于瀑布处理时跳过湖泊区域) + const lakeCells = new Set(); + + // 主循环 + while (steps < MAX_STEPS) { + steps++; + stepsSinceLastTributary++; + + // 不在这里检测边缘,因为起点本身就在边缘 + + // 【修正】功能3:盆地湖泊检测 - 只在真正的死胡同才生成 + // 检测条件:当前位置陷入盆地(四周都更高)且无法继续下降 + // 注意:在主循环中不主动检测,只在遇到障碍时才考虑湖泊 + // 这样可以避免过早生成湖泊,让河流尽可能流得更远 + + // 【修改】功能2:边缘导向(最低高度后向最低且最远的边缘流) + let preferredDirection = currentDirection; + const isAtMinHeight = currentHeight <= mapMinHeight; + + if (isAtMinHeight) { + // 在最低高度时,优先朝向最低且最远的边缘(延长河流) + preferredDirection = getDirectionToLowestFarthestEdge(currentPos); + + // 如果首选方向与当前方向不同,尝试调整 + if (preferredDirection !== currentDirection) { + const prefVec = getDirectionVector(preferredDirection); + const prefNextLx = currentPos.lx + prefVec.dx; + const prefNextLz = currentPos.lz + prefVec.dz; + + // 检查首选方向是否可行 + if (prefNextLx >= 0 && prefNextLx < mapSize && + prefNextLz >= 0 && prefNextLz < mapSize && + getGrid(prefNextLx, prefNextLz) === 0) { + currentDirection = preferredDirection; + } + } + } + + // 尝试沿当前方向前进 + const dirVec = getDirectionVector(currentDirection); + const nextLx = currentPos.lx + dirVec.dx; + const nextLz = currentPos.lz + dirVec.dz; + + // 边界检查 + if (nextLx < 0 || nextLx >= mapSize || nextLz < 0 || nextLz >= mapSize) { + break; + } + + // 【修改】访问检查:防止循环,但给出更详细的日志 + const nextKey = posKey(nextLx, nextLz); + if (visitedPositions.has(nextKey)) { + console.warn(`[河流生成] 检测到循环路径(位置: ${nextLx}, ${nextLz}),尝试生成湖泊`); + + // 检查是否在盆地中,是的话生成湖泊 + if (isInBasin(currentPos, currentHeight)) { + const lakeResult = fillBasinLake( + currentPos, + currentHeight, + streamMap, + microScale, + 12 + ); + if (lakeResult.size > 0) { + // 记录湖泊覆盖的逻辑格 + lakeResult.cells.forEach(cell => lakeCells.add(cell)); + console.log(`[河流生成] 循环点位于盆地,生成湖泊,河流终止`); + } + } + break; + } + + const nextHeight = getHeight(nextLx, nextLz); + const isBlocked = getGrid(nextLx, nextLz) === 1; // 有石头 + const isUphill = nextHeight > currentHeight; // 【严格模式】任何上坡都不允许(水只能往下流) + + // 碰撞处理:遇到障碍或上坡 + if (isBlocked || isUphill) { + // 考虑生成支流 + if (tributaryCount < MAX_TRIBUTARIES && stepsSinceLastTributary >= TRIBUTARY_COOLDOWN) { + // 【修改】支流宽度:主流的40%,最小3微体素(更窄) + const tributaryWidth = Math.max(3, Math.floor(currentWidth * 0.4)); + + // 【新增】随机选择支流方向(左或右) + const isLeftTributary = rng() > 0.5; + const tributaryDir = rotate90(currentDirection, !isLeftTributary); + + // 【新增】如果与上一个支流方向相同,降低生成概率(避免同向密集) + let shouldGenerate = true; + if (lastTributaryDirection !== null && lastTributaryDirection === tributaryDir) { + shouldGenerate = rng() > 0.7; // 只有30%概率生成同向支流 + } + + if (shouldGenerate) { + const tributary = generateTributary( + { lx: currentPos.lx, lz: currentPos.lz }, + currentHeight, + tributaryWidth, + tributaryDir, // 【修改】直接传入确定的方向 + 6 + Math.floor(rng() * 5), // 【修改】长度:6-10格(稍微短一点) + grid, + getHeight, + mapSize + ); + + // 【修改】支流至少要有3个节点才算有效(避免半截支流) + if (tributary.length >= 3) { + tributaries.push(tributary); + tributaryCount++; + stepsSinceLastTributary = 0; + lastTributaryDirection = tributaryDir; // 记录方向 + console.log(`[支流生成] 生成支流 ${tributaryCount},长度: ${tributary.length},宽度: ${tributaryWidth}`); + } + } + } + + // 【严格模式】转向策略:只允许下坡或同高 + const newDirection = chooseBestTurn(currentPos, currentDirection, currentHeight, targetEdge); + + if (newDirection === null) { + // 【最终检查】真的无路可走了,检查是否在盆地中 + if (isInBasin(currentPos, currentHeight)) { + console.log(`[河流生成] 河流陷入盆地,尝试生成终点湖泊`); + const lakeResult = fillBasinLake( + currentPos, + currentHeight, + streamMap, + microScale, + 12 // 最大3x4方块 + ); + + if (lakeResult.size > 0) { + // 记录湖泊覆盖的逻辑格 + lakeResult.cells.forEach(cell => lakeCells.add(cell)); + console.log(`[河流生成] 在盆地中生成终点湖泊(面积: ${lakeResult.size} 格),河流终止`); + } else { + console.warn(`[河流生成] 盆地过小或过浅,无法生成湖泊,河流提前结束,路径长度: ${mainPath.length} 格`); + } + } else { + // 【错误情况】不在盆地但无路可走 - 这不应该发生 + console.error(`[河流生成] 严重错误:河流无路可走且不在盆地中,提前结束,路径长度: ${mainPath.length} 格`); + console.error(`[河流生成] 当前位置: (${currentPos.lx}, ${currentPos.lz}),高度: ${currentHeight}`); + } + break; + } + + currentDirection = newDirection; + continue; // 转向后重新尝试 + } + + // 可以前进 + currentPos = { lx: nextLx, lz: nextLz }; + visitedPositions.add(nextKey); // 标记为已访问 + + // 【严格模式】高度处理:只支持下降或平路(不允许上坡) + if (nextHeight < currentHeight) { + // 下降 + const drop = currentHeight - nextHeight; + accumulatedDrop += drop; + + currentWidth = 6; // 宽度固定为6微体素 + currentHeight = nextHeight; + straightSteps = 0; // 高度变化,重置直行计数 + } else { + // 同一高度平面上直行(nextHeight === currentHeight) + straightSteps++; + } + + // 【新增】功能1:自适应转向(3-5格后转向低处) + // 只在非最低高度时触发,避免干扰边缘导向逻辑 + if (!isAtMinHeight && straightSteps >= 3 && rng() < 0.4) { + // 40% 概率触发转向检测 + const leftDir = rotate90(currentDirection, false); + const rightDir = rotate90(currentDirection, true); + + const leftVec = getDirectionVector(leftDir); + const rightVec = getDirectionVector(rightDir); + + const leftLx = currentPos.lx + leftVec.dx; + const leftLz = currentPos.lz + leftVec.dz; + const rightLx = currentPos.lx + rightVec.dx; + const rightLz = currentPos.lz + rightVec.dz; + + let leftHeight = Infinity; + let rightHeight = Infinity; + let canTurnLeft = false; + let canTurnRight = false; + + // 检查左转 + if (leftLx >= 0 && leftLx < mapSize && leftLz >= 0 && leftLz < mapSize && + getGrid(leftLx, leftLz) === 0) { + leftHeight = getHeight(leftLx, leftLz); + canTurnLeft = true; + } + + // 检查右转 + if (rightLx >= 0 && rightLx < mapSize && rightLz >= 0 && rightLz < mapSize && + getGrid(rightLx, rightLz) === 0) { + rightHeight = getHeight(rightLx, rightLz); + canTurnRight = true; + } + + // 选择高度更低的方向 + if (canTurnLeft && leftHeight < currentHeight) { + if (!canTurnRight || leftHeight <= rightHeight) { + currentDirection = leftDir; + straightSteps = 0; + } + } + if (canTurnRight && rightHeight < currentHeight) { + if (!canTurnLeft || rightHeight < leftHeight) { + currentDirection = rightDir; + straightSteps = 0; + } + } + } + + // 记录节点 + mainPath.push({ + lx: currentPos.lx, + lz: currentPos.lz, + height: currentHeight, + width: currentWidth, + direction: currentDirection, + accumulatedDrop: accumulatedDrop, + }); + + // 【修改】在记录节点后,检查当前位置是否到达非起始边缘 + // 只有前进至少一步后才检测边缘(起点本身就在边缘) + if (mainPath.length > 1) { + const reachedEdge = ( + currentPos.lx === 0 || + currentPos.lx === mapSize - 1 || + currentPos.lz === 0 || + currentPos.lz === mapSize - 1 + ); + + if (reachedEdge) { + console.log(`[河流生成] 到达地图边缘 (${currentPos.lx}, ${currentPos.lz}),河流长度: ${mainPath.length} 格`); + break; + } + } + + lastHeight = currentHeight; + } + + if (mainPath.length < mapSize * 0.3) { + console.warn(`[MountainStream V2] 路径过短 (${mainPath.length} < ${mapSize * 0.3}),可能生成失败`); + // 不返回空,尝试渲染短路径 + } + + // 6. 生成支流函数(需要在主循环之前定义) + /** + * 生成支流(正交90度方向,短路径) + */ + function generateTributary( + startPos: { lx: number; lz: number }, + startHeight: number, + tributaryWidth: number, + tributaryDirection: Direction, // 【修改】直接传入方向,不再随机 + maxLength: number, + grid: Uint8Array, + getHeight: (lx: number, lz: number) => number, + mapSize: number + ): RiverNode[] { + const tributary: RiverNode[] = []; + + let currentPos = { lx: startPos.lx, lz: startPos.lz }; + let currentHeight = startHeight; + let currentWidth = tributaryWidth; // 【修改】直接使用传入的宽度,不再取最小值 + let accumulatedDrop = 0; + + // 【修复】先添加分叉点(起点),确保与主流连接 + tributary.push({ + lx: startPos.lx, + lz: startPos.lz, + height: startHeight, + width: currentWidth, + direction: tributaryDirection, + accumulatedDrop: 0, + }); + + for (let step = 0; step < maxLength; step++) { + const dirVec = getDirectionVector(tributaryDirection); + const nextLx = currentPos.lx + dirVec.dx; + const nextLz = currentPos.lz + dirVec.dz; + + // 边界检查 + if (nextLx < 0 || nextLx >= mapSize || nextLz < 0 || nextLz >= mapSize) { + break; + } + + // 障碍物检查 + const nextGrid = grid[nextLz * mapSize + nextLx]; + if (nextGrid === 1) { + break; // 遇到石头停止 + } + + const nextHeight = getHeight(nextLx, nextLz); + + // 上坡停止 + if (nextHeight > currentHeight) { + break; + } + + // 更新位置 + currentPos = { lx: nextLx, lz: nextLz }; + + // 高度下降处理(支流宽度固定) + if (nextHeight < currentHeight) { + const drop = currentHeight - nextHeight; + accumulatedDrop += drop; + currentHeight = nextHeight; + } + + // 【修改】支流宽度保持固定 + tributary.push({ + lx: currentPos.lx, + lz: currentPos.lz, + height: currentHeight, + width: currentWidth, // 使用固定宽度 + direction: tributaryDirection, + accumulatedDrop: accumulatedDrop, + }); + } + + // 【新增】如果支流太短(< 3个节点),返回空数组 + if (tributary.length < 3) { + console.log(`[支流生成] 支流过短 (${tributary.length} < 3),丢弃`); + return []; + } + + return tributary; + } + + // 7. 河床雕刻(微体素层级) + // 注意:streamMap 已在函数开头定义,避免初始化顺序错误 + + /** + * 填充瀑布侧面的水体素 - 修复版:只在上游方块的朝向下游的侧面填充 + * 关键:只填充上游方块,且只填充靠近下游方向的边缘区域 + */ + function fillCascadeWater( + fromPos: { lx: number; lz: number }, + toPos: { lx: number; lz: number }, + fromHeight: number, + toHeight: number, + riverWidth: number, + streamMap: Map, + microScale: number, + gridWidth: number, + gridHeight: number, + noise2D: (x: number, y: number) => number + ) { + const dropHeight = fromHeight - toHeight; // 逻辑格高度差 + if (dropHeight < 2) return; // 高度差不够 + + const verticalWaterHeight = dropHeight * microScale * 0.9; // 垂直水柱高度(微体素单位) + + // 计算流向方向 + const flowDx = toPos.lx - fromPos.lx; // -1, 0, 或 1 + const flowDz = toPos.lz - fromPos.lz; // -1, 0, 或 1 + + // 侧面厚度(微体素)- 瀑布水帘的厚度 + const edgeThickness = 3; + + // 只填充上游方块(fromPos) + for (let mx = 0; mx < microScale; mx++) { + for (let mz = 0; mz < microScale; mz++) { + const ix = fromPos.lx * microScale + mx; + const iz = fromPos.lz * microScale + mz; + + if (ix < 0 || ix >= gridWidth || iz < 0 || iz >= gridHeight) continue; + + // 判断当前微体素是否在"朝向下游的侧面"区域 + // 关键:必须同时满足X方向和Z方向的约束(AND逻辑,不是OR) + let satisfiesDx = false; + let satisfiesDz = false; + + // X方向约束 + if (flowDx === -1) { + // 向西流:只填充西侧边缘(mx 较小) + satisfiesDx = mx < edgeThickness; + } else if (flowDx === 1) { + // 向东流:只填充东侧边缘(mx 较大) + satisfiesDx = mx >= microScale - edgeThickness; + } else { + // 不沿X方向流动(flowDx === 0):X方向不限制 + satisfiesDx = true; + } + + // Z方向约束 + if (flowDz === -1) { + // 向北流:只填充北侧边缘(mz 较小) + satisfiesDz = mz < edgeThickness; + } else if (flowDz === 1) { + // 向南流:只填充南侧边缘(mz 较大) + satisfiesDz = mz >= microScale - edgeThickness; + } else { + // 不沿Z方向流动(flowDz === 0):Z方向不限制 + satisfiesDz = true; + } + + // 必须同时满足两个方向的约束 + const isOnDownstreamEdge = satisfiesDx && satisfiesDz; + + if (!isOnDownstreamEdge) continue; + + // 只填充已存在河床的位置(由 carveRiverBed 创建) + // 这样可以确保瀑布水流只出现在河道内,不会溢出 + const key = `${ix}|${iz}`; + const existing = streamMap.get(key); + + if (existing) { + // 设置高水柱(瀑布效果) + streamMap.set(key, { + ...existing, + waterHeight: Math.max(existing.waterHeight, verticalWaterHeight), + isCascade: true, + cascadeHeight: verticalWaterHeight, + }); + } + } + } + } + + /** + * 雕刻河床的核心函数 - 改进版:确保路径连续性 + */ + function carveRiverBed(path: RiverNode[], isTributary: boolean = false) { + // 先将逻辑格路径转换为微体素中心点路径 + const microPath: Array<{ + ix: number; + iz: number; + width: number; + height: number; + direction: Direction; + isOriginalNode?: boolean; // 【新增】标记原始节点 + isPathEndpoint?: boolean; // 【新增】标记路径端点 + }> = []; + for (let i = 0; i < path.length; i++) { const node = path[i]; - const cx = node.x; - const cz = node.z; + // 逻辑格中心对应的微体素坐标 + const centerIx = node.lx * microScale + Math.floor(microScale / 2); + const centerIz = node.lz * microScale + Math.floor(microScale / 2); - // 河流宽度和深度(支流更窄更浅) - 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 微体素 + const isEndpoint = (i === 0 || i === path.length - 1); - // 检查是否为瀑布段(高度落差 >= 2 微体素) + microPath.push({ + ix: centerIx, + iz: centerIz, + width: node.width, + height: node.height, + direction: node.direction, + isOriginalNode: true, // 标记为原始节点 + isPathEndpoint: isEndpoint, // 标记路径端点 + }); + + // 【新增】起点和终点向边缘外延伸,确保边缘体素被完全蚀刻 + if (isEndpoint) { + const isOnLeftEdge = node.lx === 0; + const isOnRightEdge = node.lx === mapSize - 1; + const isOnTopEdge = node.lz === 0; + const isOnBottomEdge = node.lz === mapSize - 1; + + // 如果在边缘,向边缘外延伸 + if (isOnLeftEdge || isOnRightEdge || isOnTopEdge || isOnBottomEdge) { + let extendIx = centerIx; + let extendIz = centerIz; + + // 延伸距离:河床宽度的一半 + 1,确保覆盖到边缘 + const extendDist = Math.ceil(node.width / 2) + 1; // 约4微体素 + + // 根据边缘位置计算延伸方向 + if (isOnLeftEdge) { + extendIx = centerIx - extendDist; // 向左延伸 + } else if (isOnRightEdge) { + extendIx = centerIx + extendDist; // 向右延伸 + } + + if (isOnTopEdge) { + extendIz = centerIz - extendDist; // 向上延伸 + } else if (isOnBottomEdge) { + extendIz = centerIz + extendDist; // 向下延伸 + } + + // 添加延伸点 + microPath.push({ + ix: extendIx, + iz: extendIz, + width: node.width, + height: node.height, + direction: node.direction, + isOriginalNode: false, + isPathEndpoint: true, // 延伸点也标记为端点 + }); + } + } + + // 在相邻节点之间插值,确保连续性 + if (i < path.length - 1) { + const nextNode = path[i + 1]; + const nextCenterIx = nextNode.lx * microScale + Math.floor(microScale / 2); + const nextCenterIz = nextNode.lz * microScale + Math.floor(microScale / 2); + + // 【新增】检测是否有转角(方向改变) + const hasDirectionChange = node.direction !== nextNode.direction; + + if (hasDirectionChange) { + // 转角圆滑处理:在对角线方向添加额外插值点 + const currentDirVec = getDirectionVector(node.direction); + const nextDirVec = getDirectionVector(nextNode.direction); + + // 计算对角线方向(两个方向的向量和) + const cornerDx = currentDirVec.dx + nextDirVec.dx; + const cornerDz = currentDirVec.dz + nextDirVec.dz; + + // 在转角处添加1-3个圆角插值点 + const cornerSteps = 2; + for (let cs = 1; cs <= cornerSteps; cs++) { + const cornerT = cs / (cornerSteps + 1); + + const cornerIx = Math.round(centerIx + cornerDx * microScale * 0.4 * cornerT); + const cornerIz = Math.round(centerIz + cornerDz * microScale * 0.4 * cornerT); + + // 【严格模式】插值高度:取当前和下一节点的较小值,确保不上坡 + // 水只能往下流,所以插值高度不能超过起点高度 + const rawInterpHeight = node.height + (nextNode.height - node.height) * cornerT; + const safeHeight = Math.min(node.height, rawInterpHeight); + + microPath.push({ + ix: cornerIx, + iz: cornerIz, + width: node.width, + height: safeHeight, + direction: node.direction, + isOriginalNode: false, + isPathEndpoint: false, + }); + } + } + + // 计算两点之间的距离 + const dx = nextCenterIx - centerIx; + const dz = nextCenterIz - centerIz; + const dist = Math.sqrt(dx * dx + dz * dz); + const steps = Math.ceil(dist); + + // 在两个节点之间插值 + for (let step = 1; step < steps; step++) { + const t = step / steps; + const interpIx = Math.round(centerIx + dx * t); + const interpIz = Math.round(centerIz + dz * t); + const interpWidth = node.width + (nextNode.width - node.width) * t; + + // 【严格模式】插值高度:确保不上坡 + const rawInterpHeight = node.height + (nextNode.height - node.height) * t; + const safeHeight = Math.min(node.height, rawInterpHeight); + + microPath.push({ + ix: interpIx, + iz: interpIz, + width: interpWidth, + height: safeHeight, + direction: node.direction, + }); + } + } + } + + // 【严格模式】最终验证:确保整个 microPath 高度单调递减 + // 遍历路径,如果发现高度上升,则强制修正为前一个点的高度 + let maxHeightSoFar = Infinity; + for (let i = 0; i < microPath.length; i++) { + const point = microPath[i]; + if (point.height > maxHeightSoFar) { + // 发现高度上升(这不应该发生,但作为安全措施) + console.warn(`[河床验证] 发现高度上升: 点${i} 高度 ${point.height} > 最大高度 ${maxHeightSoFar},强制修正`); + point.height = maxHeightSoFar; + } + maxHeightSoFar = Math.min(maxHeightSoFar, point.height); + } + + // 沿着微体素路径雕刻河床 + for (let i = 0; i < microPath.length; i++) { + const point = microPath[i]; + const riverWidth = point.width; // 微体素单位 + const currentHeight = point.height; + + // 河床宽度:与河流宽度一致(6微体素) + const riverBedWidth = riverWidth; + + // 深度:根据宽度动态调整 + const baseDepth = 1.5 + (riverWidth / 8) * 1.5; + + // 检查是否为瀑布段 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 (i < microPath.length - 1) { + const nextHeight = microPath[i + 1].height; + const heightDiff = currentHeight - nextHeight; if (heightDiff >= 2) { isCascade = true; - cascadeHeight = heightDiff; + cascadeHeight = heightDiff * microScale; } } - // 在河床宽度范围内雕刻 - const scanRad = Math.ceil(riverBedWidth / 2) + 1; + // 【修复】只有路径端点使用方形雕刻,其他都用圆形 + const isEndpoint = point.isPathEndpoint === true; - for (let dx = -scanRad; dx <= scanRad; dx++) { + // 在河流中心点周围雕刻区域 + // 扫描半径:基于河床宽度 + const scanRad = Math.ceil(riverBedWidth / 2); // 3 + + for (let dx = -scanRad; dx <= scanRad; dx++) { // [-3, 3] for (let dz = -scanRad; dz <= scanRad; dz++) { - const tx = cx + dx; - const tz = cz + dz; + const ix = point.ix + dx; + const iz = point.iz + dz; - if (tx < 0 || tx >= width || tz < 0 || tz >= height) continue; - if (getGrid(tx, tz) === 1) continue; // 避开石头 + if (ix < 0 || ix >= width || iz < 0 || iz >= height) continue; - const dist = Math.sqrt(dx * dx + dz * dz); + const distToCenter = 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; + // 使用固定宽度判断河床范围 + const cleanWaterRadius = riverWidth / 2; // 3 + const cleanBedRadius = riverBedWidth / 2; // 3 - if (dist < noisyBedWidth) { - const isWater = dist < noisyWaterWidth; - const depthFactor = 1 - dist / noisyBedWidth; // 中心深,边缘浅 - const depth = baseDepth * Math.max(0.3, depthFactor); + // 【修复】只有端点使用方形,其他都用圆形(防止过度雕刻) + const manhattanDist = Math.max(Math.abs(dx), Math.abs(dz)); + const inBedRange = isEndpoint + ? manhattanDist < cleanBedRadius // 端点:方形覆盖 < 3,覆盖5x5区域 + : distToCenter < cleanBedRadius; // 其他:圆形覆盖 + + if (inBedRange) { + const isWater = distToCenter < cleanWaterRadius; - const key = `${tx}|${tz}`; + // 【简化】两级深度系统:河岸浅层 + 河道中心深层 + // 河道中心深度:根据宽度,3-4微体素 + const centerDepth = Math.min(4, Math.max(3, Math.floor(baseDepth))); + // 河岸深度:2微体素 + const bankDepth = 2; + + // 判断深度:在水道内用中心深度,否则用河岸深度 + const depth = distToCenter < cleanWaterRadius ? centerDepth : bankDepth; + + const key = `${ix}|${iz}`; 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; - } - } + const dirVec = getDirectionVector(point.direction); + const flowDir = { dx: dirVec.dx, dz: dirVec.dz }; streamMap.set(key, { depth: depth, - waterHeight: isWater ? depth * 0.8 : 0, // 水深为河床深度的80% - isCenter: dist < 1, + waterHeight: isWater ? depth * 0.8 : 0, + isCenter: distToCenter < 1, isCascade, cascadeHeight, - isSpring: i === 0 && !isTributary, // 主河流起点标记为山泉 + isSpring: false, isTributary, flowDirection: flowDir, }); @@ -856,7 +1876,40 @@ export const generateMountainStream = ( } } } - }; + + // 瀑布处理(基于原始逻辑格路径) + // 【修复】跳过湖泊区域,避免在湖泊边缘生成不必要的瀑布 + for (let i = 0; i < path.length - 1; i++) { + const node = path[i]; + const nextNode = path[i + 1]; + + // 检查当前节点或下一个节点是否在湖泊区域 + const nodeKey = `${node.lx}|${node.lz}`; + const nextNodeKey = `${nextNode.lx}|${nextNode.lz}`; + + if (lakeCells.has(nodeKey) || lakeCells.has(nextNodeKey)) { + // 跳过湖泊区域的瀑布处理 + continue; + } + + const heightDiff = node.height - nextNode.height; + + if (heightDiff >= 2) { + fillCascadeWater( + { lx: node.lx, lz: node.lz }, // 上游方块 (fromPos) + { lx: nextNode.lx, lz: nextNode.lz }, // 下游方块 (toPos) + node.height, + nextNode.height, + node.width, + streamMap, + microScale, + width, + height, + noise2D + ); + } + } + } // 雕刻主河流 carveRiverBed(mainPath, false); @@ -866,7 +1919,5 @@ export const generateMountainStream = ( carveRiverBed(tributary, true); } - console.log(`[MountainStream] 溪流系统生成完成,共 ${streamMap.size} 个微体素`); - return streamMap; };