Update Map logic, Vegetation system and App entry point

This commit is contained in:
2025-11-28 15:46:10 +08:00
parent b403672264
commit d7ab602fdc
4 changed files with 776 additions and 71 deletions

View File

@@ -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(() => {

View File

@@ -33,9 +33,11 @@ const C: Record<string, string> = {
// 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<string, string> = 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<numLowBranches; i++) {
const bAng = Math.random() * Math.PI * 2;
const bLen = (6 + Math.random() * 5) * sizeFactor;
// Horizontal or slightly down
const bEnd = drawTwistedLimb(
builder, cursor.x, cursor.y, cursor.z,
bLen, currentR * 0.8, currentR * 0.4, woodColor,
{x: Math.cos(bAng), y: 0.1, z: Math.sin(bAng)}, // Flat
0.2
);
// Foliage pads on branches
createScotsCloud(builder, Math.round(bEnd.x), Math.round(bEnd.y), Math.round(bEnd.z), 5.0 * sizeFactor, pineColors);
}
// Continue Trunk
cursor = drawTwistedLimb(
builder, cursor.x, cursor.y, cursor.z,
seg2Len, currentR, currentR * 0.7, woodColor,
{x: Math.cos(elbowAng)*0.3, y: 0.9, z: Math.sin(elbowAng)*0.3},
0.5
);
currentR *= 0.7;
// Segment 3: Top crown support
const seg3Len = h * 0.3;
// --- Generate Upper Branches ---
const numHighBranches = 2 + Math.floor(Math.random() * 2);
for(let i=0; i<numHighBranches; i++) {
const bAng = Math.random() * Math.PI * 2;
const bLen = (4 + Math.random() * 4) * sizeFactor;
// Upward reaching
const bEnd = drawTwistedLimb(
builder, cursor.x, cursor.y, cursor.z,
bLen, currentR * 0.8, currentR * 0.4, woodColor,
{x: Math.cos(bAng)*0.8, y: 0.6, z: Math.sin(bAng)*0.8},
0.2
);
createScotsCloud(builder, Math.round(bEnd.x), Math.round(bEnd.y), Math.round(bEnd.z), 4.5 * sizeFactor, pineColors);
}
// Top Crown
createScotsCloud(builder, Math.round(cursor.x), Math.round(cursor.y + seg3Len), Math.round(cursor.z), 6.0 * sizeFactor, pineColors);
};
// Helper for Cloud-like Foliage (Flattened Noisy Ellipsoid)
const createScotsCloud = (builder: VoxelBuilder, ox: number, oy: number, oz: number, radius: number, colors: string[]) => {
// 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);
}
}
}
}

View File

@@ -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<string, { voxel: VoxelData; baseType: VoxelType }>();

View File

@@ -26,7 +26,7 @@ export const useMapStore = create<MapState>((set, get) => ({
voxels: [],
logicalHeightMap: {},
terrainVersion: 0,
currentScene: 'desert', // 默认场景:沙漠戈壁(固定用于调试)
currentScene: 'mountain', // 默认场景:山地
isGenerating: false,
generationProgress: null,
// 戈壁风化参数默认值