From d7ab602fdc9a15497b030717a91b3a1feb1a6cfa Mon Sep 17 00:00:00 2001 From: Rocky Date: Fri, 28 Nov 2025 15:46:10 +0800 Subject: [PATCH] Update Map logic, Vegetation system and App entry point --- voxel-tactics-horizon/src/App.tsx | 4 +- .../src/features/Map/logic/newVegetation.ts | 836 ++++++++++++++++-- .../src/features/Map/logic/terrain.ts | 5 +- .../src/features/Map/store.ts | 2 +- 4 files changed, 776 insertions(+), 71 deletions(-) diff --git a/voxel-tactics-horizon/src/App.tsx b/voxel-tactics-horizon/src/App.tsx index 51789ce..609bd49 100644 --- a/voxel-tactics-horizon/src/App.tsx +++ b/voxel-tactics-horizon/src/App.tsx @@ -31,9 +31,9 @@ function GameScene({ skyPreset }: { skyPreset: SkyPreset }) { // 使用 ref 跟踪上一次的版本号 const prevVersionRef = useRef(TERRAIN_VERSION); - // 初始化地图 - 固定为沙漠场景方便调试 + // 初始化地图 - 默认生成山地场景 useEffect(() => { - generateMap('small', undefined, 'desert'); + generateMap('small', undefined, 'mountain'); // 临时注释:角色生成和战斗初始化 // setTimeout(() => { diff --git a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts index 4402cda..df05910 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts @@ -33,9 +33,11 @@ const C: Record = { // Temperate Forest (Balanced greens) leafDeep: '#3E6B42', leafMid: '#4E7A45', leafLight: '#6A9655', + leafBirch: '#85C265', // New: Brighter, fresh green for Birch pineD: '#385E4B', pineL: '#567D65', willow: '#7E9E64', poplar: '#68994D', woodOak: '#6B503B', woodBirch: '#E8E8E0', spot: '#3B3B3B', + pineCone: '#C29B61', acorn: '#D4B483', // Bright Tan/Light Brown for visibility dirt: '#755E49', grass: '#6FA648', stone: '#888C8D', // Ice/Tundra (Balanced greens) @@ -89,19 +91,69 @@ type AddBlockFn = (dx: number, dy: number, dz: number, color: string) => void; class VoxelBuilder { private addBlock: AddBlockFn; + private offsetX: number = 0; + private offsetY: number = 0; + private offsetZ: number = 0; + + // Temporary storage for face culling + private tempVoxels: Map = new Map(); constructor(addBlock: AddBlockFn) { this.addBlock = addBlock; } + setOffset(x: number, y: number, z: number) { + this.offsetX = x; + this.offsetY = y; + this.offsetZ = z; + this.tempVoxels.clear(); // Clear previous tree data + } + add(x: number, y: number, z: number, hexColor: string) { + const finalX = Math.round(x + this.offsetX); + const finalY = Math.round(y + this.offsetY); + const finalZ = Math.round(z + this.offsetZ); + let darkFactor = 1.0; // Simple AO / shading if (y < 1) darkFactor = 0.85; if (y < 0.5) darkFactor = 0.7; const finalColor = darkFactor < 1.0 ? darkenHex(hexColor, darkFactor) : hexColor; - this.addBlock(Math.round(x), Math.round(y), Math.round(z), finalColor); + + // Store in temp map instead of adding directly + this.tempVoxels.set(`${finalX},${finalY},${finalZ}`, finalColor); + } + + commit() { + // Process temp voxels and perform internal face culling + // Only add voxels that have at least one exposed face + for (const [key, color] of this.tempVoxels) { + const [x, y, z] = key.split(',').map(Number); + + // Check neighbors + let exposed = false; + const neighbors = [ + `${x+1},${y},${z}`, + `${x-1},${y},${z}`, + `${x},${y+1},${z}`, + `${x},${y-1},${z}`, + `${x},${y},${z+1}`, + `${x},${y},${z-1}` + ]; + + for (const neighborKey of neighbors) { + if (!this.tempVoxels.has(neighborKey)) { + exposed = true; + break; + } + } + + if (exposed) { + this.addBlock(x, y, z, color); + } + } + this.tempVoxels.clear(); } } @@ -235,77 +287,727 @@ const createSaguaro = (b: VoxelBuilder, ox: number, oz: number) => { }; // (Omitting some minor desert variations for brevity, keeping the impressive ones) -// --- TEMPERATE FOREST (Qiulin) --- +// --- TEMPERATE FOREST (Qiulin) - Enhanced for Mountainous Majesty --- const createLushOak = (builder: VoxelBuilder, ox: number, oz: number) => { - drawCylinder(builder, ox, 0, oz, 7, 2, C.woodOak); - drawSphere(builder, ox, 0, oz, 4, C.dirt, C.grass, 0.4); - drawCloudCluster(builder, ox, 8, oz, 4.5, 12, 2.5, 4.0, C.leafDeep, C.leafLight); -}; -const createFullBirch = (builder: VoxelBuilder, ox: number, oz: number) => { - let h = 14; - let lean = (Math.random() - 0.5) * 2; - for (let y = 0; y < h; y++) { - let dx = Math.round(lean * (y / h)); - builder.add(ox + dx, y, oz, Math.random() > 0.85 ? C.spot : C.woodBirch); - if (y > 1 && y < 4) { - builder.add(ox + dx + 1, y, oz, C.woodBirch); - builder.add(ox + dx, y, oz + 1, C.woodBirch); + // 1. Height & Scale + // Height: 24 - 36 (Wide and majestic, shorter than Poplar/Pine but massive) + const h = 24 + Math.random() * 12; + const sizeFactor = 0.9 + (h - 24) / 24.0; + + // 2. Colors (Oak Palette - Classic Deep Green) + const oakColors = [C.leafDeep, C.leafDeep, C.leafMid, C.leafLight]; + + // 3. Trunk (Thick and sturdy) + const trunkR = 2.2 * sizeFactor; + const trunkH = h * 0.45; // Trunk goes up about halfway + + // Roots (Spreading wide) + drawCylinder(builder, ox, -4, oz, 4, trunkR * 1.6, C.woodOak); + + // Main Trunk + for (let y = 0; y < trunkH; y++) { + const t = y / trunkH; + // Slight taper but mostly thick + const r = trunkR * (1 - t * 0.2); + const rCeil = Math.ceil(r); + for(let dx = -rCeil; dx <= rCeil; dx++) { + for(let dz = -rCeil; dz <= rCeil; dz++) { + if(dx*dx + dz*dz <= r*r) builder.add(ox+dx, y, oz+dz, C.woodOak); + } } } - drawSphere(builder, ox + Math.round(lean), h - 1, oz, 4.5, C.poplar, C.leafLight, 1.2, 0.2, 0.9); + + // 4. Large Branches (3-4 branches reaching out) + const numBranches = 3 + Math.floor(Math.random() * 2); + const branchStartH = trunkH * 0.6; // Branches start slightly below top of trunk + + for (let i = 0; i < numBranches; i++) { + const angle = (i / numBranches) * Math.PI * 2 + Math.random() * 0.5; + const length = (6 + Math.random() * 4) * sizeFactor; + const branchY = branchStartH + Math.random() * 3; + + // Draw Branch + const endX = ox + Math.cos(angle) * length; + const endZ = oz + Math.sin(angle) * length; + const endY = branchY + length * 0.5; // Branches go up + + // Simple line for branch + const steps = Math.ceil(length * 1.5); + for (let s = 0; s <= steps; s++) { + const t = s / steps; + const bx = Math.round(ox + (endX - ox) * t); + const by = Math.round(branchY + (endY - branchY) * t); + const bz = Math.round(oz + (endZ - oz) * t); + // Thicker near trunk + if (t < 0.4) builder.add(bx, by, bz, C.woodOak); + if (t < 0.2) { // Extra thick base + builder.add(bx+1, by, bz, C.woodOak); + builder.add(bx-1, by, bz, C.woodOak); + builder.add(bx, by, bz+1, C.woodOak); + builder.add(bx, by, bz-1, C.woodOak); + } + } + + // Branch Sub-Crown (Flattened ellipsoids) + drawNoisyEllipsoid( + builder, + Math.round(endX), Math.round(endY), Math.round(endZ), + 6.0 * sizeFactor, 4.5 * sizeFactor, 6.0 * sizeFactor, + oakColors, + 0, 0.3, true, 0.2, + { chance: 0.01, color: C.acorn } // Acorns on branches + ); + } + + // 5. Main Crown (Top center) + // Huge covering canopy + const topY = trunkH + 2; + drawNoisyEllipsoid( + builder, + ox, topY, oz, + 8.0 * sizeFactor, 7.0 * sizeFactor, 8.0 * sizeFactor, + oakColors, + 0, 0.35, true, 0.15, + { chance: 0.015, color: C.acorn } // Acorns on top + ); }; -const createDensePine = (builder: VoxelBuilder, ox: number, oz: number) => { - drawCylinder(builder, ox, 0, oz, 4, 1.5, C.woodOak); - let startY = 3; - let layers = 4; - for (let i = 0; i < layers; i++) { - let y = startY + i * 3.5; - let bottomR = 5.5 - i * 1.2; - let topR = 3.5 - i * 0.8; - let height = 4; - for (let ly = 0; ly < height; ly++) { - let r = bottomR + (topR - bottomR) * (ly / height); - for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { - for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { - if (x * x + z * z <= r * r) { - if (x * x + z * z < r * r * 0.8 || Math.random() > 0.3) - builder.add(ox + x, Math.round(y + ly), oz + z, C.pineD); + +// Procedural Shapes (Noise-based) +const drawNoisyEllipsoid = ( + builder: VoxelBuilder, + ox: number, + oy: number, + oz: number, + rx: number, + ry: number, + rz: number, + colors: string[], // Changed to array: [Shadow, Base, Highlight, Noise] + noiseScale: number = 0.2, + noiseStrength: number = 0.3, + isHollow: boolean = true, + weepingFactor: number = 0.0, // New: Weeping effect (0.0 - 1.0) + fruitSettings?: { chance: number, color: string } // New: Fruit generation +) => { + // Bounding box + const minX = -Math.ceil(rx); + const maxX = Math.ceil(rx); + const minY = -Math.ceil(ry * (1.0 + weepingFactor * 0.5)); // Extend downward + const maxY = Math.ceil(ry); + const minZ = -Math.ceil(rz); + const maxZ = Math.ceil(rz); + + const [cShadow, cBase, cHighlight, cNoise] = colors; + + for (let x = minX; x <= maxX; x++) { + for (let y = minY; y <= maxY; y++) { + for (let z = minZ; z <= maxZ; z++) { + // Normalized distance from center (0 to 1 at surface) + const dx = x / rx; + const dy = y / ry; + const dz = z / rz; + + // Basic Ellipsoid distance + let dist = Math.sqrt(dx*dx + dy*dy + dz*dz); + + // Weeping Deformation: + // If y is negative (bottom half), stretch it downwards based on x/z distance + // The further from center horizontally, the more it droops + if (y < 0 && weepingFactor > 0) { + const horizDist = Math.sqrt(dx*dx + dz*dz); // 0..1 + // Warp 'y' coordinate to simulate droop + // Effectively, we make 'dy' smaller (so dist is smaller) for lower voxels + const droop = horizDist * weepingFactor * 1.5; + const warpedDy = (y + Math.abs(y) * droop) / ry; + dist = Math.sqrt(dx*dx + warpedDy*warpedDy + dz*dz); + } + + if (dist > 1.2) continue; + + // Soft fuzzy edge probability + let probability = 1.0; + if (dist > 0.8) { + probability = 1.0 - (dist - 0.8) / 0.4; + } + + if (Math.random() < probability) { + // Hollow check + if (isHollow && dist < 0.6) continue; + + // Fruit Generation (Check first, overwrite leaf) + if (fruitSettings && Math.random() < fruitSettings.chance) { + // Fruits usually near edges or bottom + if (dist > 0.5 || y < 0) { + // Make Acorns larger (Cluster of 2) + builder.add(ox + x, oy + y, oz + z, fruitSettings.color); + // Add a neighbor for volume + if (Math.random() > 0.5) { + builder.add(ox + x + (Math.random()>0.5?1:-1), oy + y, oz + z, fruitSettings.color); + } else { + builder.add(ox + x, oy + y - 1, oz + z, fruitSettings.color); + } + continue; + } } + + // --- Improved Color Logic --- + // Goal: Rich 4-color mix everywhere, with slight vertical bias + const normalizedY = y / ry; // -1 (bottom) to 1 (top) + + // Base Weights (Global Richness) + // Ensure Deep Green (Shadow) appears everywhere, even at top + let pShadow = 0.20; + let pHighlight = 0.20; + let pNoise = 0.25; + // Base takes remaining ~35% + + // Apply Gradient Bias + // Top: More highlight, but keep some shadow + if (normalizedY > 0.3) { + pHighlight += 0.25 * normalizedY; // Up to 0.45 + pShadow -= 0.05 * normalizedY; // Down to 0.15 (Still present!) + } + // Bottom: More shadow + else if (normalizedY < -0.3) { + pShadow += 0.3 * Math.abs(normalizedY); // Up to 0.50 + pHighlight -= 0.1 * Math.abs(normalizedY); // Down to 0.10 + } + + const r = Math.random(); + let col = cBase; + + if (r < pShadow) col = cShadow; + else if (r < pShadow + pHighlight) col = cHighlight; + else if (r < pShadow + pHighlight + pNoise) col = cNoise; + else col = cBase; + + builder.add(ox + x, oy + y, oz + z, col); } } } } - drawCylinder(builder, ox, startY + layers * 3.5, oz, 3, 1, C.pineD); - builder.add(ox, startY + layers * 3.5 + 3, oz, C.pineL); }; -const createCurtainWillow = (builder: VoxelBuilder, ox: number, oz: number) => { - drawCylinder(builder, ox, 0, oz, 5, 1.5, C.woodOak); - drawSphere(builder, ox, 6, oz, 5, C.willow, C.leafLight, 0.7, 0.1, 0.95); - for (let i = 0; i < 20; i++) { - let ang = (i / 20) * Math.PI * 2; - let dist = 3.5 + Math.random(); - let sx = ox + Math.cos(ang) * dist; - let sz = oz + Math.sin(ang) * dist; - let len = 4 + Math.random() * 5; - for (let y = 0; y < len; y++) { - let px = Math.round(sx + (Math.random() - 0.5) * 0.5); - let pz = Math.round(sz + (Math.random() - 0.5) * 0.5); - let py = 6 - y; - if (py < 0) continue; - builder.add(px, py, pz, C.willow); - if (Math.random() > 0.6) builder.add(px, py, pz + 1, C.willow); + +const createProceduralBirch = (builder: VoxelBuilder, ox: number, oz: number) => { + // 1. Height-Width Correlation + // Adjusted: Nerfed to be the "middle layer" of the forest + const h = 20 + Math.random() * 10; // Height: 20 - 30 + + // Size Factor: Adjusted baseline for shorter trees + const sizeFactor = 0.8 + (h - 20) / 20.0; + + // Define Crown Dimensions (Fix ReferenceError) + const baseRy = 9.0 + Math.random() * 3.0; + const crownRy = baseRy * sizeFactor; + + const leanX = (Math.random() - 0.5) * 8; + const leanZ = (Math.random() - 0.5) * 8; + + // Root extension + for (let y = -5; y < 0; y++) { + const t = y / h; + const cx = Math.round(leanX * t * 0.5); + const cz = Math.round(leanZ * t * 0.5); + drawCylinder(builder, ox + cx, y, oz + cz, 1, 1.8 * sizeFactor, C.woodBirch); + } + + // Main trunk with Horizontal Striping + for (let y = 0; y < h - 4; y++) { + const t = y / h; + const cx = Math.round(leanX * t); + const cz = Math.round(leanZ * t); + // Tapering trunk - Thinned out + let r = (t < 0.3 ? 1.3 : (t < 0.7 ? 1.0 : 0.7)) * sizeFactor; + if (r < 0.6) r = 0.6; + + for(let dx = -Math.ceil(r); dx <= Math.ceil(r); dx++) { + for(let dz = -Math.ceil(r); dz <= Math.ceil(r); dz++) { + if(dx*dx + dz*dz <= r*r) { + // Birch bark texture: Horizontal Stripes (Lenticels) + let c = C.woodBirch; + // Use y-coordinate and noise to create horizontal bands + // Modulo with some noise + const bandNoise = Math.sin(y * 0.8 + dx * 0.5) + Math.cos(y * 0.9 + dz * 0.5); + // Occasional dark bands + if (Math.abs(bandNoise) > 1.5 && Math.random() > 0.6) { + c = C.spot; + } + // Base patches + if (y < 3 && Math.random() > 0.7) c = C.spot; + + builder.add(ox + cx + dx, y, oz + cz + dz, c); + } + } + } + } + + // --- MULTI-CLUSTER CROWN SYSTEM --- + + // Birch Colors: [Base, Highlight, Light, Poplar] -> Brighter mix + const birchColors = [C.leafMid, C.leafBirch, C.leafLight, C.poplar]; + const topX = Math.round(ox + leanX); + const topZ = Math.round(oz + leanZ); + + // 1. Top Main Cluster + // Lowered the main crown slightly (was h - 2) to feel more grounded + const crownY = h - crownRy * 0.3; + + drawNoisyEllipsoid( + builder, + topX, + crownY, + topZ, + 7.0 * sizeFactor, + 8.0 * sizeFactor, + 7.0 * sizeFactor, + birchColors, + 0, 0.4, true, + 0.3 // Slight weeping + ); + + // 2. Side Clusters (2-3) + const numSide = 2 + Math.floor(Math.random() * 2); // 2 or 3 + + for (let i = 0; i < numSide; i++) { + const angle = (i / numSide) * Math.PI * 2 + Math.random() * 0.5; + const dist = (4 + Math.random() * 3) * sizeFactor; + // Lowered side branches significantly: was 0.6-0.8, now 0.5-0.7 + // This makes the crown start lower and look fuller/less top-heavy + const sideH = h * (0.5 + Math.random() * 0.2); + + // Calculate side position + // Interpolate trunk position at this height + const t = sideH / h; + const trunkX = ox + leanX * t; + const trunkZ = oz + leanZ * t; + + const cx = Math.round(trunkX + Math.cos(angle) * dist); + const cz = Math.round(trunkZ + Math.sin(angle) * dist); + const cy = Math.round(sideH); + + // Draw Cluster + drawNoisyEllipsoid( + builder, + cx, cy, cz, + 5.5 * sizeFactor, + 6.5 * sizeFactor, + 5.5 * sizeFactor, + birchColors, + 0, 0.4, true, + 0.4 // More weeping for lower branches + ); + } +}; + +// New Helper: Draw Noisy Cone +const drawNoisyCone = ( + builder: VoxelBuilder, + ox: number, + yStart: number, + oz: number, + height: number, + bottomR: number, + colors: string[], + noiseStrength: number = 0.3, + fruitSettings?: { chance: number, color: string } +) => { + const [cShadow, cBase, cHighlight, cNoise] = colors; + + for (let y = 0; y < height; y++) { + // Normalized Height (0 at bottom, 1 at top) + const t = y / height; + + // Radius tapers linearly from bottomR to 0.5 + let currentR = bottomR * (1 - t * 0.8); // Don't go to 0, keep top slightly thick + if (currentR < 0.5) currentR = 0.5; + + // Draw circle at this height + const rCeil = Math.ceil(currentR + 1); + for (let x = -rCeil; x <= rCeil; x++) { + for (let z = -rCeil; z <= rCeil; z++) { + const distSq = x*x + z*z; + + // Add noise to the edge radius + // Noise based on angle and height + const angle = Math.atan2(z, x); + const noise = Math.sin(angle * 5 + y * 0.5) * noiseStrength * currentR; + + if (distSq <= (currentR + noise) ** 2) { + // Fruit Generation + if (fruitSettings && Math.random() < fruitSettings.chance) { + // Pine cones hang near edges or bottom of layer + if (distSq > (currentR * 0.6)**2 || y < height * 0.3) { + // Make Pine Cones longer (Hang down 2 blocks) + builder.add(ox + x, Math.round(yStart + y), oz + z, fruitSettings.color); + builder.add(ox + x, Math.round(yStart + y) - 1, oz + z, fruitSettings.color); + continue; + } + } + + // Color Logic + let col = cBase; + // Bottom/Inner = Darker + if (y < height * 0.2 || distSq < (currentR * 0.5)**2) { + col = Math.random() > 0.6 ? cShadow : cBase; + } + // Top/Outer = Lighter + else if (y > height * 0.8) { + col = Math.random() > 0.7 ? cHighlight : cBase; + } + // Random noise/snow + if (Math.random() > 0.95) col = cNoise; // Can act as snow or texture + + builder.add(ox + x, Math.round(yStart + y), oz + z, col); + } + } } } }; + +const createDensePine = (builder: VoxelBuilder, ox: number, oz: number) => { + // 1. Height & Scale + // Buffed: Pines are now the giants (36 - 54) + const h = 36 + Math.random() * 18; + + // 2. Colors (Deep Coniferous Palette) + // Shadow: PineD, Base: PineD, Highlight: PineL, Noise: PineL (Snow tips?) + const pineColors = [C.gDarkest, C.pineD, C.pineL, C.pineL]; + + // 3. Root & Trunk + // Roots + drawCylinder(builder, ox, -4, oz, 4, 2.2, C.woodOak); + + // Main Trunk (Visible at bottom) + // The trunk goes all the way up but gets thinner + const trunkH = h - 2; + + // Draw Trunk - Thicker base for taller trees + // Reduced thickness: 2.5 -> 1.6 base + const baseR = 1.6 + (h - 36) / 25.0; + + for(let y = 0; y < trunkH; y++) { + const t = y / trunkH; + const r = baseR * (1 - t * 0.5); // Tapering (gentler taper since base is thinner) + for(let dx = -Math.ceil(r); dx <= Math.ceil(r); dx++) { + for(let dz = -Math.ceil(r); dz <= Math.ceil(r); dz++) { + if(dx*dx + dz*dz <= r*r) builder.add(ox+dx, y, oz+dz, C.woodOak); + } + } + } + + // 4. Tiered Conical Foliage + // Number of layers depends on height + let numLayers = 3; + if (h > 46) numLayers = 4; // Giants get 4 layers + + // Foliage covers top 70% of the tree + const foliageStartH = h * 0.3; + const foliageTopH = h; + const totalFoliageH = foliageTopH - foliageStartH; + + const layerHeight = (totalFoliageH / numLayers) * 1.2; // Overlap layers + + for (let i = 0; i < numLayers; i++) { + // i=0 is bottom layer + const t = i / (numLayers - 1 || 1); // 0 to 1 + + // Position + // Bottom layer starts at foliageStartH + // Top layer ends near top + const startY = foliageStartH + i * (totalFoliageH / numLayers * 0.85); + + // Radius + // Bottom layer is widest (Buffed radius for giants: 8.0 -> 10.0+) + const maxR = 8.0 + (h - 36) / 5.0; + const bottomR = maxR * (1 - t * 0.6); + + drawNoisyCone( + builder, + ox, + startY, + oz, + layerHeight, + bottomR, + pineColors, + 0.4, // Noise strength + { chance: 0.02, color: C.pineCone } // Pine Cones + ); + } + + // Top Spire + builder.add(ox, h, oz, C.pineL); + builder.add(ox, h+1, oz, C.pineL); +}; + +// Helper: Draw a twisted limb (Trunk or Branch) +// Returns the end position for attaching foliage or sub-branches +const drawTwistedLimb = ( + builder: VoxelBuilder, + sx: number, sy: number, sz: number, + length: number, + startR: number, + endR: number, + woodCol: string, + direction: {x: number, y: number, z: number}, // General direction bias + waviness: number = 1.0 +): {x: number, y: number, z: number} => { + let cx = sx; + let cy = sy; + let cz = sz; + let cr = startR; + const rStep = (startR - endR) / length; + + for (let i = 0; i < length; i++) { + // Draw slice + const rCeil = Math.ceil(cr); + for(let dx = -rCeil; dx <= rCeil; dx++) { + for(let dy = -rCeil; dy <= rCeil; dy++) { + for(let dz = -rCeil; dz <= rCeil; dz++) { + if(dx*dx + dy*dy + dz*dz <= cr*cr) { + builder.add(Math.round(cx+dx), Math.round(cy+dy), Math.round(cz+dz), woodCol); + } + } + } + } + + // Move walker + // 1. Follow general direction + cx += direction.x; + cy += direction.y; + cz += direction.z; + + // 2. Add Waviness (Sine/Cos noise based on step i) + cx += Math.sin(i * 0.3) * waviness; + cz += Math.cos(i * 0.3) * waviness; + + // 3. Taper + cr -= rStep; + if (cr < 0.5) cr = 0.5; + } + return {x: cx, y: cy, z: cz}; +}; + +const createScotsPine = (builder: VoxelBuilder, ox: number, oz: number) => { + // 1. Height & Scale + const h = 20 + Math.random() * 10; // Not too tall, but wide + const sizeFactor = 0.9 + (h - 20) / 20.0; + + // 2. Colors + const pineColors = [C.gDarkest, C.pineD, C.pineL, C.pineL]; + const woodColor = Math.random() > 0.5 ? C.woodOak : C.woodD; + + // 3. Root Base + drawCylinder(builder, ox, -3, oz, 3, 2.8 * sizeFactor, woodColor); + + // 4. Main Trunk (S-Shape / Crooked) + // Split trunk into 2-3 segments to change direction drastically + let cursor = {x: ox, y: 0, z: oz}; + let currentR = 2.2 * sizeFactor; + + // Segment 1: Lean one way + const leanAng = Math.random() * Math.PI * 2; + cursor = drawTwistedLimb( + builder, cursor.x, cursor.y, cursor.z, + h * 0.4, currentR, currentR * 0.8, woodColor, + {x: Math.cos(leanAng)*0.4, y: 0.9, z: Math.sin(leanAng)*0.4}, + 0.5 + ); + currentR *= 0.8; + + // Segment 2: Curve back or sideways (The "Elbow") + const elbowAng = leanAng + Math.PI * (0.5 + Math.random()); // Turn 90-270 deg + const seg2Len = h * 0.4; + + // --- Generate Lower Branches at Elbow --- + const numLowBranches = 1 + Math.floor(Math.random() * 2); + for(let i=0; i { + // Very Flat pads (Plate-like) + const ry = radius * 0.4; + const rx = radius; + const rz = radius; + + drawNoisyEllipsoid( + builder, + ox, oy, oz, + rx, ry, rz, + colors, + 0, 0.5, // High noise + true, + 0.0, // No weeping, holds shape + { chance: 0.04, color: C.pineCone } + ); + // Add a second smaller pad slightly above for volume + if (Math.random() > 0.5) { + drawNoisyEllipsoid( + builder, + ox, oy + ry, oz, + rx * 0.7, ry * 0.8, rz * 0.7, + colors, + 0, 0.5, true, 0.0 + ); + } +}; + +const createCurtainWillow = (builder: VoxelBuilder, ox: number, oz: number) => { + // Giant Willow + // Root extension + drawCylinder(builder, ox, -5, oz, 5, 4, C.woodOak); + + drawCylinder(builder, ox, 0, oz, 8, 3.5, C.woodOak); // Thicker trunk + + // Huge crown + drawSphere(builder, ox, 10, oz, 10, C.willow, C.leafLight, 0.6, 0.2, 0.9); + + // Many more vines, longer + for (let i = 0; i < 40; i++) { + let ang = (i / 40) * Math.PI * 2 + Math.random()*0.5; + let dist = 5 + Math.random() * 4; + let sx = ox + Math.cos(ang) * dist; + let sz = oz + Math.sin(ang) * dist; + let len = 8 + Math.random() * 10; // Longer vines + + for (let y = 0; y < len; y++) { + let px = Math.round(sx + (Math.random() - 0.5) * 0.8); + let pz = Math.round(sz + (Math.random() - 0.5) * 0.8); + let py = 10 - y; + if (py < 1) continue; + builder.add(px, py, pz, C.willow); + if (Math.random() > 0.6) builder.add(px, py, pz + 1, Math.random() > 0.5 ? C.willow : C.leafMid); + } + } +}; + const createFatPoplar = (builder: VoxelBuilder, ox: number, oz: number) => { - drawCylinder(builder, ox, 0, oz, 3, 1, C.woodOak); - let h = 16; - for (let y = 0; y < h; y++) { - let r = 2.2 * Math.sin((y / h) * Math.PI); - for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { - for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { - if (x * x + z * z <= r * r) { - builder.add(ox + x, y + 2, oz + z, C.poplar); + // 1. Height: 30 - 46 (Middle-Upper layer, taller than Birch, overlapping lower Pines) + const h = 30 + Math.random() * 16; + const sizeFactor = 0.8 + (h - 30) / 20.0; + + // 2. Colors (Poplar Palette) + const poplarColors = [C.leafDeep, C.poplar, C.leafLight, C.leafMid]; + const [cShadow, cBase, cHighlight, cNoise] = poplarColors; + + // 3. Trunk + // Exposed height: 3-5 blocks (Clear ground clearance) + const trunkH = 3 + Math.random() * 2; + const crownH = h - trunkH; + + // Thickness: 1.5 base, tapering slightly (Sturdy but slender) + const trunkBaseR = 1.5 * sizeFactor; + + // Roots + drawCylinder(builder, ox, -4, oz, 4, trunkBaseR * 1.4, C.woodOak); + + // Exposed Trunk + for (let y = 0; y < trunkH + 2; y++) { // +2 extends into crown + const t = y / (trunkH + 2); + const r = trunkBaseR * (1 - t * 0.3); // Gentle taper + const rCeil = Math.ceil(r); + for(let dx = -rCeil; dx <= rCeil; dx++) { + for(let dz = -rCeil; dz <= rCeil; dz++) { + if(dx*dx + dz*dz <= r*r) builder.add(ox+dx, y, oz+dz, C.woodOak); + } + } + } + + // 4. Spindle Crown (Custom construction for Fusiform shape) + // Max Radius: ~4.5 - 6.5 (Ratio ~ 1:7 of height, slender) + const maxR = (h / 7.0) * sizeFactor; + const startY = trunkH; + + for (let y = 0; y < crownH; y++) { + const t = y / crownH; // 0 to 1 + + // Spindle Shape Formula: + // 1. Math.pow(t, 0.85): Shifts the peak slightly downwards (approx 40% height) for stability + // 2. Math.sin(... * PI): Creates the curve + // 3. Math.pow(..., 0.9): Slightly sharpens the curve, avoiding the "balloon" look + let rBase = maxR * Math.pow(Math.sin(Math.pow(t, 0.85) * Math.PI), 0.9); + + // Taper tip aggressively to form a point + if (t > 0.9) rBase *= (1 - t) * 10.0; + if (rBase < 0.1) continue; + + const rCeil = Math.ceil(rBase + 1.5); // Search radius + + for (let x = -rCeil; x <= rCeil; x++) { + for (let z = -rCeil; z <= rCeil; z++) { + const distSq = x*x + z*z; + + // Noise Logic + // Vertical streaks for Poplar characteristic + const angle = Math.atan2(z, x); + // High frequency angle noise makes it look like vertical branches + const noise = Math.sin(y * 0.6) * 0.2 + Math.cos(angle * 5) * 0.3; + + const effectiveR = rBase * (1 + noise * 0.4); + + if (distSq <= effectiveR * effectiveR) { + // Color Logic (Vertical Gradient + Volume) + const normalizedY = y / crownH; // 0 to 1 + + let pShadow = 0.25; + let pHighlight = 0.15; + let pNoise = 0.2; + + // Vertical Gradient + if (normalizedY > 0.7) { + pHighlight += 0.25 * (normalizedY - 0.7); // Top is lighter + pShadow -= 0.1; + } else if (normalizedY < 0.2) { + pShadow += 0.3 * (0.2 - normalizedY); // Bottom undercut is darker + } + + const rnd = Math.random(); + let col = cBase; + if (rnd < pShadow) col = cShadow; + else if (rnd < pShadow + pHighlight) col = cHighlight; + else if (rnd < pShadow + pHighlight + pNoise) col = cNoise; + + builder.add(ox + x, Math.round(startY + y), oz + z, col); } } } @@ -540,7 +1242,7 @@ const createBanana = (builder: VoxelBuilder, ox: number, oz: number) => { // ========================================= const DESERT_GENERATORS = [createJointedClassic, createJointedCandelabra, createSaguaro]; -const FOREST_GENERATORS = [createLushOak, createFullBirch, createDensePine, createCurtainWillow, createFatPoplar]; +const FOREST_GENERATORS = [createLushOak, createProceduralBirch, createDensePine, createFatPoplar, createScotsPine]; const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom]; const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch]; const ICE_GENERATORS = [createCoralTree, createIceSpike]; @@ -661,15 +1363,15 @@ export const generateVegetation = async ( if (Math.random() < density) { const gen = generators[Math.floor(Math.random() * generators.length)]; - // Inject offset into builder - const originalAdd = builder['addBlock']; - builder['addBlock'] = (dx, dy, dz, color) => { - addVoxel(microX + dx, surfaceY + dy, microZ + dz, color); - }; + // 使用内置的 offset 功能,避免修改方法带来的潜在栈溢出风险 + builder.setOffset(microX, surfaceY, microZ); - gen(builder, 0, 0); - - builder['addBlock'] = originalAdd; + try { + gen(builder, 0, 0); + builder.commit(); // Commit and cull hidden voxels + } catch (e) { + console.warn('Tree generation failed at', lx, lz, e); + } } } } diff --git a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts index 518b907..56901e4 100644 --- a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -876,7 +876,10 @@ export const generateTerrain = async ( const newPlantVoxels = await generateVegetation(vegContext); // 关键修复:将植被添加到 culledVoxels (最终返回的数组),而不是 voxels (原始数组) // 植被体素不需要再次进行剔除检查,直接作为表面装饰添加 - culledVoxels.push(...newPlantVoxels); + // 使用 for 循环逐个添加,避免 push(...arr) 导致的栈溢出 (RangeError: Maximum call stack size exceeded) + for (const plantVoxel of newPlantVoxels) { + culledVoxels.push(plantVoxel); + } // Rebuild topColumns map after culling using coordinates const topColumnsAfterCull = new Map(); diff --git a/voxel-tactics-horizon/src/features/Map/store.ts b/voxel-tactics-horizon/src/features/Map/store.ts index 5445d87..30cc226 100644 --- a/voxel-tactics-horizon/src/features/Map/store.ts +++ b/voxel-tactics-horizon/src/features/Map/store.ts @@ -26,7 +26,7 @@ export const useMapStore = create((set, get) => ({ voxels: [], logicalHeightMap: {}, terrainVersion: 0, - currentScene: 'desert', // 默认场景:沙漠戈壁(固定用于调试) + currentScene: 'mountain', // 默认场景:山地 isGenerating: false, generationProgress: null, // 戈壁风化参数默认值