From bd0b00c37e17c2a130785d4eb68f2a6d4c53ad77 Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:19:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A4=8D=E7=89=A9=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E3=80=81=E5=9C=B0=E5=BD=A2=E5=92=8C=E6=B0=B4=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/Map/logic/newVegetation.ts | 1884 ++++++++++++++++- .../src/features/Map/logic/terrain.ts | 6 +- .../src/features/Map/logic/waterSystem.ts | 254 ++- 3 files changed, 2081 insertions(+), 63 deletions(-) diff --git a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts index 3dca8ba..bd22861 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts @@ -17,6 +17,10 @@ export interface VegetationGenerationContext { isDesertScene: boolean; sceneType?: string; heightMap: Map; + // 山地场景专用数据 + mountainStreamMap?: Map; + mountainRockContext?: { stoneHeight: number[][]; stoneDepth: number[][] }; + terrainHeightMap?: number[][]; // 逻辑层级的高度图 (mapSize x mapSize) } // ========================================= @@ -72,7 +76,87 @@ const C: Record = { cactusGreen: '#62944B', cactusLight: '#86B56A', cactusFlower: '#FF7777', flowerBird: '#FF8800', flowerPurple: '#9933CC', cycadTrunk: '#54463A', cycadLeaf: '#3C9663', - coconut: '#664A3C' + coconut: '#664A3C', + + // ========================================= + // --- 山地植物专用颜色 --- + // ========================================= + + // 龙胆草 (Gentian) - 蓝紫色系 + gentianBlue: '#4169E1', // 龙胆蓝 + gentianPurple: '#6B5B95', // 龙胆紫 + gentianDeep: '#483D8B', // 深龙胆紫 + gentianLight: '#87CEEB', // 浅蓝高光 + + // 高山杜鹃 (Alpine Azalea) - 粉红紫色系 + azaleaPink: '#FF69B4', // 杜鹃粉 + azaleaDeep: '#C71585', // 深粉红 + azaleaLight: '#FFB6C1', // 浅粉 + azaleaMagenta: '#DA70D6', // 洋红 + azaleaLeaf: '#2E8B57', // 杜鹃叶绿 + azaleaLeafDark: '#1E5631', // 深绿叶 + + // 越橘/蓝莓 (Blueberry) - 浆果色 + blueberryFruit: '#4A5568', // 蓝莓深蓝 + blueberryRipe: '#5B6B8A', // 成熟蓝莓 + blueberryLeaf: '#6B8E23', // 橄榄绿叶 + blueberryLeafDark: '#556B2F',// 深橄榄 + blueberryStem: '#8B4513', // 棕色茎 + + // 溪荪/菖蒲 (Iris) - 紫黄色系 + irisPurple: '#9370DB', // 鸢尾紫 + irisYellow: '#FFD700', // 鸢尾黄心 + irisDeep: '#663399', // 深紫 + irisLeaf: '#228B22', // 森林绿 + irisLeafDark: '#006400', // 深绿 + + // 羊齿蕨 (Fern) - 绿色系 + fernGreen: '#4A7A52', // 蕨绿 + fernLight: '#6B9B6A', // 浅蕨绿 + fernDark: '#2F5233', // 深蕨绿 + fernStem: '#5D4E37', // 蕨茎褐 + + // 水芹 (Water Parsley) - 绿白色 + parsleyGreen: '#7CB342', // 水芹绿 + parsleyLight: '#9CCC65', // 浅绿 + parsleyFlower: '#FAFAFA', // 白色小花 + parsley_stem: '#795548', // 茎褐色 + + // ========================================= + // --- 鲜艳蘑菇专用颜色 --- + // ========================================= + + // 毒蝇伞 (Fly Agaric) - 红白色 + flyAgaricRed: '#DC143C', // 猩红 + flyAgaricDeep: '#8B0000', // 深红 + flyAgaricSpot: '#FFFAFA', // 雪白斑点 + flyAgaricGill: '#FFF8DC', // 米白菌褶 + flyAgaricStem: '#FAF0E6', // 亚麻白茎 + + // 紫晶蜡蘑 (Amethyst Deceiver) - 紫色系 + amethystPurple: '#9966CC', // 紫水晶 + amethystDeep: '#7B68EE', // 深紫 + amethystLight: '#DDA0DD', // 浅紫 + amethystStem: '#BA55D3', // 紫茎 + + // 鸡油菌 (Chanterelle) - 橙黄色 + chanterelleOrange: '#FFA500',// 橙色 + chanterelleYellow: '#FFD700',// 金黄 + chanterelleDeep: '#FF8C00', // 深橙 + chanterelleRidge: '#F4A460', // 菌褶沙褐 + + // 金针菇 (Enoki) - 金白色 + enokiCap: '#F5DEB3', // 小麦色菇盖 + enokiStem: '#FFFACD', // 柠檬绸 + enokiLight: '#FFEFD5', // 淡黄 + enokiBase: '#D2B48C', // 棕色根部 + + // 荧光小菇 (Bioluminescent) - 蓝绿发光色 + bioGlow: '#00FFFF', // 青色发光 + bioGlowGreen: '#7FFFD4', // 碧绿发光 + bioGlowBlue: '#00CED1', // 暗青发光 + bioStem: '#20B2AA', // 浅海绿茎 + bioGlowCore: '#E0FFFF' // 淡青核心 }; const darkenHex = (hex: string, factor: number): string => { @@ -1102,51 +1186,100 @@ const createGrassCluster = (builder: VoxelBuilder, ox: number, oz: number) => { // ============= 超小型稀疏草(2-3 体素高度) ============= -// 微型草丝 - 单根极细的草,只有 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 根(稀疏分布在较大范围) + const targetCount = 8 + Math.floor(Math.random() * 6); // 8-13 根(减少数量以保证间距) + const usedPositions = new Set(); + const minSpacing = 1; // 最小间距 1 体素 - for (let i = 0; i < count; i++) { + let placed = 0; + let attempts = 0; + const maxAttempts = targetCount * 5; + + while (placed < targetCount && attempts < maxAttempts) { + attempts++; const angle = Math.random() * Math.PI * 2; - const dist = Math.random() * 5; // 更大的分布范围 + const dist = Math.random() * 6; // 更大的分布范围 const gx = Math.round(ox + Math.cos(angle) * dist); const gz = Math.round(oz + Math.sin(angle) * dist); + + // 检查与已有草的间距 + let tooClose = false; + for (const pos of usedPositions) { + const [px, pz] = pos.split(',').map(Number); + if (Math.abs(gx - px) <= minSpacing && Math.abs(gz - pz) <= minSpacing) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + usedPositions.add(`${gx},${gz}`); 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); } + placed++; } }; -// 微型草皮 - 贴地的超矮草覆盖 +// 微型草皮 - 贴地的超矮草覆盖(草根间保持间距) const createTinyGrassPatch = (builder: VoxelBuilder, ox: number, oz: number) => { const grassColors = ['#4A8A3A', '#5A9A4A', '#4A9A3A', '#5AAA4A']; const patchR = 3 + Math.random() * 2; // 半径 3-5 + const minSpacing = 2; // 草皮间距 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.4) { // 稀疏分布(60%覆盖) + // 使用网格模式生成,保证间距 + for (let x = -Math.ceil(patchR); x <= Math.ceil(patchR); x += minSpacing) { + for (let z = -Math.ceil(patchR); z <= Math.ceil(patchR); z += minSpacing) { + // 添加随机偏移避免太规则 + const offsetX = Math.round((Math.random() - 0.5) * 0.8); + const offsetZ = Math.round((Math.random() - 0.5) * 0.8); + const finalX = x + offsetX; + const finalZ = z + offsetZ; + + if (finalX * finalX + finalZ * finalZ <= patchR * patchR && Math.random() > 0.3) { 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); + builder.add(ox + finalX, y, oz + finalZ, col); } } } } }; -// 散落草芽 - 极其稀疏的小草芽 +// 散落草芽 - 极其稀疏的小草芽(草根间保持间距) const createScatteredSprouts = (builder: VoxelBuilder, ox: number, oz: number) => { const sproutColors = ['#5AAA4A', '#6ABA5A', '#5ABA4A']; - const count = 6 + Math.floor(Math.random() * 6); // 6-11 个草芽 + const targetCount = 5 + Math.floor(Math.random() * 4); // 5-8 个草芽 + const usedPositions = new Set(); + const minSpacing = 1; // 最小间距 1 体素 - 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); + let placed = 0; + let attempts = 0; + const maxAttempts = targetCount * 5; + + while (placed < targetCount && attempts < maxAttempts) { + attempts++; + const gx = Math.round(ox + (Math.random() - 0.5) * 10); + const gz = Math.round(oz + (Math.random() - 0.5) * 10); + + // 检查与已有草的间距 + let tooClose = false; + for (const pos of usedPositions) { + const [px, pz] = pos.split(',').map(Number); + if (Math.abs(gx - px) <= minSpacing && Math.abs(gz - pz) <= minSpacing) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + usedPositions.add(`${gx},${gz}`); const col = sproutColors[Math.floor(Math.random() * sproutColors.length)]; // 只有 2-3 高 @@ -1154,25 +1287,47 @@ const createScatteredSprouts = (builder: VoxelBuilder, ox: number, oz: number) = for (let y = 0; y < h; y++) { builder.add(gx, y, gz, col); } + placed++; } }; -// 河岸微草 - 专门为河岸设计的超小型草 +// 河岸微草 - 专门为河岸设计的超小型草(草根间保持间距) const createRiverbankTinyGrass = (builder: VoxelBuilder, ox: number, oz: number) => { const grassColors = ['#4A9A4A', '#5AAA5A', '#4AAA4A', '#5ABA5A']; // 更鲜绿的颜色 - const count = 15 + Math.floor(Math.random() * 12); // 15-26 根(高密度) + const targetCount = 10 + Math.floor(Math.random() * 6); // 10-15 根(减少数量以保证间距) + const usedPositions = new Set(); + const minSpacing = 1; // 最小间距 1 体素 - for (let i = 0; i < count; i++) { + let placed = 0; + let attempts = 0; + const maxAttempts = targetCount * 5; + + while (placed < targetCount && attempts < maxAttempts) { + attempts++; const angle = Math.random() * Math.PI * 2; - const dist = Math.random() * 4; + const dist = Math.random() * 5; // 稍大的分布范围 const gx = Math.round(ox + Math.cos(angle) * dist); const gz = Math.round(oz + Math.sin(angle) * dist); + + // 检查与已有草的间距 + let tooClose = false; + for (const pos of usedPositions) { + const [px, pz] = pos.split(',').map(Number); + if (Math.abs(gx - px) <= minSpacing && Math.abs(gz - pz) <= minSpacing) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + usedPositions.add(`${gx},${gz}`); 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); } + placed++; } }; @@ -1264,20 +1419,27 @@ const createCattail = (builder: VoxelBuilder, ox: number, oz: number) => { } }; -// 莎草 (Sedge) - 低矮密集的湿地草 +// 莎草 (Sedge) - 低矮密集的湿地草(草根间保持间距) const createSedge = (builder: VoxelBuilder, ox: number, oz: number) => { const sedgeGreen = '#4A6A3A'; const sedgeLight = '#6A8A5A'; const clumpR = 3 + Math.random() * 2; + const minSpacing = 2; // 莎草间距 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) { + // 使用网格模式生成,保证间距 + for (let x = -Math.ceil(clumpR); x <= Math.ceil(clumpR); x += minSpacing) { + for (let z = -Math.ceil(clumpR); z <= Math.ceil(clumpR); z += minSpacing) { + // 添加随机偏移避免太规则 + const offsetX = Math.round((Math.random() - 0.5) * 0.8); + const offsetZ = Math.round((Math.random() - 0.5) * 0.8); + const finalX = x + offsetX; + const finalZ = z + offsetZ; + + if (finalX * finalX + finalZ * finalZ <= clumpR * clumpR && Math.random() > 0.25) { 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); + builder.add(ox + finalX, y, oz + finalZ, col); } } } @@ -2487,6 +2649,801 @@ const DESERT_RARE_DECORATIONS = [ createDesertCrystal, // 沙漠水晶 createAnimalBones, // 动物骨骼 ]; + +// ========================================= +// --- 山地植物生成器 (Mountain Vegetation) --- +// ========================================= + +// ===================== +// 1. 低矮植物 (Low Plants) +// ===================== + +/** + * 龙胆草 (Gentian) + * 蓝紫色的高山花朵,常见于岩石缝隙 + * 高度: 4-8 体素,宽度: 3-5 体素 + */ +const createGentian = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + flower: [C.gentianBlue, C.gentianPurple, C.gentianDeep], + highlight: C.gentianLight, + stem: C.fernStem, + leaf: C.fernGreen + }; + + // 生成 1-3 朵花 + const flowerCount = 1 + Math.floor(Math.random() * 3); + + for (let f = 0; f < flowerCount; f++) { + // 每朵花的偏移 + const fx = ox + (f === 0 ? 0 : (Math.random() - 0.5) * 4); + const fz = oz + (f === 0 ? 0 : (Math.random() - 0.5) * 4); + + // 茎 (2-4 高) + const stemH = 2 + Math.floor(Math.random() * 3); + for (let y = 0; y < stemH; y++) { + builder.add(Math.round(fx), y, Math.round(fz), colors.stem); + } + + // 基部叶片 (2-3 片) + const leafCount = 2 + Math.floor(Math.random() * 2); + for (let i = 0; i < leafCount; i++) { + const angle = (i / leafCount) * Math.PI * 2; + const lx = Math.round(fx + Math.cos(angle) * 1.5); + const lz = Math.round(fz + Math.sin(angle) * 1.5); + builder.add(lx, 0, lz, colors.leaf); + builder.add(lx, 1, lz, colors.leaf); + } + + // 花朵 - 钟形结构 + const flowerY = stemH; + const flowerColor = colors.flower[Math.floor(Math.random() * colors.flower.length)]; + + // 花瓣 (钟形,5 瓣) + for (let i = 0; i < 5; i++) { + const angle = (i / 5) * Math.PI * 2; + const px = Math.round(fx + Math.cos(angle) * 1); + const pz = Math.round(fz + Math.sin(angle) * 1); + builder.add(px, flowerY, pz, flowerColor); + builder.add(px, flowerY + 1, pz, flowerColor); + // 顶部收拢 + if (Math.random() > 0.5) { + builder.add(Math.round(fx + Math.cos(angle) * 0.5), flowerY + 2, Math.round(fz + Math.sin(angle) * 0.5), flowerColor); + } + } + + // 花蕊/高光 + builder.add(Math.round(fx), flowerY + 1, Math.round(fz), colors.highlight); + } +}; + +/** + * 高山杜鹃 (Alpine Azalea) + * 粉红色的低矮灌木丛,花朵繁茂 + * 高度: 8-14 体素,宽度: 10-16 体素 + */ +const createAlpineAzalea = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + flower: [C.azaleaPink, C.azaleaDeep, C.azaleaLight, C.azaleaMagenta], + leaf: [C.azaleaLeaf, C.azaleaLeafDark], + stem: C.woodOak + }; + + // 灌木主体高度 + const height = 8 + Math.floor(Math.random() * 7); + const sizeFactor = height / 10; + + // 主干 (矮壮,多分枝) + const trunkH = 2 + Math.floor(Math.random() * 2); + for (let y = 0; y < trunkH; y++) { + builder.add(ox, y, oz, colors.stem); + // 基部较粗 + if (y < 2) { + builder.add(ox + 1, y, oz, colors.stem); + builder.add(ox - 1, y, oz, colors.stem); + builder.add(ox, y, oz + 1, colors.stem); + builder.add(ox, y, oz - 1, colors.stem); + } + } + + // 分枝 (3-5 个主枝) + const branchCount = 3 + Math.floor(Math.random() * 3); + for (let b = 0; b < branchCount; b++) { + const angle = (b / branchCount) * Math.PI * 2 + Math.random() * 0.5; + const dist = 2 + Math.random() * 3 * sizeFactor; + const branchH = trunkH + 1 + Math.random() * 2; + + // 枝干 + const bx = ox + Math.cos(angle) * dist * 0.5; + const bz = oz + Math.sin(angle) * dist * 0.5; + for (let y = trunkH; y < branchH + 2; y++) { + builder.add(Math.round(bx), y, Math.round(bz), colors.stem); + } + + // 每个分枝顶端的叶球和花朵 + const clusterX = ox + Math.cos(angle) * dist; + const clusterZ = oz + Math.sin(angle) * dist; + const clusterY = branchH + 2; + + // 叶球 (半球形) + const leafR = 2 + Math.random() * 2 * sizeFactor; + for (let x = -Math.ceil(leafR); x <= Math.ceil(leafR); x++) { + for (let y = 0; y <= Math.ceil(leafR); y++) { + for (let z = -Math.ceil(leafR); z <= Math.ceil(leafR); z++) { + const d2 = x * x + y * y + z * z; + if (d2 <= leafR * leafR && Math.random() > 0.3) { + const leafColor = colors.leaf[Math.floor(Math.random() * colors.leaf.length)]; + builder.add( + Math.round(clusterX + x), + Math.round(clusterY + y), + Math.round(clusterZ + z), + leafColor + ); + } + } + } + } + + // 花朵 (散布在叶球表面) + const flowerCount = 4 + Math.floor(Math.random() * 6); + for (let f = 0; f < flowerCount; f++) { + const fAngle = Math.random() * Math.PI * 2; + const fPhi = Math.random() * Math.PI * 0.5; // 只在上半球 + const fDist = leafR * 0.9; + + const fx = clusterX + Math.sin(fPhi) * Math.cos(fAngle) * fDist; + const fy = clusterY + Math.cos(fPhi) * fDist; + const fz = clusterZ + Math.sin(fPhi) * Math.sin(fAngle) * fDist; + + const flowerColor = colors.flower[Math.floor(Math.random() * colors.flower.length)]; + builder.add(Math.round(fx), Math.round(fy), Math.round(fz), flowerColor); + // 相邻花瓣 + if (Math.random() > 0.5) { + builder.add(Math.round(fx + 1), Math.round(fy), Math.round(fz), flowerColor); + } + if (Math.random() > 0.5) { + builder.add(Math.round(fx), Math.round(fy), Math.round(fz + 1), flowerColor); + } + } + } +}; + +/** + * 越橘/蓝莓丛 (Blueberry Bush) + * 低矮灌木,带有蓝色浆果 + * 高度: 6-10 体素,宽度: 8-12 体素 + */ +const createBlueberryBush = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + berry: [C.blueberryFruit, C.blueberryRipe], + leaf: [C.blueberryLeaf, C.blueberryLeafDark], + stem: C.blueberryStem + }; + + // 灌木高度 + const height = 6 + Math.floor(Math.random() * 5); + const radius = 4 + Math.floor(Math.random() * 4); + + // 主茎 (多个细茎从基部分出) + const stemCount = 3 + Math.floor(Math.random() * 3); + for (let s = 0; s < stemCount; s++) { + const angle = (s / stemCount) * Math.PI * 2 + Math.random() * 0.3; + const dist = Math.random() * 1.5; + const sx = ox + Math.cos(angle) * dist; + const sz = oz + Math.sin(angle) * dist; + const stemH = height * (0.6 + Math.random() * 0.4); + + // 茎向外倾斜生长 + for (let y = 0; y < stemH; y++) { + const t = y / stemH; + const lean = t * 2; + const px = sx + Math.cos(angle) * lean; + const pz = sz + Math.sin(angle) * lean; + builder.add(Math.round(px), y, Math.round(pz), colors.stem); + } + } + + // 叶球 (不规则球形) + const centerY = height * 0.6; + for (let x = -radius; x <= radius; x++) { + for (let y = -Math.ceil(radius * 0.6); y <= Math.ceil(radius * 0.6); y++) { + for (let z = -radius; z <= radius; z++) { + // 压扁的椭球 + const d2 = (x * x) / (radius * radius) + + (y * y) / ((radius * 0.6) * (radius * 0.6)) + + (z * z) / (radius * radius); + + // 随机密度 + if (d2 <= 1 && Math.random() > 0.4) { + const leafColor = colors.leaf[Math.floor(Math.random() * colors.leaf.length)]; + builder.add(ox + x, Math.round(centerY + y), oz + z, leafColor); + } + } + } + } + + // 浆果 (散布在叶丛表面和内部) + const berryCount = 8 + Math.floor(Math.random() * 12); + for (let b = 0; b < berryCount; b++) { + const bAngle = Math.random() * Math.PI * 2; + const bPhi = Math.random() * Math.PI; + const bDist = radius * (0.5 + Math.random() * 0.4); + + const bx = ox + Math.sin(bPhi) * Math.cos(bAngle) * bDist; + const by = centerY + Math.cos(bPhi) * bDist * 0.6; + const bz = oz + Math.sin(bPhi) * Math.sin(bAngle) * bDist; + + // 浆果通常成簇出现 + const berryColor = colors.berry[Math.floor(Math.random() * colors.berry.length)]; + builder.add(Math.round(bx), Math.round(by), Math.round(bz), berryColor); + + // 有时相邻有更多浆果 + if (Math.random() > 0.6) { + builder.add(Math.round(bx + 1), Math.round(by), Math.round(bz), berryColor); + } + if (Math.random() > 0.7) { + builder.add(Math.round(bx), Math.round(by - 1), Math.round(bz), berryColor); + } + } +}; + +// ===================== +// 2. 亲水植物 (Riparian Plants) +// ===================== + +/** + * 溪荪/菖蒲 (Iris / Sweet Flag) + * 剑形叶,紫黄色花朵,生长在溪边 + * 高度: 10-16 体素,宽度: 6-10 体素 + */ +const createStreamIris = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + flower: [C.irisPurple, C.irisDeep], + center: C.irisYellow, + leaf: [C.irisLeaf, C.irisLeafDark] + }; + + // 植株丛 + const clumpCount = 2 + Math.floor(Math.random() * 3); + + for (let c = 0; c < clumpCount; c++) { + const cx = ox + (c === 0 ? 0 : (Math.random() - 0.5) * 6); + const cz = oz + (c === 0 ? 0 : (Math.random() - 0.5) * 6); + + // 剑形叶片 (4-7 片) + const leafCount = 4 + Math.floor(Math.random() * 4); + const maxLeafH = 10 + Math.floor(Math.random() * 7); + + for (let l = 0; l < leafCount; l++) { + const angle = (l / leafCount) * Math.PI * 2 + Math.random() * 0.3; + const leafH = maxLeafH * (0.7 + Math.random() * 0.3); + const leafColor = colors.leaf[Math.floor(Math.random() * colors.leaf.length)]; + + // 叶片从基部向外倾斜 + for (let y = 0; y < leafH; y++) { + const t = y / leafH; + const lean = t * 1.5; // 向外倾斜 + const px = cx + Math.cos(angle) * lean; + const pz = cz + Math.sin(angle) * lean; + builder.add(Math.round(px), y, Math.round(pz), leafColor); + } + } + + // 花茎 (高于叶片) + if (Math.random() > 0.3) { + const flowerStemH = maxLeafH + 2 + Math.floor(Math.random() * 4); + for (let y = 0; y < flowerStemH; y++) { + builder.add(Math.round(cx), y, Math.round(cz), colors.leaf[0]); + } + + // 花朵 + const flowerY = flowerStemH; + const flowerColor = colors.flower[Math.floor(Math.random() * colors.flower.length)]; + + // 外轮花瓣 (3 片下垂) + for (let i = 0; i < 3; i++) { + const angle = (i / 3) * Math.PI * 2; + for (let d = 1; d <= 2; d++) { + const px = cx + Math.cos(angle) * d; + const pz = cz + Math.sin(angle) * d; + builder.add(Math.round(px), flowerY - Math.floor(d * 0.5), Math.round(pz), flowerColor); + } + } + + // 内轮花瓣 (3 片直立) + for (let i = 0; i < 3; i++) { + const angle = (i / 3) * Math.PI * 2 + Math.PI / 3; + const px = cx + Math.cos(angle) * 0.8; + const pz = cz + Math.sin(angle) * 0.8; + builder.add(Math.round(px), flowerY, Math.round(pz), flowerColor); + builder.add(Math.round(px), flowerY + 1, Math.round(pz), flowerColor); + } + + // 花心 (黄色) + builder.add(Math.round(cx), flowerY, Math.round(cz), colors.center); + } + } +}; + +/** + * 羊齿蕨 (Large Fern) + * 大型蕨类,羽状复叶展开 + * 高度: 8-14 体素,展幅: 12-18 体素 + */ +const createLargeFern = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + frond: [C.fernGreen, C.fernLight, C.fernDark], + stem: C.fernStem + }; + + // 蕨类中心 + const centerY = 2; + + // 短茎/根状茎 + for (let y = 0; y < centerY; y++) { + builder.add(ox, y, oz, colors.stem); + if (y === 0) { + builder.add(ox + 1, y, oz, colors.stem); + builder.add(ox - 1, y, oz, colors.stem); + builder.add(ox, y, oz + 1, colors.stem); + builder.add(ox, y, oz - 1, colors.stem); + } + } + + // 羽状复叶 (5-8 片) + const frondCount = 5 + Math.floor(Math.random() * 4); + const maxLength = 8 + Math.floor(Math.random() * 6); + + for (let f = 0; f < frondCount; f++) { + const angle = (f / frondCount) * Math.PI * 2 + Math.random() * 0.2; + const frondLength = maxLength * (0.7 + Math.random() * 0.3); + + // 叶轴 + for (let i = 0; i < frondLength; i++) { + const t = i / frondLength; + // 叶片从中心向外向上弯曲,然后下垂 + const rise = Math.sin(t * Math.PI) * 4; // 抛物线上升 + const droop = t * t * 2; // 末端下垂 + const dist = i * 0.8; + + const px = ox + Math.cos(angle) * dist; + const py = centerY + rise - droop; + const pz = oz + Math.sin(angle) * dist; + + const stemColor = colors.frond[0]; + builder.add(Math.round(px), Math.round(py), Math.round(pz), stemColor); + + // 小叶 (羽片) - 两侧交替 + if (i > 1 && i < frondLength - 1) { + const leafSize = Math.max(1, Math.floor((1 - t) * 3)); // 末端叶片较小 + + // 左侧小叶 + const leftAngle = angle + Math.PI / 2; + for (let ls = 1; ls <= leafSize; ls++) { + const lpx = px + Math.cos(leftAngle) * ls; + const lpz = pz + Math.sin(leftAngle) * ls; + const leafColor = colors.frond[Math.floor(Math.random() * colors.frond.length)]; + builder.add(Math.round(lpx), Math.round(py), Math.round(lpz), leafColor); + } + + // 右侧小叶 + const rightAngle = angle - Math.PI / 2; + for (let rs = 1; rs <= leafSize; rs++) { + const rpx = px + Math.cos(rightAngle) * rs; + const rpz = pz + Math.sin(rightAngle) * rs; + const leafColor = colors.frond[Math.floor(Math.random() * colors.frond.length)]; + builder.add(Math.round(rpx), Math.round(py), Math.round(rpz), leafColor); + } + } + } + } +}; + +/** + * 水芹 (Water Parsley) + * 低矮的伞形花序植物,白色小花 + * 高度: 5-9 体素,宽度: 6-10 体素 + */ +const createWaterParsley = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + leaf: [C.parsleyGreen, C.parsleyLight], + flower: C.parsleyFlower, + stem: C.parsley_stem + }; + + // 丛生,多个茎 + const stemCount = 4 + Math.floor(Math.random() * 4); + const maxH = 5 + Math.floor(Math.random() * 5); + + for (let s = 0; s < stemCount; s++) { + const angle = (s / stemCount) * Math.PI * 2 + Math.random() * 0.4; + const baseDist = Math.random() * 2; + const sx = ox + Math.cos(angle) * baseDist; + const sz = oz + Math.sin(angle) * baseDist; + const stemH = maxH * (0.7 + Math.random() * 0.3); + + // 茎 + for (let y = 0; y < stemH; y++) { + const t = y / stemH; + const lean = t * 1.5; + const px = sx + Math.cos(angle) * lean; + const pz = sz + Math.sin(angle) * lean; + builder.add(Math.round(px), y, Math.round(pz), colors.stem); + } + + // 羽状复叶 (沿茎分布) + const leafLevels = 2 + Math.floor(Math.random() * 2); + for (let l = 0; l < leafLevels; l++) { + const leafY = Math.floor(stemH * (0.3 + l * 0.25)); + const leafAngle = angle + (l % 2 === 0 ? Math.PI / 3 : -Math.PI / 3); + + for (let d = 1; d <= 2; d++) { + const lx = sx + Math.cos(angle) * (leafY / stemH * 1.5) + Math.cos(leafAngle) * d; + const lz = sz + Math.sin(angle) * (leafY / stemH * 1.5) + Math.sin(leafAngle) * d; + const leafColor = colors.leaf[Math.floor(Math.random() * colors.leaf.length)]; + builder.add(Math.round(lx), leafY, Math.round(lz), leafColor); + } + } + + // 伞形花序 (顶端) + const flowerY = Math.floor(stemH); + const umbrellaSize = 2 + Math.floor(Math.random() * 2); + const topX = sx + Math.cos(angle) * 1.5; + const topZ = sz + Math.sin(angle) * 1.5; + + for (let fx = -umbrellaSize; fx <= umbrellaSize; fx++) { + for (let fz = -umbrellaSize; fz <= umbrellaSize; fz++) { + if (fx * fx + fz * fz <= umbrellaSize * umbrellaSize && Math.random() > 0.4) { + builder.add(Math.round(topX + fx), flowerY, Math.round(topZ + fz), colors.flower); + } + } + } + } +}; + +// ===================== +// 3. 鲜艳蘑菇 (Colorful Mushrooms) +// ===================== + +/** + * 毒蝇伞 (Fly Agaric) + * 经典的红白斑点蘑菇 + * 高度: 6-12 体素,菌盖宽度: 6-10 体素 + */ +const createFlyAgaric = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + cap: [C.flyAgaricRed, C.flyAgaricDeep], + spot: C.flyAgaricSpot, + gill: C.flyAgaricGill, + stem: C.flyAgaricStem + }; + + // 随机大小 + const sizeFactor = 0.8 + Math.random() * 0.5; + const stemH = Math.floor((4 + Math.random() * 4) * sizeFactor); + const capR = Math.floor((3 + Math.random() * 2) * sizeFactor); + + // 茎 (白色,底部略粗) + for (let y = 0; y < stemH; y++) { + const t = y / stemH; + const r = 1.5 - t * 0.5; // 底粗顶细 + const rCeil = Math.ceil(r); + + for (let x = -rCeil; x <= rCeil; x++) { + for (let z = -rCeil; z <= rCeil; z++) { + if (x * x + z * z <= r * r) { + builder.add(ox + x, y, oz + z, colors.stem); + } + } + } + } + + // 茎环 (菌环) + const ringY = Math.floor(stemH * 0.6); + for (let i = 0; i < 8; i++) { + const angle = (i / 8) * Math.PI * 2; + const rx = Math.round(ox + Math.cos(angle) * 1.8); + const rz = Math.round(oz + Math.sin(angle) * 1.8); + builder.add(rx, ringY, rz, colors.stem); + } + + // 菌盖 (伞形,红色带白斑) + const capY = stemH; + const capH = Math.ceil(capR * 0.6); + + for (let y = 0; y < capH; y++) { + const t = y / capH; + // 伞形:底部宽,顶部窄 + const currentR = capR * (1 - t * 0.7); + const rCeil = Math.ceil(currentR); + + for (let x = -rCeil; x <= rCeil; x++) { + for (let z = -rCeil; z <= rCeil; z++) { + const d2 = x * x + z * z; + if (d2 <= currentR * currentR) { + // 底层是菌褶 + if (y === 0 && d2 > 1) { + builder.add(ox + x, capY, oz + z, colors.gill); + } else { + // 红色菌盖 + const capColor = colors.cap[Math.floor(Math.random() * colors.cap.length)]; + builder.add(ox + x, capY + y, oz + z, capColor); + } + } + } + } + } + + // 白色斑点 (随机分布在菌盖表面) + const spotCount = 5 + Math.floor(Math.random() * 8); + for (let s = 0; s < spotCount; s++) { + const spotAngle = Math.random() * Math.PI * 2; + const spotDist = Math.random() * capR * 0.8; + const spotY = capY + Math.floor(Math.random() * capH); + + const sx = Math.round(ox + Math.cos(spotAngle) * spotDist); + const sz = Math.round(oz + Math.sin(spotAngle) * spotDist); + builder.add(sx, spotY, sz, colors.spot); + + // 有时斑点更大 + if (Math.random() > 0.6) { + builder.add(sx + 1, spotY, sz, colors.spot); + } + } +}; + +/** + * 紫晶蜡蘑 (Amethyst Deceiver) + * 全株紫色的梦幻蘑菇 + * 高度: 4-8 体素,菌盖宽度: 3-6 体素 + */ +const createAmethystMushroom = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + cap: [C.amethystPurple, C.amethystDeep, C.amethystLight], + stem: C.amethystStem + }; + + // 通常成群生长 (1-4 个) + const count = 1 + Math.floor(Math.random() * 4); + + for (let m = 0; m < count; m++) { + const mx = ox + (m === 0 ? 0 : (Math.random() - 0.5) * 5); + const mz = oz + (m === 0 ? 0 : (Math.random() - 0.5) * 5); + + const sizeFactor = 0.6 + Math.random() * 0.6; + const stemH = Math.floor((3 + Math.random() * 3) * sizeFactor); + const capR = Math.floor((2 + Math.random() * 2) * sizeFactor); + + // 紫色茎 (纤细) + for (let y = 0; y < stemH; y++) { + builder.add(Math.round(mx), y, Math.round(mz), colors.stem); + // 底部稍宽 + if (y === 0) { + builder.add(Math.round(mx + 1), y, Math.round(mz), colors.stem); + } + } + + // 紫色菌盖 (扁平伞形) + const capY = stemH; + for (let x = -capR; x <= capR; x++) { + for (let z = -capR; z <= capR; z++) { + const d2 = x * x + z * z; + if (d2 <= capR * capR) { + const capColor = colors.cap[Math.floor(Math.random() * colors.cap.length)]; + builder.add(Math.round(mx + x), capY, Math.round(mz + z), capColor); + // 顶部中心凸起 + if (d2 <= (capR * 0.3) * (capR * 0.3)) { + builder.add(Math.round(mx + x), capY + 1, Math.round(mz + z), colors.cap[0]); + } + } + } + } + } +}; + +/** + * 鸡油菌 (Chanterelle) + * 橙黄色喇叭形蘑菇 + * 高度: 4-8 体素,宽度: 4-7 体素 + */ +const createChanterelle = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + body: [C.chanterelleOrange, C.chanterelleYellow, C.chanterelleDeep], + ridge: C.chanterelleRidge + }; + + // 成群生长 (2-5 个) + const count = 2 + Math.floor(Math.random() * 4); + + for (let m = 0; m < count; m++) { + const mx = ox + (m === 0 ? 0 : (Math.random() - 0.5) * 6); + const mz = oz + (m === 0 ? 0 : (Math.random() - 0.5) * 6); + + const sizeFactor = 0.7 + Math.random() * 0.5; + const height = Math.floor((4 + Math.random() * 4) * sizeFactor); + const topR = Math.floor((2 + Math.random() * 2) * sizeFactor); + + // 喇叭形主体 (底窄顶宽) + for (let y = 0; y < height; y++) { + const t = y / height; + // 喇叭曲线:底部窄,向上逐渐展开 + const currentR = 0.5 + (topR - 0.5) * Math.pow(t, 0.7); + const rCeil = Math.ceil(currentR); + + for (let x = -rCeil; x <= rCeil; x++) { + for (let z = -rCeil; z <= rCeil; z++) { + const d2 = x * x + z * z; + // 空心喇叭形 + const outerR2 = currentR * currentR; + const innerR2 = (currentR - 0.8) * (currentR - 0.8); + + if (d2 <= outerR2 && (y < 2 || d2 >= innerR2)) { + const bodyColor = colors.body[Math.floor(Math.random() * colors.body.length)]; + builder.add(Math.round(mx + x), y, Math.round(mz + z), bodyColor); + } + } + } + } + + // 菌褶 (沿着喇叭内侧向下延伸的脊) + const ridgeCount = 6 + Math.floor(Math.random() * 4); + for (let r = 0; r < ridgeCount; r++) { + const angle = (r / ridgeCount) * Math.PI * 2; + for (let y = Math.floor(height * 0.3); y < height; y++) { + const t = y / height; + const ridgeDist = (0.3 + (topR - 0.5) * Math.pow(t, 0.7)) * 0.7; + const rx = Math.round(mx + Math.cos(angle) * ridgeDist); + const rz = Math.round(mz + Math.sin(angle) * ridgeDist); + builder.add(rx, y, rz, colors.ridge); + } + } + } +}; + +/** + * 金针菇群 (Enoki Cluster) + * 细长的金黄色蘑菇成簇生长 + * 高度: 6-12 体素,簇宽度: 4-8 体素 + */ +const createEnokiCluster = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + cap: C.enokiCap, + stem: [C.enokiStem, C.enokiLight], + base: C.enokiBase + }; + + // 基座 (共同的根部) + const baseR = 2 + Math.floor(Math.random() * 2); + for (let x = -baseR; x <= baseR; x++) { + for (let z = -baseR; z <= baseR; z++) { + if (x * x + z * z <= baseR * baseR) { + builder.add(ox + x, 0, oz + z, colors.base); + } + } + } + + // 金针菇群 (15-30 根细长的菌柄) + const mushroomCount = 15 + Math.floor(Math.random() * 16); + + for (let m = 0; m < mushroomCount; m++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * baseR * 0.8; + const mx = ox + Math.cos(angle) * dist; + const mz = oz + Math.sin(angle) * dist; + + // 细长茎 (高度随机) + const stemH = 5 + Math.floor(Math.random() * 8); + const stemColor = colors.stem[Math.floor(Math.random() * colors.stem.length)]; + + // 轻微弯曲 + const leanAngle = Math.random() * Math.PI * 2; + const leanAmount = Math.random() * 0.3; + + for (let y = 1; y < stemH; y++) { + const t = y / stemH; + const lean = t * leanAmount * 2; + const px = mx + Math.cos(leanAngle) * lean; + const pz = mz + Math.sin(leanAngle) * lean; + builder.add(Math.round(px), y, Math.round(pz), stemColor); + } + + // 小菌盖 (顶端) + const capY = stemH; + const finalX = mx + Math.cos(leanAngle) * leanAmount * 2; + const finalZ = mz + Math.sin(leanAngle) * leanAmount * 2; + builder.add(Math.round(finalX), capY, Math.round(finalZ), colors.cap); + + // 有时盖子稍大 + if (Math.random() > 0.7) { + builder.add(Math.round(finalX + 1), capY, Math.round(finalZ), colors.cap); + } + } +}; + +/** + * 荧光小菇 (Bioluminescent Mushroom) + * 发出蓝绿色光芒的神秘蘑菇 + * 高度: 3-6 体素,宽度: 2-4 体素 + */ +const createBioluminescentMushroom = (builder: VoxelBuilder, ox: number, oz: number) => { + const colors = { + glow: [C.bioGlow, C.bioGlowGreen, C.bioGlowBlue], + stem: C.bioStem, + core: C.bioGlowCore + }; + + // 成群生长 (3-8 个) + const count = 3 + Math.floor(Math.random() * 6); + + for (let m = 0; m < count; m++) { + const mx = ox + (m === 0 ? 0 : (Math.random() - 0.5) * 6); + const mz = oz + (m === 0 ? 0 : (Math.random() - 0.5) * 6); + + const sizeFactor = 0.5 + Math.random() * 0.7; + const stemH = Math.floor((2 + Math.random() * 3) * sizeFactor); + const capR = Math.floor((1 + Math.random() * 1.5) * sizeFactor); + + // 半透明发光茎 + for (let y = 0; y < stemH; y++) { + builder.add(Math.round(mx), y, Math.round(mz), colors.stem); + } + + // 发光菌盖 (半球形) + const capY = stemH; + const glowColor = colors.glow[Math.floor(Math.random() * colors.glow.length)]; + + for (let x = -capR; x <= capR; x++) { + for (let y = 0; y <= capR; y++) { + for (let z = -capR; z <= capR; z++) { + const d2 = x * x + y * y + z * z; + if (d2 <= capR * capR) { + builder.add(Math.round(mx + x), capY + y, Math.round(mz + z), glowColor); + } + } + } + } + + // 中心发光核心 + builder.add(Math.round(mx), capY, Math.round(mz), colors.core); + } +}; + +// ========================================= +// --- 山地生成器列表注册 --- +// ========================================= + +// 山地低矮植物(地被植物) +const MOUNTAIN_GROUND_COVER = [ + createGentian, // 龙胆草 (蓝紫色花) + createGentian, // 权重 x2 + createAlpineAzalea, // 高山杜鹃 (粉红灌丛) + createBlueberryBush, // 越橘/蓝莓丛 (带浆果) + createBlueberryBush, // 权重 x2 +]; + +// 山地亲水植物(溪边) +const MOUNTAIN_RIPARIAN = [ + createStreamIris, // 溪荪/菖蒲 (紫黄花) + createStreamIris, // 权重 x2 + createLargeFern, // 羊齿蕨 (大型) + createLargeFern, // 权重 x2 + createWaterParsley, // 水芹 (白色小花) +]; + +// 山地蘑菇 +const MOUNTAIN_MUSHROOMS = [ + createFlyAgaric, // 毒蝇伞 (红白斑点) ⭐ + createFlyAgaric, // 权重 x2 - 经典蘑菇 + createAmethystMushroom, // 紫晶蜡蘑 (紫色) + createChanterelle, // 鸡油菌 (橙黄色) + createChanterelle, // 权重 x2 + createEnokiCluster, // 金针菇群 (金黄细长) + createBioluminescentMushroom, // 荧光小菇 (蓝绿发光) +]; + +// 完整的山地植物生成器(树木 + 地被 + 蘑菇) +const MOUNTAIN_TREE_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine]; + const FOREST_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine]; const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom]; const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch]; @@ -2555,7 +3512,11 @@ export const generateVegetation = async ( generators = DESERT_GENERATORS; density = 0.05; // 基础密度(用于非戈壁场景的兼容) break; - case 'mountain': // Mapping Qiulin/Temperate to Mountain/Plains + case 'mountain': + // 山地场景:使用树木 + 地被植物 + 蘑菇的组合 + generators = MOUNTAIN_TREE_GENERATORS; + density = 0.12; // 适中密度 + break; case 'plains': generators = FOREST_GENERATORS; density = 0.15; // Dense forest @@ -2928,8 +3889,877 @@ export const generateVegetation = async ( console.log(`Placed: desert=${desertPlaced}, colonies=${totalColonyPlants}, scattered=${scatteredPlants}, rare=${rarePlaced}`); + } else if (sceneType === 'mountain') { + // ============================================================================ + // 山地场景:两种生成策略 + // 策略一:基于海拔高度的树林生成(种群特征) + // 策略二:沿河道的植物生成 + // ============================================================================ + console.log(`Mountain vegetation: generating with elevation-based forest clusters and riparian zones`); + + // 从 context 获取山地专用数据 + const { mountainStreamMap, mountainRockContext, terrainHeightMap } = context; + + // 统计计数器 + let forestClustersCreated = 0; + let treesPlaced = 0; + let shrubsPlaced = 0; + let groundCoverPlaced = 0; + let riparianGrassPlaced = 0; + let riparianPlantsPlaced = 0; + let mushroomsPlaced = 0; + + // ========================================== + // 辅助函数 + // ========================================== + + // 检查位置是否在水中 + const isInWater = (lx: number, lz: number): boolean => { + if (!mountainStreamMap) return false; + // 检查该 tile 的多个微体素位置 + for (let dx = 0; dx < 8; dx += 2) { + for (let dz = 0; dz < 8; dz += 2) { + const mx = lx * 8 + dx; + const mz = lz * 8 + dz; + const streamInfo = mountainStreamMap.get(`${mx}|${mz}`); + if (streamInfo && streamInfo.depth > 0) return true; + } + } + return false; + }; + + // 检查位置是否有石头 + const hasStone = (lx: number, lz: number): boolean => { + if (!mountainRockContext) return false; + const stoneH = mountainRockContext.stoneHeight[lx]?.[lz] ?? 0; + const stoneD = mountainRockContext.stoneDepth[lx]?.[lz] ?? 0; + return stoneH > 0 || stoneD > 0; + }; + + // 检查位置是否靠近石头(用于生成松树) + const isNearStone = (lx: number, lz: number, radius: number = 2): boolean => { + if (!mountainRockContext) return false; + for (let dx = -radius; dx <= radius; dx++) { + for (let dz = -radius; dz <= radius; dz++) { + const nx = lx + dx; + const nz = lz + dz; + if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) { + if (hasStone(nx, nz)) return true; + } + } + } + return false; + }; + + // 计算到最近河流的距离(体素级别) + const getDistanceToRiver = (lx: number, lz: number): number => { + if (!mountainStreamMap || mountainStreamMap.size === 0) return Infinity; + let minDist = Infinity; + const mx = lx * 8 + 4; + const mz = lz * 8 + 4; + + // 搜索周围区域 + const searchRadius = 15 * 8; // 15 tiles 的微体素范围 + mountainStreamMap.forEach((info, key) => { + if (info.depth > 0) { + const [sx, sz] = key.split('|').map(Number); + const dist = Math.sqrt((mx - sx) ** 2 + (mz - sz) ** 2); + if (dist < minDist) minDist = dist; + } + }); + return minDist / 8; // 转换为 tile 距离 + }; + + // 打乱数组 + 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; + }; + + // ========================================== + // 收集所有可用位置和高度信息 + // ========================================== + interface TileInfo { + lx: number; + lz: number; + surfaceY: number; + microX: number; + microZ: number; + terrainY: number; + isWater: boolean; + isStone: boolean; + nearStone: boolean; + distToRiver: number; + } + + const allTiles: TileInfo[] = []; + + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + 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; + + allTiles.push({ + lx, lz, surfaceY, microX, microZ, terrainY, + isWater: isInWater(lx, lz), + isStone: hasStone(lx, lz), + nearStone: isNearStone(lx, lz), + distToRiver: getDistanceToRiver(lx, lz) + }); + } + } + + // 计算高度分布 + const heights = allTiles.filter(t => !t.isWater && !t.isStone).map(p => p.terrainY); + const minHeight = Math.min(...heights); + const maxHeight = Math.max(...heights); + const heightRange = maxHeight - minHeight || 1; + + // 海拔阈值(放宽高海拔判定,让更多区域能生成阔叶林和杨树) + const highAltitude = minHeight + heightRange * 0.80; // 高海拔:80%+(只有最高区域) + const midAltitude = minHeight + heightRange * 0.40; // 中海拔:40%-80% + // 低海拔:0-40%(大部分平坦区域) + + // 可用于植物生成的位置(排除水和石头) + const availableTiles = allTiles.filter(t => !t.isWater && !t.isStone); + + console.log(`Mountain terrain: ${availableTiles.length} available tiles, height range: ${minHeight}-${maxHeight}`); + + // ========================================== + // 策略一:基于海拔高度的树林生成 + // ========================================== + + // 树木生成器配置(按海拔分布) + // 针叶林:主要使用密集针叶树,减少弯曲松树 + const CONIFER_TREES = [ + createDensePine, createDensePine, createDensePine, createDensePine, // 密集针叶 x4 + createScotsPine, // 弯曲松树 x1(少量点缀) + ]; + // 阔叶林:橡树和白桦(杨树单独成林) + const BROADLEAF_TREES = [createLushOak, createLushOak, createProceduralBirch, createProceduralBirch]; + // 杨树林:单独成林 + const POPLAR_TREES = [createFatPoplar]; + // 石头附近:主要用密集针叶树 + const STONE_TREES = [createDensePine, createDensePine, createDensePine, createScotsPine]; + + // 灌木生成器(用于树林边缘过渡) + const EDGE_SHRUBS = [createBlueberryBush, createAlpineAzalea]; + + // 计算树林数量(与地图大小正相关) + // 每 100 个 tile 约 1 个树林,最少 2 个,最多 6 个(减少树林数量) + const targetForestCount = Math.max(2, Math.min(6, Math.floor(mapArea / 100))); + + // 选择树林中心点(分散在地图各处) + interface ForestCluster { + centerX: number; + centerZ: number; + altitude: 'high' | 'mid' | 'low'; + nearStone: boolean; + treeType: 'conifer' | 'broadleaf' | 'poplar' | 'stone'; // 树林类型 + radius: number; // 树林半径(tile 数) + trees: TileInfo[]; + } + + const forestClusters: ForestCluster[] = []; + + // 按高度降序排序,让高处优先被选为树林中心(针叶林优先) + const sortedByHeight = [...availableTiles].sort((a, b) => b.terrainY - a.terrainY); + + // 最小树林间距(tile)- 增加间距,确保树林之间有足够空隙 + // 至少 5 tiles,或者地图大小的 1/4(取较大值) + const minForestSpacing = Math.max(5, Math.floor(mapSize / 4)); + + for (const tile of sortedByHeight) { + if (forestClusters.length >= targetForestCount) break; + + // 检查与已有树林的距离 + let tooClose = false; + for (const existing of forestClusters) { + const dx = tile.lx - existing.centerX; + const dz = tile.lz - existing.centerZ; + if (Math.sqrt(dx * dx + dz * dz) < minForestSpacing) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + // 确定海拔类型 + let altitude: 'high' | 'mid' | 'low'; + if (tile.terrainY >= highAltitude) { + altitude = 'high'; + } else if (tile.terrainY >= midAltitude) { + altitude = 'mid'; + } else { + altitude = 'low'; + } + + // 确定树林类型(单一树种成林) + // 大幅提高杨树和橡树(阔叶林)的比例 + let treeType: 'conifer' | 'broadleaf' | 'poplar' | 'stone'; + if (tile.nearStone) { + treeType = 'stone'; // 石头附近用针叶树 + } else if (altitude === 'high') { + // 高海拔(只有最高 20%):60% 针叶林,25% 阔叶林,15% 杨树林 + const r = Math.random(); + if (r < 0.60) { + treeType = 'conifer'; + } else if (r < 0.85) { + treeType = 'broadleaf'; + } else { + treeType = 'poplar'; + } + } else if (altitude === 'mid') { + // 中海拔:15% 针叶林,50% 阔叶林,35% 杨树林 + const r = Math.random(); + if (r < 0.15) { + treeType = 'conifer'; + } else if (r < 0.65) { + treeType = 'broadleaf'; + } else { + treeType = 'poplar'; + } + } else { + // 低海拔:50% 阔叶林,50% 杨树林 + treeType = Math.random() < 0.50 ? 'broadleaf' : 'poplar'; + } + + // 树林半径:针叶林更大(2-3 tiles),其他树林较小(1-2 tiles) + let radius: number; + if (treeType === 'conifer' || treeType === 'stone') { + radius = 2 + Math.floor(Math.random() * 2); // 针叶林:2-3 tiles + } else { + radius = 1 + Math.floor(Math.random() * 2); // 其他:1-2 tiles + } + + forestClusters.push({ + centerX: tile.lx, + centerZ: tile.lz, + altitude, + nearStone: tile.nearStone, + treeType, + radius, + trees: [] + }); + forestClustersCreated++; + } + + console.log(`Created ${forestClustersCreated} forest clusters (target: ${targetForestCount})`); + + // 为每个树林收集可用的 tile 并生成树木 + const tilesUsedByForest = new Set(); + + for (const forest of forestClusters) { + // 收集树林范围内的 tiles + for (const tile of availableTiles) { + const dx = tile.lx - forest.centerX; + const dz = tile.lz - forest.centerZ; + const dist = Math.sqrt(dx * dx + dz * dz); + + if (dist <= forest.radius) { + forest.trees.push(tile); + tilesUsedByForest.add(`${tile.lx}|${tile.lz}`); + } + } + + // 根据树林类型选择生成器(同一树林使用同一种树) + let treeGenerators: any[]; + switch (forest.treeType) { + case 'stone': + treeGenerators = STONE_TREES; + break; + case 'conifer': + treeGenerators = CONIFER_TREES; + break; + case 'poplar': + treeGenerators = POPLAR_TREES; + break; + case 'broadleaf': + default: + treeGenerators = BROADLEAF_TREES; + break; + } + + // 在树林中生成树木 + // 针叶林密度更高,其他树林正常密度 + const isConifer = forest.treeType === 'conifer' || forest.treeType === 'stone'; + + for (const tile of forest.trees) { + const dx = tile.lx - forest.centerX; + const dz = tile.lz - forest.centerZ; + const distFromCenter = Math.sqrt(dx * dx + dz * dz); + const normalizedDist = forest.radius > 0 ? distFromCenter / forest.radius : 0; + + // 边缘的树更矮(通过随机偏移减少) + const isEdge = normalizedDist > 0.6; + + // 针叶林密度更高:中心 100%,边缘 85% + // 其他树林:中心 95%,边缘 70% + let density: number; + if (isConifer) { + density = isEdge ? 0.85 : 1.0; // 针叶林更密集 + } else { + density = isEdge ? 0.7 : 0.95; + } + + if (Math.random() < density) { + // 选择树木生成器 + const gen = treeGenerators[Math.floor(Math.random() * treeGenerators.length)]; + + // 随机偏移避免完全对齐 + const offsetX = (Math.random() - 0.5) * 16; + const offsetZ = (Math.random() - 0.5) * 16; + + builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ); + try { + gen(builder, 0, 0); + builder.commit(); + treesPlaced++; + } catch (e) { + // 忽略 + } + } + + // 边缘生成灌木过渡 + if (isEdge && Math.random() < 0.5) { + const shrubGen = EDGE_SHRUBS[Math.floor(Math.random() * EDGE_SHRUBS.length)]; + const shrubOffsetX = (Math.random() - 0.5) * 20; + const shrubOffsetZ = (Math.random() - 0.5) * 20; + + builder.setOffset(tile.microX + shrubOffsetX, tile.surfaceY, tile.microZ + shrubOffsetZ); + try { + shrubGen(builder, 0, 0); + builder.commit(); + shrubsPlaced++; + } catch (e) { + // 忽略 + } + } + } + } + + // ========================================== + // 策略二:沿河道的植物生成 + // ========================================== + + if (mountainStreamMap && mountainStreamMap.size > 0) { + // 收集河岸位置(按距离分类) + const riverBankTiles: TileInfo[] = []; // 距离 <= 2 的紧贴河岸 + const nearRiverTiles: TileInfo[] = []; // 距离 2-5 的河岸附近 + + for (const tile of availableTiles) { + if (tile.distToRiver <= 2) { + riverBankTiles.push(tile); + } else if (tile.distToRiver <= 5) { + nearRiverTiles.push(tile); + } + } + + // === 2.1 沿河道直接生成草(不基于 tile 网格,完全随机位置)=== + // 直接遍历河流微体素,在河岸边生成草 + const streamLength = mountainStreamMap.size; + + // 河岸矮草(主要草类)- 不包含芦苇和香蒲,它们单独成丛生成 + const SHORT_RIVERBANK_GRASS = [ + createRiverbankTinyGrass, createRiverbankTinyGrass, createRiverbankTinyGrass, + createTinyGrass, createTinyGrass, + createTinyGrassPatch, + createSedge, // 莎草(中高)也放这里 + ]; + + console.log(`River stats: ${streamLength} stream voxels, generating grass along riverbank`); + + let grassPlaced = 0; + const grassPositions = new Set(); // 记录已生成草的位置,避免完全重叠 + + // 遍历所有河流微体素,在其周围生成矮草 + mountainStreamMap.forEach((streamInfo, key) => { + if (streamInfo.depth <= 0) return; // 只处理有水的位置 + + const [streamX, streamZ] = key.split('|').map(Number); + + // 在河流微体素周围 1-10 微体素范围内生成草 + // 适中密度,远水稍稀疏 + for (let dist = 1; dist <= 10; dist++) { + // 每个距离圈生成的草数量(恢复适中密度) + let grassCountAtDist: number; + let spawnChance: number; + + if (dist <= 2) { + grassCountAtDist = 2; // 近水:2 + spawnChance = 0.7; // 70% 概率 + } else if (dist <= 4) { + grassCountAtDist = 2; // 中近:2 + spawnChance = 0.6; + } else if (dist <= 6) { + grassCountAtDist = 1; // 中等:1 + spawnChance = 0.5; + } else if (dist <= 8) { + grassCountAtDist = 1; // 远水:1 + spawnChance = 0.35; + } else { + grassCountAtDist = 1; // 最远:1 + spawnChance = 0.2; + } + + for (let g = 0; g < grassCountAtDist; g++) { + // 根据距离决定是否生成 + if (Math.random() > spawnChance) continue; + + // 随机角度 + const angle = Math.random() * Math.PI * 2; + // 在该距离圈内随机位置(加一点随机偏移) + const actualDist = dist + (Math.random() - 0.5) * 1.5; + + const gx = Math.round(streamX + Math.cos(angle) * actualDist); + const gz = Math.round(streamZ + Math.sin(angle) * actualDist); + + // 转换为 32x32 微体素坐标 + const microX = gx * 4; // 从 8x8 转换到 32x32 + const microZ = gz * 4; + + // 检查是否在水中 + const checkKey = `${gx}|${gz}`; + const checkInfo = mountainStreamMap.get(checkKey); + if (checkInfo && checkInfo.depth > 0) continue; // 在水中,跳过 + + // 检查间距 + const spacingDivisor = dist <= 4 ? 4 : 5; + const posKey = `${Math.floor(microX/spacingDivisor)}|${Math.floor(microZ/spacingDivisor)}`; + if (grassPositions.has(posKey)) continue; + grassPositions.add(posKey); + + // 获取地面高度 + const lx = Math.floor(gx / 8); + const lz = Math.floor(gz / 8); + if (lx < 0 || lx >= mapSize || lz < 0 || lz >= mapSize) continue; + + // === 检查是否在石头上(避免在石头上生成草)=== + if (hasStone(lx, lz)) continue; + + const hKey = `${lx*8+4}|${lz*8+4}`; + const terrainY = heightMap.get(hKey); + if (terrainY === undefined) continue; + const surfaceY = (terrainY + 1) * 4; + + // === 检查是否在悬崖边缘(避免悬空)=== + // 检查周围 4 个方向的地形高度 + let isOnEdge = false; + const checkDirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + for (const [dx, dz] of checkDirs) { + const neighborLx = lx + dx; + const neighborLz = lz + dz; + if (neighborLx < 0 || neighborLx >= mapSize || neighborLz < 0 || neighborLz >= mapSize) { + isOnEdge = true; + break; + } + const neighborKey = `${neighborLx*8+4}|${neighborLz*8+4}`; + const neighborY = heightMap.get(neighborKey); + if (neighborY === undefined || Math.abs(neighborY - terrainY) > 1) { + // 高度差大于 1,是悬崖边缘 + isOnEdge = true; + break; + } + } + + // 如果在边缘,降低生成概率 + if (isOnEdge && Math.random() > 0.3) continue; + + // 只生成矮草 + const gen = SHORT_RIVERBANK_GRASS[Math.floor(Math.random() * SHORT_RIVERBANK_GRASS.length)]; + + // 添加小随机偏移让位置更自然(减少偏移避免悬空) + const maxOffset = isOnEdge ? 2 : 4; + const offsetX = (Math.random() - 0.5) * maxOffset; + const offsetZ = (Math.random() - 0.5) * maxOffset; + + builder.setOffset(microX + offsetX, surfaceY, microZ + offsetZ); + try { + gen(builder, 0, 0); + builder.commit(); + riparianGrassPlaced++; + grassPlaced++; + } catch (e) { + // 忽略 + } + } + } + }); + + // === 2.1b 在 tile 级别补充一些远离河岸的草(距离 3-5 tiles)=== + // 适中密度 + for (const tile of nearRiverTiles) { + if (tile.distToRiver < 3) continue; // 近河区域已经由上面的逻辑处理 + + // 距离 3-5 tiles,稀疏生成 + const distanceFactor = Math.max(0, 1 - (tile.distToRiver - 3) / 3); + const density = 0.08 + distanceFactor * 0.12; // 8%-20% + + if (Math.random() < density) { + // 检查是否在石头上 + if (hasStone(tile.lx, tile.lz)) continue; + + // 检查是否在悬崖边缘 + const checkDirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + let isOnEdge = false; + const tileY = Math.floor((tile.surfaceY - 4) / 4); + + for (const [dx, dz] of checkDirs) { + const neighborLx = tile.lx + dx; + const neighborLz = tile.lz + dz; + if (neighborLx < 0 || neighborLx >= mapSize || neighborLz < 0 || neighborLz >= mapSize) { + isOnEdge = true; + break; + } + const neighborKey = `${neighborLx*8+4}|${neighborLz*8+4}`; + const neighborY = heightMap.get(neighborKey); + if (neighborY === undefined || Math.abs(neighborY - tileY) > 1) { + isOnEdge = true; + break; + } + } + + // 边缘位置降低概率 + if (isOnEdge && Math.random() > 0.3) continue; + + // 使用完全随机的微体素位置(不绑定到 tile 中心) + const randomMicroX = tile.lx * 32 + Math.random() * 32; + const randomMicroZ = tile.lz * 32 + Math.random() * 32; + + const gen = SHORT_RIVERBANK_GRASS[Math.floor(Math.random() * SHORT_RIVERBANK_GRASS.length)]; + + builder.setOffset(randomMicroX, tile.surfaceY, randomMicroZ); + try { + gen(builder, 0, 0); + builder.commit(); + riparianGrassPlaced++; + grassPlaced++; + } catch (e) { + // 忽略 + } + } + } + + // === 2.2 芦苇/香蒲种群(成丛生成,紧贴河岸)=== + // 芦苇和香蒲只在这里生成,只在最靠近河流的位置 + // 优先选择 distToRiver <= 1 的 tiles(紧贴河岸) + const closestBankTiles = riverBankTiles.filter(t => t.distToRiver <= 1); + const shuffledClosestTiles = shuffleArray(closestBankTiles.length > 0 ? closestBankTiles : riverBankTiles); + + // 芦苇丛数量:每 10 个紧贴河岸的 tile 生成 1 丛 + const targetReedClusters = Math.max(1, Math.floor(shuffledClosestTiles.length / 10)); + + let reedClustersCreated = 0; + const usedForReeds = new Set(); + + for (const tile of shuffledClosestTiles) { + if (reedClustersCreated >= targetReedClusters) break; + + // 检查是否在石头上 + if (hasStone(tile.lx, tile.lz)) continue; + + // 检查间距:4 tiles + let tooClose = false; + for (const key of usedForReeds) { + const [px, pz] = key.split('|').map(Number); + const dist = Math.sqrt((tile.lx - px) ** 2 + (tile.lz - pz) ** 2); + if (dist < 4) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + usedForReeds.add(`${tile.lx}|${tile.lz}`); + + // 每个芦苇丛 6-10 株 + const reedCount = 6 + Math.floor(Math.random() * 5); + for (let r = 0; r < reedCount; r++) { + const gen = Math.random() < 0.7 ? createReed : createCattail; + // 丛内分布紧凑,朝向河流方向偏移 + // 随机偏移但范围小,保持靠近河岸 + const offsetX = (Math.random() - 0.5) * 14; + const offsetZ = (Math.random() - 0.5) * 14; + + builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ); + try { + gen(builder, 0, 0); + builder.commit(); + riparianGrassPlaced++; + } catch (e) { + // 忽略 + } + } + reedClustersCreated++; + } + + console.log(`Reed clusters created: ${reedClustersCreated}`); + + // === 2.3 河岸两侧的亲水植物(种群效应)=== + // 选择几个亲水植物种群中心 + const targetRiparianClusters = Math.max(2, Math.floor(nearRiverTiles.length / 6)); // 增加种群数量 + const riparianClusters: { centerTile: TileInfo; type: 'iris' | 'fern' | 'parsley' }[] = []; + + const shuffledNearRiver = shuffleArray(nearRiverTiles); + const minClusterSpacing = 3; + + for (const tile of shuffledNearRiver) { + if (riparianClusters.length >= targetRiparianClusters) break; + + // 检查间距 + let tooClose = false; + for (const existing of riparianClusters) { + const dx = tile.lx - existing.centerTile.lx; + const dz = tile.lz - existing.centerTile.lz; + if (Math.sqrt(dx * dx + dz * dz) < minClusterSpacing) { + tooClose = true; + break; + } + } + if (tooClose) continue; + + // 随机选择植物类型 + const types: ('iris' | 'fern' | 'parsley')[] = ['iris', 'fern', 'parsley']; + const type = types[Math.floor(Math.random() * types.length)]; + + riparianClusters.push({ centerTile: tile, type }); + } + + // 为每个种群生成植物 + for (const cluster of riparianClusters) { + const gen = cluster.type === 'iris' ? createStreamIris + : cluster.type === 'fern' ? createLargeFern + : createWaterParsley; + + // 种群内 3-6 株 + const plantCount = 3 + Math.floor(Math.random() * 4); + + for (let p = 0; p < plantCount; p++) { + const offsetX = (Math.random() - 0.5) * 28; + const offsetZ = (Math.random() - 0.5) * 28; + + builder.setOffset( + cluster.centerTile.microX + offsetX, + cluster.centerTile.surfaceY, + cluster.centerTile.microZ + offsetZ + ); + try { + gen(builder, 0, 0); + builder.commit(); + riparianPlantsPlaced++; + } catch (e) { + // 忽略 + } + } + } + } + + // ========================================== + // 空地:地被植物和蘑菇 + // ========================================== + + // 收集空地(没被树林占用、不是河岸紧贴处) + const emptyTiles = availableTiles.filter(t => + !tilesUsedByForest.has(`${t.lx}|${t.lz}`) && + t.distToRiver > 2 + ); + + // 地被植物:围绕树林边缘生成 + const groundCoverDensity = 0.15; + for (const tile of emptyTiles) { + // 检查是否靠近树林 + let nearForest = false; + for (const forest of forestClusters) { + const dx = tile.lx - forest.centerX; + const dz = tile.lz - forest.centerZ; + const dist = Math.sqrt(dx * dx + dz * dz); + if (dist <= forest.radius + 2) { + nearForest = true; + break; + } + } + + // 靠近树林的地方生成地被植物 + if (nearForest && Math.random() < groundCoverDensity) { + const gen = MOUNTAIN_GROUND_COVER[Math.floor(Math.random() * MOUNTAIN_GROUND_COVER.length)]; + const offsetX = (Math.random() - 0.5) * 20; + const offsetZ = (Math.random() - 0.5) * 20; + + builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ); + try { + gen(builder, 0, 0); + builder.commit(); + groundCoverPlaced++; + } catch (e) { + // 忽略 + } + } + } + + // 蘑菇:在所有树林内部和周围生成 + // 不同类型的树林有不同的蘑菇密度 + for (const forest of forestClusters) { + // 根据树林类型和海拔调整密度 + // 阔叶林和低海拔区域蘑菇更多 + let baseDensity: number; + if (forest.treeType === 'broadleaf' || forest.treeType === 'poplar') { + baseDensity = 0.15; // 阔叶林蘑菇更多 + } else if (forest.altitude === 'high') { + baseDensity = 0.05; // 高海拔针叶林蘑菇较少 + } else { + baseDensity = 0.1; // 其他针叶林 + } + + // 在树林内部生成蘑菇 + for (const tile of forest.trees) { + if (Math.random() < baseDensity) { + const gen = MOUNTAIN_MUSHROOMS[Math.floor(Math.random() * MOUNTAIN_MUSHROOMS.length)]; + const offsetX = (Math.random() - 0.5) * 24; + const offsetZ = (Math.random() - 0.5) * 24; + + builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ); + try { + gen(builder, 0, 0); + builder.commit(); + mushroomsPlaced++; + } catch (e) { + // 忽略 + } + } + } + + // 在树林周围(边缘外 1-2 tiles)也生成少量蘑菇 + for (const tile of availableTiles) { + const dx = tile.lx - forest.centerX; + const dz = tile.lz - forest.centerZ; + const dist = Math.sqrt(dx * dx + dz * dz); + + // 树林边缘外 1-2 tiles 的范围 + if (dist > forest.radius && dist <= forest.radius + 2) { + // 边缘蘑菇密度较低 + if (Math.random() < baseDensity * 0.5) { + const gen = MOUNTAIN_MUSHROOMS[Math.floor(Math.random() * MOUNTAIN_MUSHROOMS.length)]; + const offsetX = (Math.random() - 0.5) * 28; + const offsetZ = (Math.random() - 0.5) * 28; + + builder.setOffset(tile.microX + offsetX, tile.surfaceY, tile.microZ + offsetZ); + try { + gen(builder, 0, 0); + builder.commit(); + mushroomsPlaced++; + } catch (e) { + // 忽略 + } + } + } + } + } + + // ========================================== + // 随机散布:蘑菇、花、草 + // ========================================== + + // 在整个地图上随机位置生成一些植被,增加自然感 + let randomPlantsPlaced = 0; + + // 随机花卉 + const RANDOM_FLOWERS = [ + createGentian, // 龙胆草(蓝紫色) + createAlpineAzalea, // 高山杜鹃(粉红) + ]; + + // 随机草类 + const RANDOM_GRASS = [ + createTinyGrass, + createTinyGrassPatch, + createScatteredSprouts, + ]; + + // 目标数量:根据地图大小 + const targetRandomPlants = Math.floor(mapSize * mapSize * 0.08); // 约 8% 的 tiles + + for (let i = 0; i < targetRandomPlants; i++) { + // 随机选择一个 tile + const lx = Math.floor(Math.random() * mapSize); + const lz = Math.floor(Math.random() * mapSize); + + // 检查是否在石头上 + if (hasStone(lx, lz)) continue; + + // 检查是否在水中 + const centerX = lx * 8 + 4; + const centerZ = lz * 8 + 4; + const streamKey = `${centerX}|${centerZ}`; + const streamInfo = mountainStreamMap?.get(streamKey); + if (streamInfo && streamInfo.depth > 0) continue; + + // 获取地面高度 + const hKey = `${lx*8+4}|${lz*8+4}`; + const terrainY = heightMap.get(hKey); + if (terrainY === undefined) continue; + const surfaceY = (terrainY + 1) * 4; + + // 检查是否在悬崖边缘 + let isOnEdge = false; + const checkDirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + for (const [dx, dz] of checkDirs) { + const neighborLx = lx + dx; + const neighborLz = lz + dz; + if (neighborLx < 0 || neighborLx >= mapSize || neighborLz < 0 || neighborLz >= mapSize) { + isOnEdge = true; + break; + } + const neighborKey = `${neighborLx*8+4}|${neighborLz*8+4}`; + const neighborY = heightMap.get(neighborKey); + if (neighborY === undefined || Math.abs(neighborY - terrainY) > 1) { + isOnEdge = true; + break; + } + } + if (isOnEdge) continue; + + // 随机位置偏移 + const microX = lx * 32 + Math.random() * 32; + const microZ = lz * 32 + Math.random() * 32; + + // 随机选择植物类型:50% 草,30% 花,20% 蘑菇 + const typeRoll = Math.random(); + let gen: any; + + if (typeRoll < 0.5) { + // 草 + gen = RANDOM_GRASS[Math.floor(Math.random() * RANDOM_GRASS.length)]; + } else if (typeRoll < 0.8) { + // 花 + gen = RANDOM_FLOWERS[Math.floor(Math.random() * RANDOM_FLOWERS.length)]; + } else { + // 蘑菇 + gen = MOUNTAIN_MUSHROOMS[Math.floor(Math.random() * MOUNTAIN_MUSHROOMS.length)]; + } + + builder.setOffset(microX, surfaceY, microZ); + try { + gen(builder, 0, 0); + builder.commit(); + randomPlantsPlaced++; + } catch (e) { + // 忽略 + } + } + + console.log(`Mountain placed: forests=${forestClustersCreated}, trees=${treesPlaced}, shrubs=${shrubsPlaced}, groundCover=${groundCoverPlaced}, riverGrass=${riparianGrassPlaced}, riparianPlants=${riparianPlantsPlaced}, mushrooms=${mushroomsPlaced}, randomPlants=${randomPlantsPlaced}`); + } else { - // ============= 非戈壁场景:使用原有概率生成逻辑 ============= + // ============= 其他场景:使用原有概率生成逻辑 ============= for (let lx = 0; lx < mapSize; lx++) { for (let lz = 0; lz < mapSize; lz++) { const stoneHeight = desertContext?.stoneHeight[lx]?.[lz] ?? 0; diff --git a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts index 9db213a..9352046 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -1512,7 +1512,11 @@ export const generateTerrain = async ( desertContext, isDesertScene, sceneType: sceneConfig?.name, - heightMap + heightMap, + // 山地场景专用数据 + mountainStreamMap: mountainStreamMap ?? undefined, + mountainRockContext: mountainRockContext ?? undefined, + terrainHeightMap: terrainHeightMap ?? undefined, }; const newPlantVoxels = await generateVegetation(vegContext); diff --git a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts index cc97ec8..4a267ac 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts @@ -1286,8 +1286,8 @@ export const generateMountainStream = ( if (isBlocked || isUphill) { // 考虑生成支流 if (tributaryCount < MAX_TRIBUTARIES && stepsSinceLastTributary >= TRIBUTARY_COOLDOWN) { - // 【修改】支流宽度:主流的40%,最小3微体素(更窄) - const tributaryWidth = Math.max(3, Math.floor(currentWidth * 0.4)); + // 【修改】支流宽度:与主流一致(6微体素) + const tributaryWidth = 6; // 【新增】随机选择支流方向(左或右) const isLeftTributary = rng() > 0.5; @@ -1304,8 +1304,8 @@ export const generateMountainStream = ( { lx: currentPos.lx, lz: currentPos.lz }, currentHeight, tributaryWidth, - tributaryDir, // 【修改】直接传入确定的方向 - 6 + Math.floor(rng() * 5), // 【修改】长度:6-10格(稍微短一点) + tributaryDir, + visitedPositions, // 传入主流已访问位置,避免支流与主流相交 grid, getHeight, mapSize @@ -1459,80 +1459,264 @@ export const generateMountainStream = ( // 6. 生成支流函数(需要在主循环之前定义) /** - * 生成支流(正交90度方向,短路径) + * 生成支流 - 增强版:支持转向,延伸到边界或盆地 + * + * 特性: + * 1. 遇到障碍/上坡时可以转向(类似主流) + * 2. 一直延伸直到到达地图边界或陷入盆地 + * 3. 避免与主流路径相交 */ function generateTributary( startPos: { lx: number; lz: number }, startHeight: number, tributaryWidth: number, - tributaryDirection: Direction, // 【修改】直接传入方向,不再随机 - maxLength: number, + tributaryDirection: Direction, + mainPathVisited: Set, // 主流已访问的位置,用于避免相交 grid: Uint8Array, getHeight: (lx: number, lz: number) => number, mapSize: number ): RiverNode[] { const tributary: RiverNode[] = []; + const tributaryVisited = new Set(); // 支流自己的访问记录 + const posKey = (lx: number, lz: number) => `${lx}|${lz}`; - let currentPos = { lx: startPos.lx, lz: startPos.lz }; - let currentHeight = startHeight; - let currentWidth = tributaryWidth; // 【修改】直接使用传入的宽度,不再取最小值 + let currentWidth = tributaryWidth; + let currentDir = tributaryDirection; let accumulatedDrop = 0; - // 【修复】先添加分叉点(起点),确保与主流连接 - tributary.push({ - lx: startPos.lx, - lz: startPos.lz, - height: startHeight, - width: currentWidth, - direction: tributaryDirection, - accumulatedDrop: 0, - }); + const MAX_TRIBUTARY_STEPS = mapSize * 2; // 防止无限循环 - for (let step = 0; step < maxLength; step++) { - const dirVec = getDirectionVector(tributaryDirection); + // 【修复】不再把分叉点作为支流起点,而是从分叉点的下一格开始 + // 这样可以避免:1) 分叉点被重复渲染 2) 在分叉点生成不合理的瀑布 + + // 计算支流的第一个实际位置(分叉点的下一格) + const dirVec = getDirectionVector(tributaryDirection); + const firstLx = startPos.lx + dirVec.dx; + const firstLz = startPos.lz + dirVec.dz; + + // 边界检查 + if (firstLx < 0 || firstLx >= mapSize || firstLz < 0 || firstLz >= mapSize) { + console.log(`[支流生成] 支流第一格越界,丢弃`); + return []; + } + + // 障碍物检查 + if (grid[firstLz * mapSize + firstLx] === 1) { + console.log(`[支流生成] 支流第一格有障碍物,丢弃`); + return []; + } + + // 与主流相交检查 + if (mainPathVisited.has(posKey(firstLx, firstLz))) { + console.log(`[支流生成] 支流第一格与主流重叠,丢弃`); + return []; + } + + // 获取第一格的高度 + const firstHeight = getHeight(firstLx, firstLz); + + // 上坡检查 + if (firstHeight > startHeight) { + console.log(`[支流生成] 支流第一格上坡,丢弃`); + return []; + } + + // 初始化当前位置为第一格 + let currentPos = { lx: firstLx, lz: firstLz }; + let currentHeight = firstHeight; + + // 计算初始累计下降 + if (firstHeight < startHeight) { + accumulatedDrop = startHeight - firstHeight; + } + + // 添加第一个节点(不是分叉点,而是分叉点的下一格) + tributary.push({ + lx: firstLx, + lz: firstLz, + height: currentHeight, + width: currentWidth, + direction: currentDir, + accumulatedDrop: accumulatedDrop, + }); + tributaryVisited.add(posKey(firstLx, firstLz)); + // 也标记分叉点为已访问,防止支流回头 + tributaryVisited.add(posKey(startPos.lx, startPos.lz)); + + // 辅助函数:支流的转向选择(类似主流,但增加避开主流路径的检查) + const chooseTributaryTurn = ( + pos: { lx: number; lz: number }, + curDir: Direction, + curH: number + ): Direction | null => { + const options: Array<{ dir: Direction; score: number }> = []; + const leftDir = rotate90(curDir, false); + const rightDir = rotate90(curDir, 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 (grid[nextLz * mapSize + nextLx] === 1) { + continue; + } + + // 避免与主流相交(关键!) + if (mainPathVisited.has(posKey(nextLx, nextLz))) { + continue; + } + + // 避免支流自身循环 + if (tributaryVisited.has(posKey(nextLx, nextLz))) { + continue; + } + + const nextHeight = getHeight(nextLx, nextLz); + const heightDiff = curH - nextHeight; + + // 禁止上坡 + if (heightDiff < 0) { + continue; + } + + let score = 0; + // 下坡优先 + if (heightDiff > 0) { + score += heightDiff * 20; + } else { + score += 5; + } + // 添加随机性 + 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 isTributaryInBasin = (pos: { lx: number; lz: number }, curH: number): boolean => { + const checkDirs = [ + { dx: 1, dz: 0 }, + { dx: -1, dz: 0 }, + { dx: 0, dz: 1 }, + { dx: 0, dz: -1 }, + ]; + + for (const dir of checkDirs) { + const nx = pos.lx + dir.dx; + const nz = pos.lz + dir.dz; + + // 边界外不算障碍 + if (nx < 0 || nx >= mapSize || nz < 0 || nz >= mapSize) { + return false; // 可以流出边界,不是盆地 + } + + // 如果有一个方向是下坡或同高且无障碍,则不是盆地 + if (grid[nz * mapSize + nx] === 0) { + const neighborH = getHeight(nx, nz); + if (neighborH <= curH && !mainPathVisited.has(posKey(nx, nz)) && !tributaryVisited.has(posKey(nx, nz))) { + return false; + } + } + } + + return true; // 四周都是高地或障碍或已访问,是盆地 + }; + + // 主循环:延伸支流 + for (let step = 0; step < MAX_TRIBUTARY_STEPS; step++) { + const dirVec = getDirectionVector(currentDir); const nextLx = currentPos.lx + dirVec.dx; const nextLz = currentPos.lz + dirVec.dz; - // 边界检查 + // 边界检查 - 到达边界是成功结束 if (nextLx < 0 || nextLx >= mapSize || nextLz < 0 || nextLz >= mapSize) { + console.log(`[支流生成] 支流到达地图边界 (${currentPos.lx}, ${currentPos.lz}),长度: ${tributary.length}`); break; } + // 检查是否会与主流相交 + const wouldIntersectMain = mainPathVisited.has(posKey(nextLx, nextLz)); + // 障碍物检查 - const nextGrid = grid[nextLz * mapSize + nextLx]; - if (nextGrid === 1) { - break; // 遇到石头停止 - } + const isBlocked = grid[nextLz * mapSize + nextLx] === 1; + // 高度检查 const nextHeight = getHeight(nextLx, nextLz); + const isUphill = nextHeight > currentHeight; - // 上坡停止 - if (nextHeight > currentHeight) { - break; + // 访问检查 + const alreadyVisited = tributaryVisited.has(posKey(nextLx, nextLz)); + + // 碰撞处理:遇到障碍、上坡、主流路径或已访问 + if (isBlocked || isUphill || wouldIntersectMain || alreadyVisited) { + // 尝试转向 + const newDir = chooseTributaryTurn(currentPos, currentDir, currentHeight); + + if (newDir === null) { + // 无路可走,检查是否在盆地中 + if (isTributaryInBasin(currentPos, currentHeight)) { + console.log(`[支流生成] 支流陷入盆地 (${currentPos.lx}, ${currentPos.lz}),长度: ${tributary.length}`); + } else { + console.log(`[支流生成] 支流无路可走 (${currentPos.lx}, ${currentPos.lz}),长度: ${tributary.length}`); + } + break; + } + + currentDir = newDir; + continue; // 转向后重新尝试 } - // 更新位置 + // 可以前进 currentPos = { lx: nextLx, lz: nextLz }; + tributaryVisited.add(posKey(nextLx, 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, + width: currentWidth, + direction: currentDir, accumulatedDrop: accumulatedDrop, }); + + // 检查是否到达边缘 + const atEdge = ( + currentPos.lx === 0 || + currentPos.lx === mapSize - 1 || + currentPos.lz === 0 || + currentPos.lz === mapSize - 1 + ); + + if (atEdge) { + console.log(`[支流生成] 支流到达地图边缘 (${currentPos.lx}, ${currentPos.lz}),长度: ${tributary.length}`); + break; + } } - // 【新增】如果支流太短(< 3个节点),返回空数组 + // 如果支流太短(< 3个节点),返回空数组 if (tributary.length < 3) { console.log(`[支流生成] 支流过短 (${tributary.length} < 3),丢弃`); return [];