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,
+ },
})