diff --git a/package-lock.json b/package-lock.json index 46358ce..0de828d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "WebCity", + "name": "VoxelGame", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/voxel-tactics-horizon/index.html b/voxel-tactics-horizon/index.html index daa7231..2f48f18 100644 --- a/voxel-tactics-horizon/index.html +++ b/voxel-tactics-horizon/index.html @@ -5,6 +5,34 @@ voxel-tactics-horizon + +
diff --git a/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts index a9519d3..14f9d8a 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts @@ -7,6 +7,7 @@ */ import type { VoxelType } from './voxelStyles'; +import { generateRockClusters } from './rockFeatures'; // MICRO_SCALE 常量:每个逻辑格子包含的微型体素数量 // const MICRO_SCALE = 8; // Unused @@ -22,6 +23,7 @@ export interface DesertContext { gobiVariant: number[][]; gobiMinHeight: number[][]; // 记录戈壁起始高度(用于拱门效果) stoneHeight: number[][]; + stoneDepth: number[][]; stoneVariant: number[][]; streamDepthMap: Map; // Added isCenter } @@ -478,89 +480,18 @@ export const createDesertContext = ( const gobiVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); const gobiMinHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); const stoneHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + const stoneDepth = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); const stoneVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); layoutStrategicDesert(mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand); - // Adjusted density: Ensure presence on 24/32 maps, occasional on 16 - // 32x32 (1024) / 200 = ~5 stones - // 24x24 (576) / 200 = ~2-3 stones - // 16x16 (256) / 200 = ~1.2 -> handled specifically below - let targetStones = Math.floor((mapSize * mapSize) / 200); - - if (mapSize <= 16) { - // For 16x16: 30% chance to spawn 1 stone, otherwise 0 - targetStones = rand() > 0.7 ? 1 : 0; - } - - let placedStones = 0; - let attempts = 0; - // Give plenty of attempts because the strict avoidance check will reject many positions - const maxAttempts = Math.max(20, targetStones * 50); - - while (placedStones < targetStones && attempts < maxAttempts) { - attempts++; - const startX = Math.floor(rand() * mapSize); - const startZ = Math.floor(rand() * mapSize); - - // Constraint: Max 3 blocks (1-3) - const targetSize = 1 + Math.floor(rand() * 3); - - // Algorithm: Random Walker - const rockCells: Array<{x: number, z: number}> = [{x: startX, z: startZ}]; - - let safety = 0; - while (rockCells.length < targetSize && safety++ < 10) { - const seed = rockCells[Math.floor(rand() * rockCells.length)]; - const dir = Math.floor(rand() * 4); - const dx = dir === 0 ? 1 : dir === 1 ? -1 : 0; - const dz = dir === 2 ? 1 : dir === 3 ? -1 : 0; - const nx = seed.x + dx; - const nz = seed.z + dz; - - if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) { - if (!rockCells.some(c => c.x === nx && c.z === nz)) { - rockCells.push({x: nx, z: nz}); - } - } - } - - let isValid = true; - const checkDist = 2; - - for (const cell of rockCells) { - for (let dx = -checkDist; dx <= checkDist; dx++) { - for (let dz = -checkDist; dz <= checkDist; dz++) { - const cx = cell.x + dx; - const cz = cell.z + dz; - - if (cx >= 0 && cx < mapSize && cz >= 0 && cz < mapSize) { - if (gobiHeight[cx][cz] > 0) { - isValid = false; - break; - } - if (stoneHeight[cx][cz] > 0) { - isValid = false; - break; - } - } - } - if (!isValid) break; - } - if (!isValid) break; - } - - if (isValid) { - const h = (targetSize > 1 && rand() > 0.85) ? 2 : 1; - const variant = Math.floor(rand() * 2); - - for (const cell of rockCells) { - stoneHeight[cell.x][cell.z] = h; - stoneVariant[cell.x][cell.z] = variant; - } - placedStones++; - } - } + // 生成巨石簇(跨越地表的石块) + generateRockClusters({ + mapSize, + rand, + field: { stoneHeight, stoneDepth, stoneVariant }, + avoidMaps: [gobiHeight], + }); const streamDepthMap = generateDesertStreamMap(mapSize, rand, gobiHeight, stoneHeight); return { @@ -568,6 +499,7 @@ export const createDesertContext = ( gobiVariant, gobiMinHeight, stoneHeight, + stoneDepth, stoneVariant, streamDepthMap, }; diff --git a/voxel-tactics-horizon/src/features/Map/logic/mountainFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/mountainFeatures.ts new file mode 100644 index 0000000..6b738f6 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/mountainFeatures.ts @@ -0,0 +1,66 @@ +/** + * 山地特征系统:生成跨越地表的巨石 + * - 石块部分在地上(1~3层),部分在地下(1~3层) + * - 直接复用石块风化与颜色过渡 + */ + +import { + createEmptyRockField, + generateRockClusters, + type RockFieldContext, +} from './rockFeatures'; + +export type MountainRockContext = RockFieldContext; + +const getMountainProfileOverride = (mapSize: number) => { + if (mapSize <= 16) { + return { + minClusters: 1, + maxClusters: 3, + maxCellsPerCluster: 2, + minAboveGround: 1, + maxAboveGround: 2, + minBelowGround: 1, + maxBelowGround: 2, + separationRadius: 2, + }; + } + if (mapSize <= 24) { + return { + minClusters: 2, + maxClusters: 4, + maxCellsPerCluster: 3, + minAboveGround: 1, + maxAboveGround: 3, + minBelowGround: 1, + maxBelowGround: 2, + separationRadius: 3, + }; + } + return { + minClusters: 3, + maxClusters: 5, + maxCellsPerCluster: 4, + minAboveGround: 2, + maxAboveGround: 3, + minBelowGround: 1, + maxBelowGround: 3, + separationRadius: 3, + }; +}; + +export const createMountainRockContext = ( + mapSize: number, + rand: () => number +): MountainRockContext => { + const field = createEmptyRockField(mapSize); + generateRockClusters({ + mapSize, + rand, + field, + profileOverride: getMountainProfileOverride(mapSize), + }); + return field; +}; + + diff --git a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts index 01e3b2f..7d8de00 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts @@ -91,6 +91,7 @@ export interface GobiWeatheringResult { */ export interface GobiWeatheringPreprocessParams { desertContext: DesertContext; // 沙漠上下文 + terrainHeightMap: number[][]; // 地形高度图(每个逻辑方块的基础高度) mapSize: number; // 地图大小(逻辑方块数) MICRO_SCALE: number; // 微观缩放比例 gobiMaxIterations: number; // 戈壁最大风蚀层数,默认 3 @@ -111,6 +112,7 @@ export interface GobiWeatheringPreprocessParams { export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams): GobiWeatheringResult { const { desertContext, + terrainHeightMap, mapSize, MICRO_SCALE, gobiMaxIterations, @@ -126,18 +128,25 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams) // 预估最大世界Y坐标(关键修复:基于真实的 worldY 计算) let maxWorldY = MIN_WORLD_Y; + let minWorldY = MIN_WORLD_Y; for (let lx = 0; lx < mapSize; lx++) { for (let lz = 0; lz < mapSize; lz++) { const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE; const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE; - const logicHeight = 2; // 沙漠场景的基础高度 - // 计算该位置的最大可能 worldY(表面高度) - const surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight; + const stoneMicroDepth = (desertContext.stoneDepth?.[lx]?.[lz] ?? 0) * MICRO_SCALE; + const logicHeight = terrainHeightMap[lx][lz]; // 使用真实地形高度 + + // 计算该位置的最大和最小可能 worldY + const groundLevelY = logicHeight * MICRO_SCALE + gobiMicroHeight; + const surfaceY = groundLevelY + stoneMicroHeight; // 最高点(石块顶部) + const bottomY = groundLevelY - stoneMicroDepth; // 最低点(石块底部) + maxWorldY = Math.max(maxWorldY, surfaceY); + minWorldY = Math.min(minWorldY, bottomY); } } - // 基于 maxWorldY 计算网格高度(相对于 MIN_WORLD_Y) + // 基于 maxWorldY 和 minWorldY 计算网格高度 const gridHeight = Math.ceil(maxWorldY - MIN_WORLD_Y + MICRO_SCALE * 2); // 创建3D网格:包含戈壁和巨石体素 @@ -168,10 +177,8 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams) // 只处理有戈壁的区域 if (gobiMicroHeight === 0) continue; - // 计算这个逻辑方块的基础高度(需要复制 terrain.ts 的逻辑) - // 注意:这里假设基础地形高度是固定的(沙漠场景通常是平坦的) - // 对于沙漠场景,logicHeight 通常是固定值 - const logicHeight = 2; // 沙漠场景的基础高度(从 terrain.ts 看出) + // 使用预计算的地形高度(不再硬编码为2) + const logicHeight = terrainHeightMap[lx][lz]; // 遍历该逻辑方块对应的所有微观方块 for (let mx = 0; mx < MICRO_SCALE; mx++) { @@ -212,15 +219,17 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams) } // 第一步(续):填充巨石体素到网格中 + let totalStoneVoxelsFilled = 0; for (let lx = 0; lx < mapSize; lx++) { for (let lz = 0; lz < mapSize; lz++) { const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE; + const stoneMicroDepth = (desertContext.stoneDepth?.[lx]?.[lz] ?? 0) * MICRO_SCALE; // 只处理有巨石的区域 - if (stoneMicroHeight === 0) continue; + if (stoneMicroHeight === 0 && stoneMicroDepth === 0) continue; - // 计算基础高度 - const logicHeight = 2; // 沙漠场景的基础高度 + // 使用预计算的地形高度(不再硬编码为2) + const logicHeight = terrainHeightMap[lx][lz]; // 遍历该逻辑方块对应的所有微观方块 for (let mx = 0; mx < MICRO_SCALE; mx++) { @@ -231,23 +240,26 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams) // 获取当前位置的戈壁高度(可能为0) const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE; - // 计算这个微观位置的 surfaceY - let surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight; + // 计算地表位置(groundLevelY) + const groundLevelY = logicHeight * MICRO_SCALE + gobiMicroHeight; - // 遍历高度,填充巨石体素 - for (let h = 0; h < stoneMicroHeight; h++) { - // 计算这个体素的真实世界Y坐标 - // 修复:巨石位于最顶层 (surfaceY),向下延伸 - // h 从 0 (底部) 到 stoneMicroHeight-1 (顶部) - // worldY 范围应为 [surfaceY - stoneMicroHeight + 1, surfaceY] - const worldY = surfaceY - stoneMicroHeight + 1 + h; - + // 石块范围:从地下延伸到地上 + // 地上部分:groundLevelY 到 groundLevelY + stoneMicroHeight + // 地下部分:groundLevelY - stoneMicroDepth 到 groundLevelY + const stoneTopY = groundLevelY + stoneMicroHeight; + const stoneBottomY = groundLevelY - stoneMicroDepth; + + // 填充整个石块范围(地下+地上) + let filledThisColumn = 0; + for (let worldY = stoneBottomY; worldY <= stoneTopY; worldY++) { // 转换为网格坐标(相对于 MIN_WORLD_Y) const gridY = worldY - MIN_WORLD_Y; if (inBounds(ix, gridY, iz)) { const idx = getIdx(ix, gridY, iz); voxelGrid[idx] = 2; // 标记为巨石体素 + totalStoneVoxelsFilled++; + filledThisColumn++; } } } @@ -441,6 +453,7 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams) // 第四步:收集被移除的体素(包括戈壁和巨石) const removedVoxels = new Set(); + let remainingStoneVoxels = 0; for (let ix = 0; ix < microMapSize; ix++) { for (let gridY = 0; gridY < gridHeight; gridY++) { @@ -454,6 +467,11 @@ export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams) const worldY = MIN_WORLD_Y + gridY; removedVoxels.add(`${ix}|${worldY}|${iz}`); } + + // 统计剩余的石块体素 + if (currentGrid[idx] === 2) { + remainingStoneVoxels++; + } } } } diff --git a/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts new file mode 100644 index 0000000..8263454 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/rockFeatures.ts @@ -0,0 +1,198 @@ +/** + * 通用巨石簇生成工具 + * - 根据地图尺寸推导默认分布策略 + * - 生成跨越地表的石块(部分在地上,部分在地下) + * - stoneHeight: 地表以上的逻辑方块层数(1~3) + * - stoneDepth: 地表以下的逻辑方块层数(1~3) + */ + +export interface RockFieldContext { + stoneHeight: number[][]; // 露出地面的方块层数 + stoneDepth: number[][]; // 埋在地下的方块层数 + stoneVariant: number[][]; +} + +interface RockClusterProfile { + minClusters: number; + maxClusters: number; + maxCellsPerCluster: number; + minAboveGround: number; // 地上最少层数 + maxAboveGround: number; // 地上最多层数 + minBelowGround: number; // 地下最少层数 + maxBelowGround: number; // 地下最多层数 + separationRadius: number; +} + +export interface RockClusterGenerationParams { + mapSize: number; + rand: () => number; + field: RockFieldContext; + avoidMaps?: Array; + profileOverride?: Partial; + maxAttempts?: number; +} + +const createZeroMatrix = (size: number): number[][] => + Array.from({ length: size }, () => Array(size).fill(0)); + +export const createEmptyRockField = (mapSize: number): RockFieldContext => ({ + stoneHeight: createZeroMatrix(mapSize), + stoneDepth: createZeroMatrix(mapSize), + stoneVariant: createZeroMatrix(mapSize), +}); + +const deriveProfile = (mapSize: number): RockClusterProfile => { + if (mapSize <= 16) { + return { + minClusters: 1, + maxClusters: 2, + maxCellsPerCluster: 2, + minAboveGround: 1, + maxAboveGround: 2, + minBelowGround: 1, + maxBelowGround: 2, + separationRadius: 2, + }; + } + if (mapSize <= 24) { + return { + minClusters: 2, + maxClusters: 4, + maxCellsPerCluster: 3, + minAboveGround: 1, + maxAboveGround: 3, + minBelowGround: 1, + maxBelowGround: 2, + separationRadius: 3, + }; + } + return { + minClusters: 3, + maxClusters: 5, + maxCellsPerCluster: 4, + minAboveGround: 1, + maxAboveGround: 3, + minBelowGround: 1, + maxBelowGround: 3, + separationRadius: 3, + }; +}; + +const isBlockedByMap = (maps: Array, x: number, z: number): boolean => + maps.some(map => (map[x]?.[z] ?? 0) > 0); + +const hasNearbyStone = ( + field: RockFieldContext, + x: number, + z: number, + radius: number, + mapSize: number +): boolean => { + for (let dx = -radius; dx <= radius; dx++) { + for (let dz = -radius; dz <= radius; dz++) { + const nx = x + dx; + const nz = z + dz; + if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) { + if ((field.stoneHeight[nx]?.[nz] ?? 0) > 0) { + return true; + } + } + } + } + return false; +}; + +const buildCluster = ( + startX: number, + startZ: number, + maxCells: number, + mapSize: number, + rand: () => number +) => { + const cells: Array<{ x: number; z: number }> = [{ x: startX, z: startZ }]; + let safety = 0; + while (cells.length < maxCells && safety++ < 10) { + const seed = cells[Math.floor(rand() * cells.length)]; + const dir = Math.floor(rand() * 4); + const dx = dir === 0 ? 1 : dir === 1 ? -1 : 0; + const dz = dir === 2 ? 1 : dir === 3 ? -1 : 0; + const nx = seed.x + dx; + const nz = seed.z + dz; + if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) { + if (!cells.some(c => c.x === nx && c.z === nz)) { + cells.push({ x: nx, z: nz }); + } + } + } + return cells; +}; + +export const generateRockClusters = (params: RockClusterGenerationParams): number => { + const { mapSize, rand, field, avoidMaps = [], profileOverride, maxAttempts } = params; + const profile: RockClusterProfile = { ...deriveProfile(mapSize), ...profileOverride }; + const targetClusters = profile.minClusters + Math.floor( + rand() * (profile.maxClusters - profile.minClusters + 1) + ); + const attemptLimit = maxAttempts ?? Math.max(25, targetClusters * 40); + + console.log(`[RockGen] Target clusters: ${targetClusters}, Profile:`, profile); + + let placed = 0; + let attempts = 0; + + while (placed < targetClusters && attempts < attemptLimit) { + attempts++; + const startX = Math.floor(rand() * mapSize); + const startZ = Math.floor(rand() * mapSize); + + if (isBlockedByMap(avoidMaps, startX, startZ)) continue; + if (field.stoneHeight[startX][startZ] > 0) continue; + + const clusterCells = buildCluster( + startX, + startZ, + 1 + Math.floor(rand() * profile.maxCellsPerCluster), + mapSize, + rand + ); + + let valid = true; + for (const cell of clusterCells) { + if ( + hasNearbyStone(field, cell.x, cell.z, profile.separationRadius, mapSize) || + isBlockedByMap(avoidMaps, cell.x, cell.z) + ) { + valid = false; + break; + } + } + + if (!valid) continue; + + // 地上部分:1~3层 + const aboveGround = profile.minAboveGround + + Math.floor(rand() * (profile.maxAboveGround - profile.minAboveGround + 1)); + + // 地下部分:1~3层 + const belowGround = profile.minBelowGround + + Math.floor(rand() * (profile.maxBelowGround - profile.minBelowGround + 1)); + + 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; + field.stoneVariant[cell.x][cell.z] = variant; + } + + 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 56901e4..44966cc 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -19,6 +19,7 @@ import { // 戈壁特征地形 import { + type DesertContext, createDesertContext, getGobiLayerType, getStoneLayerType, @@ -26,6 +27,8 @@ import { // STONE_PLATFORM_STRUCTS, // MESA_PLATFORM_STRUCTS, } from './desertFeatures'; +import { createMountainRockContext } from './mountainFeatures'; +import type { RockFieldContext } from './rockFeatures'; // 植被生成 (已移除旧系统) // import { @@ -95,6 +98,7 @@ export interface VoxelData { iz: number; heightScale: number; isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素 + isRock?: boolean; // 标记是否为石块体素(不可剔除) } export const MAP_SIZES = { @@ -115,6 +119,22 @@ export interface TerrainGenerationOptions { } // MagicaVoxel Vibrant Palette (More Saturation, Less Grey) +const createZeroMatrix = (size: number): number[][] => + Array.from({ length: size }, () => Array(size).fill(0)); + +const buildRockOnlyContext = ( + mapSize: number, + rockField: RockFieldContext +): DesertContext => ({ + gobiHeight: createZeroMatrix(mapSize), + gobiVariant: createZeroMatrix(mapSize), + gobiMinHeight: createZeroMatrix(mapSize), + stoneHeight: rockField.stoneHeight, + stoneDepth: rockField.stoneDepth, + stoneVariant: rockField.stoneVariant, + streamDepthMap: new Map(), +}); + export const generateTerrain = async ( mapSize: number, seed: string = 'default', @@ -161,10 +181,19 @@ export const generateTerrain = async ( // const forestClusterNoise = createNoise2D(() => Math.random()); - const desertContext = sceneConfig?.name === 'desert' + const isDesertScene = sceneConfig?.name === 'desert'; + + const desertContext = isDesertScene ? createDesertContext(mapSize, seededRandom) : null; - const isDesertScene = sceneConfig?.name === 'desert'; + const mountainRockContext = sceneConfig?.name === 'mountain' + ? createMountainRockContext(mapSize, seededRandom) + : null; + + // 获取石块上下文:沙漠用完整的 desertContext,山地用 mountainRockContext + const rockContext: RockFieldContext | null = isDesertScene + ? desertContext + : mountainRockContext; // 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成 let terrainType: string; @@ -200,22 +229,113 @@ export const generateTerrain = async ( // 开始生成:基础地形 reportProgress('basic', 0, '初始化地形生成'); + // ===== 预计算地形高度图(用于风化预处理)===== + const terrainHeightMap: number[][] = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + + if (rockContext) { + reportProgress('basic', 2, '计算地形高度图'); + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + let n, logicHeight; + + // 使用场景配置生成地形 + if (sceneConfig) { + if (terrainType === 'desert') { + logicHeight = Math.max(1, Math.floor(heightBase)); + } else { + const scale = heightRoughness; + n = noise2D(lx * scale, lz * scale); + n += 0.5 * noise2D(lx * scale * 2, lz * scale * 2); + n /= 1.5; + + logicHeight = Math.floor((n + 1) * sceneConfig.heightAmplitude * 0.5 + heightBase); + if (logicHeight < 0) logicHeight = 0; + } + } else { + // 旧逻辑(向后兼容) + if (terrainType === 'mountain') { + const scale = 0.06; + n = noise2D(lx * scale, lz * scale); + n += 0.7 * noise2D(lx * scale * 2, lz * scale * 2); + n /= 1.7; + + logicHeight = Math.floor((n + 1) * 5); + if (logicHeight < 2) logicHeight = 2; + } else if (terrainType === 'desert') { + const scale = 0.1; + n = noise2D(lx * scale + 300, lz * scale + 300); + n += 0.3 * noise2D(lx * scale * 3, lz * scale * 3); + n /= 1.3; + + logicHeight = Math.floor((n + 1) * 2.5); + if (logicHeight < 2) logicHeight = 2; + } else if (terrainType === 'canyon') { + const scale = 0.08; + n = noise2D(lx * scale + 600, lz * scale + 600); + n += 0.6 * noise2D(lx * scale * 2, lz * scale * 2); + n /= 1.6; + + const distToRiver = Math.abs(lx - mapSize / 2); + const riverWidth = mapSize * 0.08; + + if (distToRiver < riverWidth) { + const depthFactor = 1 - (distToRiver / riverWidth); + logicHeight = Math.max(0, Math.floor(2 - depthFactor * 2)); + } else { + const sideEffect = Math.sin(lz * 0.15) * 0.3; + logicHeight = Math.floor((n + sideEffect + 1) * 4.5); + } + } else { + const scale = 0.08; + n = noise2D(lx * scale + 100, lz * scale + 100); + n += 0.5 * noise2D(lx * scale * 2 + 200, lz * scale * 2 + 200); + n /= 1.5; + + logicHeight = Math.floor((n + 1) * 3.2); + if (logicHeight < 1) logicHeight = 1; + + const lakeCenterX = mapSize * 0.35; + const lakeCenterZ = mapSize * 0.4; + const distToLake = Math.sqrt( + Math.pow(lx - lakeCenterX, 2) + Math.pow(lz - lakeCenterZ, 2) + ); + const lakeRadius = mapSize * 0.12; + + if (distToLake < lakeRadius) { + const depthFactor = 1 - (distToLake / lakeRadius); + logicHeight = Math.max(0, Math.floor(1 - depthFactor * 0.5)); + } + } + } + + terrainHeightMap[lx][lz] = logicHeight; + } + } + } + // ===== 戈壁和巨石风化预处理 ===== - // 在沙漠场景中,预先计算风化效果 - let gobiWeatheringResult: GobiWeatheringResult | null = null; - if (isDesertScene && desertContext) { - reportProgress('basic', 5, '预处理戈壁和巨石风化效果'); + // 在需要巨石的场景中,预先计算风化效果 + let rockWeatheringResult: GobiWeatheringResult | null = null; + if (rockContext) { + const contextForWeathering = desertContext ?? buildRockOnlyContext(mapSize, rockContext); + reportProgress( + 'basic', + 5, + isDesertScene ? '预处理戈壁和巨石风化效果' : '预处理巨石风化效果' + ); await new Promise(resolve => setTimeout(resolve, 0)); - // 从options中获取参数,如果没有则使用默认值 - const gobiMaxIterations = options?.weatheringMaxIterations ?? 3; - const gobiHeightBias = options?.weatheringHeightBias ?? 0.5; - const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? 3; - const stoneHeightBias = options?.stoneWeatheringHeightBias ?? 0.5; - const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? 2; + // 从options中获取参数,如果没有则使用默认值 + const gobiMaxIterations = options?.weatheringMaxIterations ?? 3; + const gobiHeightBias = options?.weatheringHeightBias ?? 0.5; + // 山地场景:减少风化迭代次数和降低高度保护阈值,因为山地整体高度更高 + const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? (isDesertScene ? 3 : 2); + const stoneHeightBias = options?.stoneWeatheringHeightBias ?? (isDesertScene ? 0.8 : 0.8); + const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? (isDesertScene ? 2 : 2); - gobiWeatheringResult = preprocessGobiWeathering({ - desertContext, + rockWeatheringResult = preprocessGobiWeathering({ + desertContext: contextForWeathering, + terrainHeightMap, mapSize, MICRO_SCALE, gobiMaxIterations, @@ -226,7 +346,11 @@ export const generateTerrain = async ( MIN_WORLD_Y, }); - reportProgress('basic', 10, `风化预处理完成,移除 ${gobiWeatheringResult.removedVoxels.size} 个体素`); + reportProgress( + 'basic', + 10, + `风化预处理完成,移除 ${rockWeatheringResult.removedVoxels.size} 个体素` + ); await new Promise(resolve => setTimeout(resolve, 0)); } @@ -281,9 +405,17 @@ export const generateTerrain = async ( const gobiLogicHeight = isDesertScene && desertContext ? desertContext.gobiHeight[lx][lz] : 0; const gobiMicroHeight = gobiLogicHeight * MICRO_SCALE; const gobiVariantIdx = isDesertScene && desertContext ? desertContext.gobiVariant[lx][lz] : 0; - const stoneLogicHeight = isDesertScene && desertContext ? desertContext.stoneHeight[lx][lz] : 0; + + // 巨石信息:地上部分和地下部分 + const stoneLogicHeight = rockContext ? rockContext.stoneHeight[lx][lz] : 0; + const stoneLogicDepth = rockContext ? rockContext.stoneDepth[lx][lz] : 0; const stoneMicroHeight = stoneLogicHeight * MICRO_SCALE; - const stoneVariantIdx = isDesertScene && desertContext ? desertContext.stoneVariant[lx][lz] : 0; + const stoneMicroDepth = stoneLogicDepth * MICRO_SCALE; + const stoneVariantIdx = rockContext ? rockContext.stoneVariant[lx][lz] : 0; + + // 调试计数器:统计石块体素渲染情况(生产环境可移除) + let stoneVoxelsRendered = 0; + let stoneVoxelsSkippedByWeathering = 0; // Pre-fetch neighbor heights for Gobi smoothing (not used in current erosion algorithm) // Kept for potential future use @@ -457,27 +589,42 @@ export const generateTerrain = async ( // Use lower frequency for broader, gentler slopes const detailVal = detailNoise(ix * 0.08, iz * 0.08); + // 检测当前位置是否有石块(石块区域不应用 smoothing) + const hasRockColumn = stoneMicroHeight > 0 || stoneMicroDepth > 0; + + // 计算地表高度(基础地形 + 戈壁) let worldY = logicHeight * MICRO_SCALE; if (isDesertScene) { - worldY += stoneMicroHeight + gobiMicroHeight; + worldY += gobiMicroHeight; } - + + // 记录原始地表位置(用于石块范围判断) + const groundLevelY = worldY; + // SMOOTHING LOGIC: - // Only apply significant noise to Stone/Snow - // Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally) - if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') { - if (Math.abs(detailVal) > 0.85) { - worldY += Math.sign(detailVal); - } - } else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') { - worldY += Math.floor(detailVal * 2); - } else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') { - worldY = Math.floor(1.3 * MICRO_SCALE); - } else if (logicType === 'ash' || logicType === 'mud') { - if (Math.abs(detailVal) > 0.75) { - worldY += Math.sign(detailVal); + // 只对非石块区域应用 smoothing,保持石块稳固 + if (!hasRockColumn) { + // Only apply significant noise to Stone/Snow + // Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally) + if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') { + if (Math.abs(detailVal) > 0.85) { + worldY += Math.sign(detailVal); + } + } else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') { + worldY += Math.floor(detailVal * 2); + } else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') { + worldY = Math.floor(1.3 * MICRO_SCALE); + } else if (logicType === 'ash' || logicType === 'mud') { + if (Math.abs(detailVal) > 0.75) { + worldY += Math.sign(detailVal); + } } } + + // 如果有巨石,地表被石块地上部分抬高 + if (stoneMicroHeight > 0) { + worldY += stoneMicroHeight; + } // 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation let sandMoundHeight = 0; @@ -559,9 +706,16 @@ export const generateTerrain = async ( let type: VoxelType = logicType; const depth = surfaceY - y; - // 沙漠场景:优先处理戈壁和巨石,确保它们保持纯色 - const isInGobiRange = gobiMicroHeight > 0 && depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight; - const isInStoneRange = stoneMicroHeight > 0 && depth < stoneMicroHeight; + // 判断当前体素是否在石块范围内 + // 石块范围:从 groundLevelY - stoneMicroDepth 到 groundLevelY + stoneMicroHeight + const stoneTopY = groundLevelY + stoneMicroHeight; + const stoneBottomY = groundLevelY - stoneMicroDepth; + const isInStoneRange = (stoneMicroHeight > 0 || stoneMicroDepth > 0) && + y > stoneBottomY && y <= stoneTopY; + + // 戈壁场景:判断是否在戈壁范围 + const isInGobiRange = gobiMicroHeight > 0 && !isInStoneRange && + depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight; // 应用溪流蚀刻效果 if (shouldApplyStreamEtching({ streamDepthMicro, depth })) { @@ -575,21 +729,48 @@ export const generateTerrain = async ( } } else if (isInStoneRange) { // 巨石风化检查:如果该体素被风化移除,跳过生成 - if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) { + if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) { + stoneVoxelsSkippedByWeathering++; continue; // 跳过生成,风化效果 } - // 3. 确定基础颜色 - type = getStoneLayerType(depth, stoneVariantIdx); + stoneVoxelsRendered++; + // 计算石块内部的深度(从石块顶部算起) + const stoneDepthFromTop = stoneTopY - y; + // 确定基础颜色 + type = getStoneLayerType(stoneDepthFromTop, stoneVariantIdx); - // 4. 应用巨石颜色渗透效果 + // 应用巨石颜色渗透效果 type = applyStoneColorBlending({ currentType: type, - depth, + depth: stoneDepthFromTop, stoneVariantIdx, ix, iz, y }); + + // 标记为石块体素(不可剔除) + const color = varyColor(type, detailVal + y * 0.02, 0); + const rockVoxel: VoxelData = { + x: ix * VOXEL_SIZE, + y: y * VOXEL_SIZE, + z: iz * VOXEL_SIZE, + type, + color, + ix, + iy: y, + iz, + heightScale: 1, + isRock: true + }; + voxels.push(rockVoxel); + occupancy.add(keyOf(ix, y, iz)); + + if (y === surfaceY) { + topColumns.set(`${ix}|${iz}`, { index: voxels.length - 1, baseType: 'stone' }); + } + + continue; // 跳过后续的 addVoxel 调用 } else if (isInGobiRange) { const relativeDepth = depth - stoneMicroHeight; const microDepthFromBottom = gobiMicroHeight - relativeDepth - 1; @@ -604,7 +785,7 @@ export const generateTerrain = async ( } // 2. 风化检查:如果该体素被风化移除,跳过生成 - if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) { + if (shouldRemoveByWeathering(ix, y, iz, rockWeatheringResult)) { continue; // 跳过生成,风化效果 } @@ -830,6 +1011,9 @@ export const generateTerrain = async ( const culledVoxels = voxels.filter((voxel) => { // Always keep bedrock bottom layer to avoid gaps if (voxel.iy <= MIN_WORLD_Y + 1) return true; + + // 保留石块体素(不可剔除) + if (voxel.isRock) 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/vite.config.ts b/voxel-tactics-horizon/vite.config.ts index 8b0f57b..19e88f0 100644 --- a/voxel-tactics-horizon/vite.config.ts +++ b/voxel-tactics-horizon/vite.config.ts @@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + esbuild: { + // 保留 React 的调试信息 + keepNames: true, + }, })