JavaScript · 15425 bytes Raw Blame History
1 // Pond environment - 3D water, shore, and fence with Wind Waker styling
2 import * as THREE from 'three'
3
4 export function createPond(scene, gradientMap) {
5 const group = new THREE.Group()
6 const radius = 4
7
8 // Colors
9 const waterColor = 0x46a0be // Vibrant teal
10 const waterDeep = 0x2d7a94 // Darker teal
11 const shoreColor = 0x78b456 // Bright grass green
12 const sandColor = 0xc8b080 // Sandy edge
13 const fenceColor = 0xb4823c // Warm wood
14
15 // Materials
16 const waterMaterial = new THREE.MeshToonMaterial({
17 color: waterColor,
18 gradientMap: gradientMap,
19 transparent: true,
20 opacity: 0.9
21 })
22
23 const shoreMaterial = new THREE.MeshToonMaterial({
24 color: shoreColor,
25 gradientMap: gradientMap
26 })
27
28 const sandMaterial = new THREE.MeshToonMaterial({
29 color: sandColor,
30 gradientMap: gradientMap
31 })
32
33 const fenceMaterial = new THREE.MeshToonMaterial({
34 color: fenceColor,
35 gradientMap: gradientMap
36 })
37
38 // Ground plane (grass)
39 const groundGeom = new THREE.CircleGeometry(radius + 2.5, 32)
40 groundGeom.rotateX(-Math.PI / 2)
41 const ground = new THREE.Mesh(groundGeom, shoreMaterial)
42 ground.position.y = -0.05
43 group.add(ground)
44
45 // Sandy shore ring
46 const sandGeom = new THREE.RingGeometry(radius - 0.2, radius + 0.5, 32)
47 sandGeom.rotateX(-Math.PI / 2)
48 const sand = new THREE.Mesh(sandGeom, sandMaterial)
49 sand.position.y = -0.02
50 group.add(sand)
51
52 // Water surface
53 const waterGeom = new THREE.CircleGeometry(radius, 32)
54 waterGeom.rotateX(-Math.PI / 2)
55 const water = new THREE.Mesh(waterGeom, waterMaterial)
56 water.position.y = 0
57 group.add(water)
58
59 // Water depth visual (darker center with gradient)
60 const deepGeom = new THREE.CircleGeometry(radius * 0.7, 32)
61 deepGeom.rotateX(-Math.PI / 2)
62 const deepMaterial = new THREE.MeshToonMaterial({
63 color: waterDeep,
64 gradientMap: gradientMap,
65 transparent: true,
66 opacity: 0.6
67 })
68 const deep = new THREE.Mesh(deepGeom, deepMaterial)
69 deep.position.y = -0.02
70 group.add(deep)
71
72 // Secondary highlight shimmer
73 const shimmerGeom = new THREE.CircleGeometry(radius * 0.5, 24)
74 shimmerGeom.rotateX(-Math.PI / 2)
75 const shimmerMaterial = new THREE.MeshBasicMaterial({
76 color: 0x6bc4d8,
77 transparent: true,
78 opacity: 0.25
79 })
80 const shimmer = new THREE.Mesh(shimmerGeom, shimmerMaterial)
81 shimmer.position.set(radius * 0.15, 0.01, radius * 0.15)
82 group.add(shimmer)
83
84 // Main highlight (sun reflection)
85 const highlightGeom = new THREE.CircleGeometry(radius * 0.25, 16)
86 highlightGeom.rotateX(-Math.PI / 2)
87 const highlightMaterial = new THREE.MeshBasicMaterial({
88 color: 0xa8e8f8,
89 transparent: true,
90 opacity: 0.5
91 })
92 const highlight = new THREE.Mesh(highlightGeom, highlightMaterial)
93 highlight.position.set(-radius * 0.35, 0.02, -radius * 0.35)
94 group.add(highlight)
95
96 // Small sparkle highlights
97 const sparkles = []
98 const sparkleMaterial = new THREE.MeshBasicMaterial({
99 color: 0xffffff,
100 transparent: true,
101 opacity: 0.7
102 })
103 for (let i = 0; i < 6; i++) {
104 const sparkleGeom = new THREE.CircleGeometry(0.08 + Math.random() * 0.06, 6)
105 sparkleGeom.rotateX(-Math.PI / 2)
106 const sparkle = new THREE.Mesh(sparkleGeom, sparkleMaterial.clone())
107 const angle = Math.random() * Math.PI * 2
108 const dist = Math.random() * radius * 0.8
109 sparkle.position.set(
110 Math.cos(angle) * dist,
111 0.03,
112 Math.sin(angle) * dist
113 )
114 sparkle.userData.baseX = sparkle.position.x
115 sparkle.userData.baseZ = sparkle.position.z
116 sparkle.userData.phase = Math.random() * Math.PI * 2
117 group.add(sparkle)
118 sparkles.push(sparkle)
119 }
120
121 // Grass tufts around the pond
122 const grassTuftGeom = new THREE.ConeGeometry(0.15, 0.3, 4)
123 const grassMaterial = new THREE.MeshToonMaterial({
124 color: 0x4a8530,
125 gradientMap: gradientMap
126 })
127
128 for (let i = 0; i < 30; i++) {
129 const angle = Math.random() * Math.PI * 2
130 const dist = radius + 0.8 + Math.random() * 1.5
131
132 const tuft = new THREE.Mesh(grassTuftGeom, grassMaterial)
133 tuft.position.set(
134 Math.cos(angle) * dist,
135 0.1,
136 Math.sin(angle) * dist
137 )
138 tuft.rotation.x = (Math.random() - 0.5) * 0.3
139 tuft.rotation.z = (Math.random() - 0.5) * 0.3
140 tuft.scale.setScalar(0.5 + Math.random() * 0.5)
141 group.add(tuft)
142 }
143
144 // Rickety fence
145 const fenceGroup = new THREE.Group()
146 const fenceX = radius + 1
147 const postCount = 5
148 const postSpacing = 0.8
149
150 for (let i = 0; i < postCount; i++) {
151 const wobble = Math.sin(i * 1.5) * 0.1
152
153 // Fence post
154 const postGeom = new THREE.BoxGeometry(0.12, 0.8, 0.12)
155 const post = new THREE.Mesh(postGeom, fenceMaterial)
156 post.position.set(
157 fenceX + wobble,
158 0.35,
159 -1.5 + i * postSpacing
160 )
161 post.rotation.x = wobble * 0.3
162 post.rotation.z = wobble * 0.5
163 fenceGroup.add(post)
164
165 // Post cap
166 const capGeom = new THREE.BoxGeometry(0.16, 0.06, 0.16)
167 const cap = new THREE.Mesh(capGeom, fenceMaterial)
168 cap.position.set(
169 fenceX + wobble,
170 0.78,
171 -1.5 + i * postSpacing
172 )
173 cap.rotation.x = wobble * 0.3
174 cap.rotation.z = wobble * 0.5
175 fenceGroup.add(cap)
176 }
177
178 // Horizontal rails
179 const railGeom = new THREE.BoxGeometry(0.08, 0.08, postSpacing * (postCount - 1) + 0.3)
180
181 const topRail = new THREE.Mesh(railGeom, fenceMaterial)
182 topRail.position.set(fenceX + 0.05, 0.6, -1.5 + (postCount - 1) * postSpacing / 2)
183 topRail.rotation.y = 0.02
184 fenceGroup.add(topRail)
185
186 const bottomRail = new THREE.Mesh(railGeom, fenceMaterial)
187 bottomRail.position.set(fenceX - 0.03, 0.25, -1.5 + (postCount - 1) * postSpacing / 2)
188 bottomRail.rotation.y = -0.03
189 fenceGroup.add(bottomRail)
190
191 group.add(fenceGroup)
192
193 // ============================================
194 // DISTANT SCENERY - Mountains and Village
195 // Scaled small like game board pieces!
196 // ============================================
197
198 // Mountain range materials
199 const mountainMaterial = new THREE.MeshToonMaterial({
200 color: 0x6b8e7a, // Muted green-grey
201 gradientMap: gradientMap
202 })
203 const mountainSnowMaterial = new THREE.MeshToonMaterial({
204 color: 0xe8e8f0, // Snow white with slight blue
205 gradientMap: gradientMap
206 })
207 const mountainDarkMaterial = new THREE.MeshToonMaterial({
208 color: 0x4a6b5a, // Darker mountain
209 gradientMap: gradientMap
210 })
211
212 // Create mountain range (back-left corner)
213 const mountains = new THREE.Group()
214
215 // Large back mountain
216 const bigMountainGeom = new THREE.ConeGeometry(0.8, 1.4, 6)
217 const bigMountain = new THREE.Mesh(bigMountainGeom, mountainMaterial)
218 bigMountain.position.set(-5.5, 0.7, -5)
219 mountains.add(bigMountain)
220
221 // Snow cap for big mountain
222 const snowCapGeom = new THREE.ConeGeometry(0.35, 0.45, 6)
223 const snowCap = new THREE.Mesh(snowCapGeom, mountainSnowMaterial)
224 snowCap.position.set(-5.5, 1.35, -5)
225 mountains.add(snowCap)
226
227 // Medium mountain
228 const medMountainGeom = new THREE.ConeGeometry(0.6, 1.0, 5)
229 const medMountain = new THREE.Mesh(medMountainGeom, mountainDarkMaterial)
230 medMountain.position.set(-4.5, 0.5, -5.8)
231 mountains.add(medMountain)
232
233 // Small mountain
234 const smallMountainGeom = new THREE.ConeGeometry(0.5, 0.8, 5)
235 const smallMountain = new THREE.Mesh(smallMountainGeom, mountainMaterial)
236 smallMountain.position.set(-6.2, 0.4, -4.2)
237 mountains.add(smallMountain)
238
239 // Another tiny peak
240 const tinyMountainGeom = new THREE.ConeGeometry(0.4, 0.6, 5)
241 const tinyMountain = new THREE.Mesh(tinyMountainGeom, mountainDarkMaterial)
242 tinyMountain.position.set(-5, 0.3, -4)
243 mountains.add(tinyMountain)
244
245 group.add(mountains)
246
247 // ============================================
248 // VILLAGE - tiny game-board scale!
249 // ============================================
250
251 const village = new THREE.Group()
252 const villageX = 5.5
253 const villageZ = -3
254
255 // House materials
256 const houseMaterial = new THREE.MeshToonMaterial({
257 color: 0xd4a574, // Warm beige/tan
258 gradientMap: gradientMap
259 })
260 const roofMaterial = new THREE.MeshToonMaterial({
261 color: 0x8b4513, // Brown roof
262 gradientMap: gradientMap
263 })
264 const roofRedMaterial = new THREE.MeshToonMaterial({
265 color: 0xb85450, // Red roof
266 gradientMap: gradientMap
267 })
268 const windowMaterial = new THREE.MeshToonMaterial({
269 color: 0x87ceeb, // Light blue windows
270 gradientMap: gradientMap
271 })
272
273 // Helper to create a tiny house
274 function createHouse(x, z, scale, roofMat) {
275 const houseGroup = new THREE.Group()
276
277 // House body
278 const bodyGeom = new THREE.BoxGeometry(0.3, 0.25, 0.25)
279 const body = new THREE.Mesh(bodyGeom, houseMaterial)
280 body.position.y = 0.125
281 houseGroup.add(body)
282
283 // Roof
284 const roofGeom = new THREE.ConeGeometry(0.22, 0.2, 4)
285 const roof = new THREE.Mesh(roofGeom, roofMat)
286 roof.position.y = 0.32
287 roof.rotation.y = Math.PI / 4
288 houseGroup.add(roof)
289
290 // Window (tiny dot)
291 const windowGeom = new THREE.PlaneGeometry(0.06, 0.06)
292 const windowMesh = new THREE.Mesh(windowGeom, windowMaterial)
293 windowMesh.position.set(0.151, 0.14, 0)
294 houseGroup.add(windowMesh)
295
296 houseGroup.position.set(x, 0, z)
297 houseGroup.scale.setScalar(scale)
298 houseGroup.rotation.y = Math.random() * 0.5 - 0.25
299
300 return houseGroup
301 }
302
303 // Create village houses - clustered together
304 const house1 = createHouse(villageX, villageZ, 1.0, roofMaterial)
305 village.add(house1)
306
307 const house2 = createHouse(villageX + 0.5, villageZ + 0.3, 0.8, roofRedMaterial)
308 village.add(house2)
309
310 const house3 = createHouse(villageX + 0.2, villageZ + 0.6, 0.9, roofMaterial)
311 village.add(house3)
312
313 const house4 = createHouse(villageX - 0.35, villageZ + 0.25, 0.7, roofRedMaterial)
314 village.add(house4)
315
316 // Chimney on main house
317 const chimneyGeom = new THREE.BoxGeometry(0.06, 0.15, 0.06)
318 const chimney = new THREE.Mesh(chimneyGeom, new THREE.MeshToonMaterial({
319 color: 0x8b7355,
320 gradientMap: gradientMap
321 }))
322 chimney.position.set(villageX + 0.08, 0.45, villageZ + 0.04)
323 village.add(chimney)
324
325 // Smoke particles (tiny puffs)
326 const smokeParticles = []
327 const smokeMaterial = new THREE.MeshBasicMaterial({
328 color: 0xdddddd,
329 transparent: true,
330 opacity: 0.5
331 })
332
333 function createSmokeParticle() {
334 const size = 0.03 + Math.random() * 0.03
335 const smokeGeom = new THREE.SphereGeometry(size, 5, 4)
336 const smoke = new THREE.Mesh(smokeGeom, smokeMaterial.clone())
337 smoke.position.set(
338 villageX + 0.08 + (Math.random() - 0.5) * 0.03,
339 0.52,
340 villageZ + 0.04 + (Math.random() - 0.5) * 0.03
341 )
342 village.add(smoke)
343 smokeParticles.push({
344 mesh: smoke,
345 age: 0,
346 maxAge: 2.5 + Math.random() * 1.5,
347 driftX: (Math.random() - 0.5) * 0.08,
348 driftZ: (Math.random() - 0.5) * 0.08,
349 riseSpeed: 0.12 + Math.random() * 0.08
350 })
351 }
352
353 // Initial smoke
354 for (let i = 0; i < 4; i++) {
355 createSmokeParticle()
356 smokeParticles[i].age = Math.random() * 1.5
357 }
358
359 group.add(village)
360
361 // ============================================
362 // TREES - scattered around the edges
363 // ============================================
364
365 const treeMaterial = new THREE.MeshToonMaterial({
366 color: 0x2d5a3d,
367 gradientMap: gradientMap
368 })
369 const trunkMaterial = new THREE.MeshToonMaterial({
370 color: 0x5c4033,
371 gradientMap: gradientMap
372 })
373
374 // Tree positions - scattered around the scene edges
375 const treePositions = [
376 { x: 5.2, z: -4.5, scale: 0.35 },
377 { x: 6.2, z: -2.5, scale: 0.4 },
378 { x: 4.8, z: -1.8, scale: 0.3 },
379 { x: -5.8, z: -3.5, scale: 0.35 },
380 { x: -4.8, z: -2.8, scale: 0.28 },
381 { x: -6, z: 2, scale: 0.32 },
382 { x: 5.5, z: 3, scale: 0.38 },
383 { x: -4, z: 4.5, scale: 0.3 },
384 ]
385
386 for (const pos of treePositions) {
387 const treeGroup = new THREE.Group()
388
389 // Trunk
390 const trunkGeom = new THREE.CylinderGeometry(0.04, 0.06, 0.25, 5)
391 const trunk = new THREE.Mesh(trunkGeom, trunkMaterial)
392 trunk.position.y = 0.125
393 treeGroup.add(trunk)
394
395 // Foliage (stacked cones)
396 const foliage1 = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.3, 5), treeMaterial)
397 foliage1.position.y = 0.35
398 treeGroup.add(foliage1)
399
400 const foliage2 = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.25, 5), treeMaterial)
401 foliage2.position.y = 0.55
402 treeGroup.add(foliage2)
403
404 treeGroup.position.set(pos.x, 0, pos.z)
405 treeGroup.scale.setScalar(pos.scale)
406
407 group.add(treeGroup)
408 }
409
410 // Ripple system
411 const ripples = []
412 const rippleGeom = new THREE.RingGeometry(0.1, 0.15, 16)
413 rippleGeom.rotateX(-Math.PI / 2)
414
415 function addRipple(x, z) {
416 const rippleMaterial = new THREE.MeshBasicMaterial({
417 color: 0xffffff,
418 transparent: true,
419 opacity: 0.6,
420 side: THREE.DoubleSide
421 })
422 const ripple = new THREE.Mesh(rippleGeom.clone(), rippleMaterial)
423 ripple.position.set(x, 0.02, z)
424
425 group.add(ripple)
426 ripples.push({
427 mesh: ripple,
428 age: 0,
429 maxAge: 1.5
430 })
431 }
432
433 let smokeSpawnTimer = 0
434
435 function update(delta, elapsed) {
436 // Animate water highlight
437 highlight.position.x = -radius * 0.35 + Math.sin(elapsed * 0.5) * 0.3
438 highlight.position.z = -radius * 0.35 + Math.cos(elapsed * 0.5) * 0.3
439 highlight.material.opacity = 0.4 + Math.sin(elapsed * 2) * 0.1
440
441 // Animate shimmer
442 shimmer.position.x = radius * 0.15 + Math.cos(elapsed * 0.4) * 0.2
443 shimmer.position.z = radius * 0.15 + Math.sin(elapsed * 0.3) * 0.2
444 shimmer.material.opacity = 0.2 + Math.sin(elapsed * 1.5 + 1) * 0.1
445
446 // Animate sparkles - twinkle effect
447 for (const sparkle of sparkles) {
448 const twinkle = Math.sin(elapsed * 4 + sparkle.userData.phase)
449 sparkle.material.opacity = twinkle > 0.3 ? 0.8 : 0
450 sparkle.position.x = sparkle.userData.baseX + Math.sin(elapsed * 0.5 + sparkle.userData.phase) * 0.1
451 sparkle.position.z = sparkle.userData.baseZ + Math.cos(elapsed * 0.5 + sparkle.userData.phase) * 0.1
452 }
453
454 // Update ripples
455 for (let i = ripples.length - 1; i >= 0; i--) {
456 const ripple = ripples[i]
457 ripple.age += delta
458
459 const progress = ripple.age / ripple.maxAge
460 ripple.mesh.scale.setScalar(1 + progress * 3)
461 ripple.mesh.material.opacity = 0.6 * (1 - progress)
462
463 if (ripple.age >= ripple.maxAge) {
464 group.remove(ripple.mesh)
465 ripple.mesh.geometry.dispose()
466 ripple.mesh.material.dispose()
467 ripples.splice(i, 1)
468 }
469 }
470
471 // Spawn new smoke particles
472 smokeSpawnTimer += delta
473 if (smokeSpawnTimer > 0.8) {
474 smokeSpawnTimer = 0
475 createSmokeParticle()
476 }
477
478 // Update smoke particles
479 for (let i = smokeParticles.length - 1; i >= 0; i--) {
480 const smoke = smokeParticles[i]
481 smoke.age += delta
482
483 // Rise and drift
484 smoke.mesh.position.y += smoke.riseSpeed * delta
485 smoke.mesh.position.x += smoke.driftX * delta
486 smoke.mesh.position.z += smoke.driftZ * delta
487
488 // Grow and fade
489 const progress = smoke.age / smoke.maxAge
490 smoke.mesh.scale.setScalar(1 + progress * 2)
491 smoke.mesh.material.opacity = 0.6 * (1 - progress)
492
493 if (smoke.age >= smoke.maxAge) {
494 village.remove(smoke.mesh)
495 smoke.mesh.geometry.dispose()
496 smoke.mesh.material.dispose()
497 smokeParticles.splice(i, 1)
498 }
499 }
500 }
501
502 scene.add(group)
503
504 return {
505 group,
506 water,
507 radius,
508 addRipple,
509 update
510 }
511 }