@@ -153,6 +153,223 @@ export function createPond(scene, gradientMap) { |
| 153 | 153 | |
| 154 | 154 | group.add(fenceGroup) |
| 155 | 155 | |
| 156 | + // ============================================ |
| 157 | + // DISTANT SCENERY - Mountains and Village |
| 158 | + // ============================================ |
| 159 | + |
| 160 | + const sceneDistance = 12 // How far away the scenery is |
| 161 | + |
| 162 | + // Mountain range materials |
| 163 | + const mountainMaterial = new THREE.MeshToonMaterial({ |
| 164 | + color: 0x6b8e7a, // Muted green-grey |
| 165 | + gradientMap: gradientMap |
| 166 | + }) |
| 167 | + const mountainSnowMaterial = new THREE.MeshToonMaterial({ |
| 168 | + color: 0xe8e8f0, // Snow white with slight blue |
| 169 | + gradientMap: gradientMap |
| 170 | + }) |
| 171 | + const mountainDarkMaterial = new THREE.MeshToonMaterial({ |
| 172 | + color: 0x4a6b5a, // Darker mountain |
| 173 | + gradientMap: gradientMap |
| 174 | + }) |
| 175 | + |
| 176 | + // Create mountain range (back-left) |
| 177 | + const mountains = new THREE.Group() |
| 178 | + |
| 179 | + // Large back mountain |
| 180 | + const bigMountainGeom = new THREE.ConeGeometry(3, 5, 6) |
| 181 | + const bigMountain = new THREE.Mesh(bigMountainGeom, mountainMaterial) |
| 182 | + bigMountain.position.set(-sceneDistance, 2, -sceneDistance * 0.8) |
| 183 | + bigMountain.rotation.y = 0.3 |
| 184 | + mountains.add(bigMountain) |
| 185 | + |
| 186 | + // Snow cap for big mountain |
| 187 | + const snowCapGeom = new THREE.ConeGeometry(1.2, 1.5, 6) |
| 188 | + const snowCap = new THREE.Mesh(snowCapGeom, mountainSnowMaterial) |
| 189 | + snowCap.position.set(-sceneDistance, 4.2, -sceneDistance * 0.8) |
| 190 | + snowCap.rotation.y = 0.3 |
| 191 | + mountains.add(snowCap) |
| 192 | + |
| 193 | + // Medium mountain |
| 194 | + const medMountainGeom = new THREE.ConeGeometry(2.2, 3.5, 5) |
| 195 | + const medMountain = new THREE.Mesh(medMountainGeom, mountainDarkMaterial) |
| 196 | + medMountain.position.set(-sceneDistance * 0.7, 1.5, -sceneDistance) |
| 197 | + medMountain.rotation.y = -0.2 |
| 198 | + mountains.add(medMountain) |
| 199 | + |
| 200 | + // Small mountain |
| 201 | + const smallMountainGeom = new THREE.ConeGeometry(1.8, 2.8, 5) |
| 202 | + const smallMountain = new THREE.Mesh(smallMountainGeom, mountainMaterial) |
| 203 | + smallMountain.position.set(-sceneDistance * 1.1, 1.2, -sceneDistance * 0.5) |
| 204 | + mountains.add(smallMountain) |
| 205 | + |
| 206 | + // Another range on the right side (further back) |
| 207 | + const farMountainGeom = new THREE.ConeGeometry(2.5, 4, 5) |
| 208 | + const farMountain = new THREE.Mesh(farMountainGeom, mountainDarkMaterial) |
| 209 | + farMountain.position.set(sceneDistance * 0.5, 1.8, -sceneDistance * 1.2) |
| 210 | + mountains.add(farMountain) |
| 211 | + |
| 212 | + const farSnowGeom = new THREE.ConeGeometry(0.9, 1.2, 5) |
| 213 | + const farSnow = new THREE.Mesh(farSnowGeom, mountainSnowMaterial) |
| 214 | + farSnow.position.set(sceneDistance * 0.5, 3.6, -sceneDistance * 1.2) |
| 215 | + mountains.add(farSnow) |
| 216 | + |
| 217 | + group.add(mountains) |
| 218 | + |
| 219 | + // ============================================ |
| 220 | + // VILLAGE |
| 221 | + // ============================================ |
| 222 | + |
| 223 | + const village = new THREE.Group() |
| 224 | + const villageX = sceneDistance * 0.8 |
| 225 | + const villageZ = -sceneDistance * 0.4 |
| 226 | + |
| 227 | + // House materials |
| 228 | + const houseMaterial = new THREE.MeshToonMaterial({ |
| 229 | + color: 0xd4a574, // Warm beige/tan |
| 230 | + gradientMap: gradientMap |
| 231 | + }) |
| 232 | + const roofMaterial = new THREE.MeshToonMaterial({ |
| 233 | + color: 0x8b4513, // Brown roof |
| 234 | + gradientMap: gradientMap |
| 235 | + }) |
| 236 | + const roofRedMaterial = new THREE.MeshToonMaterial({ |
| 237 | + color: 0xb85450, // Red roof |
| 238 | + gradientMap: gradientMap |
| 239 | + }) |
| 240 | + const windowMaterial = new THREE.MeshToonMaterial({ |
| 241 | + color: 0x87ceeb, // Light blue windows |
| 242 | + gradientMap: gradientMap |
| 243 | + }) |
| 244 | + |
| 245 | + // Helper to create a simple house |
| 246 | + function createHouse(x, z, scale, roofMat) { |
| 247 | + const houseGroup = new THREE.Group() |
| 248 | + |
| 249 | + // House body |
| 250 | + const bodyGeom = new THREE.BoxGeometry(0.8, 0.6, 0.6) |
| 251 | + const body = new THREE.Mesh(bodyGeom, houseMaterial) |
| 252 | + body.position.y = 0.3 |
| 253 | + houseGroup.add(body) |
| 254 | + |
| 255 | + // Roof |
| 256 | + const roofGeom = new THREE.ConeGeometry(0.55, 0.5, 4) |
| 257 | + const roof = new THREE.Mesh(roofGeom, roofMat) |
| 258 | + roof.position.y = 0.75 |
| 259 | + roof.rotation.y = Math.PI / 4 |
| 260 | + houseGroup.add(roof) |
| 261 | + |
| 262 | + // Window |
| 263 | + const windowGeom = new THREE.PlaneGeometry(0.15, 0.15) |
| 264 | + const windowMesh = new THREE.Mesh(windowGeom, windowMaterial) |
| 265 | + windowMesh.position.set(0.401, 0.35, 0) |
| 266 | + houseGroup.add(windowMesh) |
| 267 | + |
| 268 | + houseGroup.position.set(x, 0, z) |
| 269 | + houseGroup.scale.setScalar(scale) |
| 270 | + houseGroup.rotation.y = Math.random() * 0.5 - 0.25 |
| 271 | + |
| 272 | + return houseGroup |
| 273 | + } |
| 274 | + |
| 275 | + // Create village houses |
| 276 | + const house1 = createHouse(villageX, villageZ, 1.2, roofMaterial) |
| 277 | + village.add(house1) |
| 278 | + |
| 279 | + const house2 = createHouse(villageX + 1.5, villageZ + 0.8, 0.9, roofRedMaterial) |
| 280 | + village.add(house2) |
| 281 | + |
| 282 | + const house3 = createHouse(villageX + 0.5, villageZ + 1.5, 1.0, roofMaterial) |
| 283 | + village.add(house3) |
| 284 | + |
| 285 | + const house4 = createHouse(villageX - 0.8, villageZ + 0.6, 0.8, roofRedMaterial) |
| 286 | + village.add(house4) |
| 287 | + |
| 288 | + // Chimney on main house |
| 289 | + const chimneyGeom = new THREE.BoxGeometry(0.15, 0.4, 0.15) |
| 290 | + const chimney = new THREE.Mesh(chimneyGeom, new THREE.MeshToonMaterial({ |
| 291 | + color: 0x8b7355, |
| 292 | + gradientMap: gradientMap |
| 293 | + })) |
| 294 | + chimney.position.set(villageX + 0.2, 1.1, villageZ + 0.1) |
| 295 | + village.add(chimney) |
| 296 | + |
| 297 | + // Smoke particles |
| 298 | + const smokeParticles = [] |
| 299 | + const smokeMaterial = new THREE.MeshBasicMaterial({ |
| 300 | + color: 0xcccccc, |
| 301 | + transparent: true, |
| 302 | + opacity: 0.6 |
| 303 | + }) |
| 304 | + |
| 305 | + function createSmokeParticle() { |
| 306 | + const size = 0.1 + Math.random() * 0.1 |
| 307 | + const smokeGeom = new THREE.SphereGeometry(size, 6, 4) |
| 308 | + const smoke = new THREE.Mesh(smokeGeom, smokeMaterial.clone()) |
| 309 | + smoke.position.set( |
| 310 | + villageX + 0.2 + (Math.random() - 0.5) * 0.1, |
| 311 | + 1.3, |
| 312 | + villageZ + 0.1 + (Math.random() - 0.5) * 0.1 |
| 313 | + ) |
| 314 | + village.add(smoke) |
| 315 | + smokeParticles.push({ |
| 316 | + mesh: smoke, |
| 317 | + age: 0, |
| 318 | + maxAge: 3 + Math.random() * 2, |
| 319 | + driftX: (Math.random() - 0.5) * 0.3, |
| 320 | + driftZ: (Math.random() - 0.5) * 0.3, |
| 321 | + riseSpeed: 0.3 + Math.random() * 0.2 |
| 322 | + }) |
| 323 | + } |
| 324 | + |
| 325 | + // Initial smoke |
| 326 | + for (let i = 0; i < 5; i++) { |
| 327 | + createSmokeParticle() |
| 328 | + smokeParticles[i].age = Math.random() * 2 // Stagger initial ages |
| 329 | + } |
| 330 | + |
| 331 | + group.add(village) |
| 332 | + |
| 333 | + // Small trees near village |
| 334 | + const treeMaterial = new THREE.MeshToonMaterial({ |
| 335 | + color: 0x2d5a3d, |
| 336 | + gradientMap: gradientMap |
| 337 | + }) |
| 338 | + const trunkMaterial = new THREE.MeshToonMaterial({ |
| 339 | + color: 0x5c4033, |
| 340 | + gradientMap: gradientMap |
| 341 | + }) |
| 342 | + |
| 343 | + for (let i = 0; i < 6; i++) { |
| 344 | + const treeGroup = new THREE.Group() |
| 345 | + |
| 346 | + // Trunk |
| 347 | + const trunkGeom = new THREE.CylinderGeometry(0.08, 0.12, 0.5, 6) |
| 348 | + const trunk = new THREE.Mesh(trunkGeom, trunkMaterial) |
| 349 | + trunk.position.y = 0.25 |
| 350 | + treeGroup.add(trunk) |
| 351 | + |
| 352 | + // Foliage (stacked cones) |
| 353 | + const foliage1 = new THREE.Mesh(new THREE.ConeGeometry(0.4, 0.6, 6), treeMaterial) |
| 354 | + foliage1.position.y = 0.7 |
| 355 | + treeGroup.add(foliage1) |
| 356 | + |
| 357 | + const foliage2 = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.5, 6), treeMaterial) |
| 358 | + foliage2.position.y = 1.1 |
| 359 | + treeGroup.add(foliage2) |
| 360 | + |
| 361 | + const angle = (i / 6) * Math.PI * 0.8 - 0.4 |
| 362 | + const dist = sceneDistance * 0.6 + Math.random() * 2 |
| 363 | + treeGroup.position.set( |
| 364 | + Math.cos(angle) * dist + villageX * 0.3, |
| 365 | + 0, |
| 366 | + Math.sin(angle) * dist + villageZ * 0.3 |
| 367 | + ) |
| 368 | + treeGroup.scale.setScalar(0.6 + Math.random() * 0.4) |
| 369 | + |
| 370 | + group.add(treeGroup) |
| 371 | + } |
| 372 | + |
| 156 | 373 | // Ripple system |
| 157 | 374 | const ripples = [] |
| 158 | 375 | const rippleGeom = new THREE.RingGeometry(0.1, 0.15, 16) |
@@ -176,6 +393,8 @@ export function createPond(scene, gradientMap) { |
| 176 | 393 | }) |
| 177 | 394 | } |
| 178 | 395 | |
| 396 | + let smokeSpawnTimer = 0 |
| 397 | + |
| 179 | 398 | function update(delta, elapsed) { |
| 180 | 399 | // Animate water highlight |
| 181 | 400 | highlight.position.x = -radius * 0.35 + Math.sin(elapsed * 0.5) * 0.2 |
@@ -197,6 +416,36 @@ export function createPond(scene, gradientMap) { |
| 197 | 416 | ripples.splice(i, 1) |
| 198 | 417 | } |
| 199 | 418 | } |
| 419 | + |
| 420 | + // Spawn new smoke particles |
| 421 | + smokeSpawnTimer += delta |
| 422 | + if (smokeSpawnTimer > 0.8) { |
| 423 | + smokeSpawnTimer = 0 |
| 424 | + createSmokeParticle() |
| 425 | + } |
| 426 | + |
| 427 | + // Update smoke particles |
| 428 | + for (let i = smokeParticles.length - 1; i >= 0; i--) { |
| 429 | + const smoke = smokeParticles[i] |
| 430 | + smoke.age += delta |
| 431 | + |
| 432 | + // Rise and drift |
| 433 | + smoke.mesh.position.y += smoke.riseSpeed * delta |
| 434 | + smoke.mesh.position.x += smoke.driftX * delta |
| 435 | + smoke.mesh.position.z += smoke.driftZ * delta |
| 436 | + |
| 437 | + // Grow and fade |
| 438 | + const progress = smoke.age / smoke.maxAge |
| 439 | + smoke.mesh.scale.setScalar(1 + progress * 2) |
| 440 | + smoke.mesh.material.opacity = 0.6 * (1 - progress) |
| 441 | + |
| 442 | + if (smoke.age >= smoke.maxAge) { |
| 443 | + village.remove(smoke.mesh) |
| 444 | + smoke.mesh.geometry.dispose() |
| 445 | + smoke.mesh.material.dispose() |
| 446 | + smokeParticles.splice(i, 1) |
| 447 | + } |
| 448 | + } |
| 200 | 449 | } |
| 201 | 450 | |
| 202 | 451 | scene.add(group) |