JavaScript · 28067 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 // BOATHOUSE - cozy little structure at pond corner
363 // ============================================
364
365 const boathouse = new THREE.Group()
366 const boathouseX = -3.3
367 const boathouseZ = 3.8
368
369 // Boathouse materials
370 const boathouseWoodMaterial = new THREE.MeshToonMaterial({
371 color: 0x8b6914, // Weathered wood
372 gradientMap: gradientMap
373 })
374 const boathouseRoofMaterial = new THREE.MeshToonMaterial({
375 color: 0x5a4a3a, // Dark wood/slate roof
376 gradientMap: gradientMap
377 })
378 const boathouseTrimMaterial = new THREE.MeshToonMaterial({
379 color: 0x6b5030, // Darker trim
380 gradientMap: gradientMap
381 })
382
383 // Main building - slightly larger than houses to dwarf Doug
384 const boathouseBodyGeom = new THREE.BoxGeometry(1.2, 0.9, 1.0)
385 const boathouseBody = new THREE.Mesh(boathouseBodyGeom, boathouseWoodMaterial)
386 boathouseBody.position.set(0, 0.45, 0)
387 boathouse.add(boathouseBody)
388
389 // Roof - pitched roof
390 const roofShape = new THREE.Shape()
391 roofShape.moveTo(-0.75, 0)
392 roofShape.lineTo(0, 0.5)
393 roofShape.lineTo(0.75, 0)
394 roofShape.lineTo(-0.75, 0)
395
396 const roofExtrudeSettings = { depth: 1.15, bevelEnabled: false }
397 const boathouseRoofGeom = new THREE.ExtrudeGeometry(roofShape, roofExtrudeSettings)
398 const boathouseRoof = new THREE.Mesh(boathouseRoofGeom, boathouseRoofMaterial)
399 boathouseRoof.position.set(0, 0.9, -0.575)
400 boathouse.add(boathouseRoof)
401
402 // Roof overhang trim
403 const overhangGeom = new THREE.BoxGeometry(1.5, 0.05, 0.08)
404 const overhangFront = new THREE.Mesh(overhangGeom, boathouseTrimMaterial)
405 overhangFront.position.set(0, 0.92, 0.54)
406 boathouse.add(overhangFront)
407 const overhangBack = new THREE.Mesh(overhangGeom, boathouseTrimMaterial)
408 overhangBack.position.set(0, 0.92, -0.54)
409 boathouse.add(overhangBack)
410
411 // Door opening (dark rectangle facing pond)
412 const doorGeom = new THREE.PlaneGeometry(0.45, 0.6)
413 const doorMaterial = new THREE.MeshToonMaterial({
414 color: 0x1a1a1a,
415 gradientMap: gradientMap
416 })
417 const door = new THREE.Mesh(doorGeom, doorMaterial)
418 door.position.set(0.601, 0.35, 0.15)
419 door.rotation.y = Math.PI / 2
420 boathouse.add(door)
421
422 // Window on side
423 const boathouseWindowGeom = new THREE.PlaneGeometry(0.25, 0.2)
424 const boathouseWindowMaterial = new THREE.MeshToonMaterial({
425 color: 0x6b9bc3,
426 gradientMap: gradientMap
427 })
428 const boathouseWindow = new THREE.Mesh(boathouseWindowGeom, boathouseWindowMaterial)
429 boathouseWindow.position.set(0, 0.55, 0.51)
430 boathouse.add(boathouseWindow)
431
432 // Window frame
433 const frameH = new THREE.Mesh(
434 new THREE.BoxGeometry(0.3, 0.02, 0.02),
435 boathouseTrimMaterial
436 )
437 frameH.position.set(0, 0.55, 0.52)
438 boathouse.add(frameH)
439 const frameV = new THREE.Mesh(
440 new THREE.BoxGeometry(0.02, 0.25, 0.02),
441 boathouseTrimMaterial
442 )
443 frameV.position.set(0, 0.55, 0.52)
444 boathouse.add(frameV)
445
446 // Dock/platform extending toward pond
447 const dockGeom = new THREE.BoxGeometry(1.0, 0.08, 0.6)
448 const dock = new THREE.Mesh(dockGeom, boathouseWoodMaterial)
449 dock.position.set(1.1, 0.04, 0.15)
450 boathouse.add(dock)
451
452 // Dock support posts
453 const dockPostGeom = new THREE.CylinderGeometry(0.04, 0.05, 0.3, 6)
454 const dockPost1 = new THREE.Mesh(dockPostGeom, boathouseTrimMaterial)
455 dockPost1.position.set(1.5, -0.1, 0.35)
456 boathouse.add(dockPost1)
457 const dockPost2 = new THREE.Mesh(dockPostGeom, boathouseTrimMaterial)
458 dockPost2.position.set(1.5, -0.1, -0.05)
459 boathouse.add(dockPost2)
460
461 // Position boathouse at corner, angled toward pond
462 boathouse.position.set(boathouseX, 0, boathouseZ)
463 boathouse.rotation.y = Math.PI / 4 + 0.3 // Angled toward pond center
464
465 group.add(boathouse)
466
467 // ============================================
468 // ROWBOAT - floating by the dock
469 // ============================================
470
471 const rowboat = new THREE.Group()
472
473 const rowboatWoodMaterial = new THREE.MeshToonMaterial({
474 color: 0x6b4423, // Dark wood
475 gradientMap: gradientMap
476 })
477 const rowboatTrimMaterial = new THREE.MeshToonMaterial({
478 color: 0x8b5a2b, // Lighter trim
479 gradientMap: gradientMap
480 })
481
482 // Boat hull - elongated bowl shape using lathe geometry
483 const hullPoints = []
484 hullPoints.push(new THREE.Vector2(0, 0))
485 hullPoints.push(new THREE.Vector2(0.18, 0))
486 hullPoints.push(new THREE.Vector2(0.22, 0.03))
487 hullPoints.push(new THREE.Vector2(0.22, 0.1))
488 hullPoints.push(new THREE.Vector2(0.18, 0.14))
489 hullPoints.push(new THREE.Vector2(0, 0.14))
490
491 const hullGeom = new THREE.LatheGeometry(hullPoints, 8)
492 hullGeom.scale(1, 1, 2.2) // Stretch into boat shape
493 const hull = new THREE.Mesh(hullGeom, rowboatWoodMaterial)
494 hull.rotation.x = Math.PI // Flip right side up
495 hull.position.y = 0.14
496 rowboat.add(hull)
497
498 // Boat seats (thwarts)
499 const seatGeom = new THREE.BoxGeometry(0.32, 0.02, 0.08)
500 const seat1 = new THREE.Mesh(seatGeom, rowboatTrimMaterial)
501 seat1.position.set(0, 0.08, 0.15)
502 rowboat.add(seat1)
503 const seat2 = new THREE.Mesh(seatGeom, rowboatTrimMaterial)
504 seat2.position.set(0, 0.08, -0.15)
505 rowboat.add(seat2)
506
507 // Oars resting in boat
508 const oarMaterial = new THREE.MeshToonMaterial({
509 color: 0x9b7b4a,
510 gradientMap: gradientMap
511 })
512 const oarHandleGeom = new THREE.CylinderGeometry(0.012, 0.012, 0.5, 6)
513 const oarBladeGeom = new THREE.BoxGeometry(0.06, 0.01, 0.15)
514
515 // Left oar
516 const oar1 = new THREE.Group()
517 const oarHandle1 = new THREE.Mesh(oarHandleGeom, oarMaterial)
518 oarHandle1.rotation.z = Math.PI / 2
519 oar1.add(oarHandle1)
520 const oarBlade1 = new THREE.Mesh(oarBladeGeom, oarMaterial)
521 oarBlade1.position.x = 0.28
522 oar1.add(oarBlade1)
523 oar1.position.set(0.12, 0.1, 0)
524 oar1.rotation.y = 0.15
525 rowboat.add(oar1)
526
527 // Right oar
528 const oar2 = new THREE.Group()
529 const oarHandle2 = new THREE.Mesh(oarHandleGeom, oarMaterial)
530 oarHandle2.rotation.z = Math.PI / 2
531 oar2.add(oarHandle2)
532 const oarBlade2 = new THREE.Mesh(oarBladeGeom, oarMaterial)
533 oarBlade2.position.x = -0.28
534 oar2.add(oarBlade2)
535 oar2.position.set(-0.12, 0.1, 0)
536 oar2.rotation.y = -0.15
537 rowboat.add(oar2)
538
539 // Position rowboat by the dock (in the water)
540 // Place it at the pond edge near the dock - inside the water
541 const rowboatAngle = Math.atan2(boathouseZ, boathouseX) // Angle from center to boathouse
542 const rowboatDist = 2.7 // Closer to pond center
543 rowboat.position.set(
544 Math.cos(rowboatAngle) * rowboatDist,
545 0,
546 Math.sin(rowboatAngle) * rowboatDist
547 )
548 rowboat.rotation.y = rowboatAngle + Math.PI / 2 + 0.2 // Parallel to shore, slightly askew
549 rowboat.userData.baseY = 0
550 rowboat.userData.baseX = rowboat.position.x
551 rowboat.userData.baseZ = rowboat.position.z
552 rowboat.userData.phase = Math.random() * Math.PI * 2
553 rowboat.userData.lastRipple = 0
554
555 group.add(rowboat)
556
557 // ============================================
558 // TREES - scattered around the edges
559 // ============================================
560
561 const treeMaterial = new THREE.MeshToonMaterial({
562 color: 0x2d5a3d,
563 gradientMap: gradientMap
564 })
565 const trunkMaterial = new THREE.MeshToonMaterial({
566 color: 0x5c4033,
567 gradientMap: gradientMap
568 })
569
570 // Tree positions - scattered around the scene edges
571 const treePositions = [
572 { x: 5.2, z: -4.5, scale: 0.35 },
573 { x: 6.2, z: -2.5, scale: 0.4 },
574 { x: 4.8, z: -1.8, scale: 0.3 },
575 { x: -5.8, z: -3.5, scale: 0.35 },
576 { x: -4.8, z: -2.8, scale: 0.28 },
577 { x: -6, z: 2, scale: 0.32 },
578 { x: 5.5, z: 3, scale: 0.38 },
579 { x: -4, z: 4.5, scale: 0.3 },
580 ]
581
582 for (const pos of treePositions) {
583 const treeGroup = new THREE.Group()
584
585 // Trunk
586 const trunkGeom = new THREE.CylinderGeometry(0.04, 0.06, 0.25, 5)
587 const trunk = new THREE.Mesh(trunkGeom, trunkMaterial)
588 trunk.position.y = 0.125
589 treeGroup.add(trunk)
590
591 // Foliage (stacked cones)
592 const foliage1 = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.3, 5), treeMaterial)
593 foliage1.position.y = 0.35
594 treeGroup.add(foliage1)
595
596 const foliage2 = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.25, 5), treeMaterial)
597 foliage2.position.y = 0.55
598 treeGroup.add(foliage2)
599
600 treeGroup.position.set(pos.x, 0, pos.z)
601 treeGroup.scale.setScalar(pos.scale)
602
603 group.add(treeGroup)
604 }
605
606 // Ripple system
607 const ripples = []
608 const rippleGeom = new THREE.RingGeometry(0.1, 0.15, 16)
609 rippleGeom.rotateX(-Math.PI / 2)
610
611 function addRipple(x, z) {
612 const rippleMaterial = new THREE.MeshBasicMaterial({
613 color: 0xffffff,
614 transparent: true,
615 opacity: 0.6,
616 side: THREE.DoubleSide
617 })
618 const ripple = new THREE.Mesh(rippleGeom.clone(), rippleMaterial)
619 ripple.position.set(x, 0.02, z)
620
621 group.add(ripple)
622 ripples.push({
623 mesh: ripple,
624 age: 0,
625 maxAge: 1.5
626 })
627 }
628
629 let smokeSpawnTimer = 0
630
631 function update(delta, elapsed) {
632 // Animate water highlight
633 highlight.position.x = -radius * 0.35 + Math.sin(elapsed * 0.5) * 0.3
634 highlight.position.z = -radius * 0.35 + Math.cos(elapsed * 0.5) * 0.3
635 highlight.material.opacity = 0.4 + Math.sin(elapsed * 2) * 0.1
636
637 // Animate shimmer
638 shimmer.position.x = radius * 0.15 + Math.cos(elapsed * 0.4) * 0.2
639 shimmer.position.z = radius * 0.15 + Math.sin(elapsed * 0.3) * 0.2
640 shimmer.material.opacity = 0.2 + Math.sin(elapsed * 1.5 + 1) * 0.1
641
642 // Animate sparkles - twinkle effect
643 for (const sparkle of sparkles) {
644 const twinkle = Math.sin(elapsed * 4 + sparkle.userData.phase)
645 sparkle.material.opacity = twinkle > 0.3 ? 0.8 : 0
646 sparkle.position.x = sparkle.userData.baseX + Math.sin(elapsed * 0.5 + sparkle.userData.phase) * 0.1
647 sparkle.position.z = sparkle.userData.baseZ + Math.cos(elapsed * 0.5 + sparkle.userData.phase) * 0.1
648 }
649
650 // Update ripples
651 for (let i = ripples.length - 1; i >= 0; i--) {
652 const ripple = ripples[i]
653 ripple.age += delta
654
655 const progress = ripple.age / ripple.maxAge
656 ripple.mesh.scale.setScalar(1 + progress * 3)
657 ripple.mesh.material.opacity = 0.6 * (1 - progress)
658
659 if (ripple.age >= ripple.maxAge) {
660 group.remove(ripple.mesh)
661 ripple.mesh.geometry.dispose()
662 ripple.mesh.material.dispose()
663 ripples.splice(i, 1)
664 }
665 }
666
667 // Spawn new smoke particles
668 smokeSpawnTimer += delta
669 if (smokeSpawnTimer > 0.8) {
670 smokeSpawnTimer = 0
671 createSmokeParticle()
672 }
673
674 // Animate rowboat - gentle bobbing and rocking
675 const boatPhase = rowboat.userData.phase
676 rowboat.position.y = rowboat.userData.baseY + Math.sin(elapsed * 1.2 + boatPhase) * 0.025
677 rowboat.rotation.x = Math.sin(elapsed * 0.8 + boatPhase) * 0.03
678 rowboat.rotation.z = Math.sin(elapsed * 1.0 + boatPhase + 1) * 0.025
679 // Slight drift/tug motion
680 rowboat.position.x = rowboat.userData.baseX + Math.sin(elapsed * 0.5 + boatPhase) * 0.015
681 rowboat.position.z = rowboat.userData.baseZ + Math.cos(elapsed * 0.4 + boatPhase) * 0.015
682
683 // Occasional ripples from rowboat
684 rowboat.userData.lastRipple += delta
685 if (rowboat.userData.lastRipple > 2.5 + Math.random() * 2) {
686 rowboat.userData.lastRipple = 0
687 addRipple(
688 rowboat.position.x + (Math.random() - 0.5) * 0.3,
689 rowboat.position.z + (Math.random() - 0.5) * 0.3
690 )
691 }
692
693 // Update smoke particles
694 for (let i = smokeParticles.length - 1; i >= 0; i--) {
695 const smoke = smokeParticles[i]
696 smoke.age += delta
697
698 // Rise and drift
699 smoke.mesh.position.y += smoke.riseSpeed * delta
700 smoke.mesh.position.x += smoke.driftX * delta
701 smoke.mesh.position.z += smoke.driftZ * delta
702
703 // Grow and fade
704 const progress = smoke.age / smoke.maxAge
705 smoke.mesh.scale.setScalar(1 + progress * 2)
706 smoke.mesh.material.opacity = 0.6 * (1 - progress)
707
708 if (smoke.age >= smoke.maxAge) {
709 village.remove(smoke.mesh)
710 smoke.mesh.geometry.dispose()
711 smoke.mesh.material.dispose()
712 smokeParticles.splice(i, 1)
713 }
714 }
715 }
716
717 scene.add(group)
718
719 // Forbidden zones for creature emergence (dock and rowboat areas)
720 const forbiddenZones = [
721 { x: rowboat.position.x, z: rowboat.position.z, radius: 0.8 }, // Rowboat
722 { // Dock area - extends from boathouse toward pond
723 x: boathouseX + Math.cos(Math.PI / 4 + 0.3) * 1.5,
724 z: boathouseZ + Math.sin(Math.PI / 4 + 0.3) * 1.5,
725 radius: 1.2
726 }
727 ]
728
729 function isValidEmergenceSpot(x, z) {
730 for (const zone of forbiddenZones) {
731 const dx = x - zone.x
732 const dz = z - zone.z
733 const dist = Math.sqrt(dx * dx + dz * dz)
734 if (dist < zone.radius) {
735 return false
736 }
737 }
738 return true
739 }
740
741 // Add a new forbidden zone (for placed buildings)
742 function addForbiddenZone(x, z, zoneRadius) {
743 forbiddenZones.push({ x, z, radius: zoneRadius })
744 }
745
746 // Get initial forbidden zones (for building placement)
747 function getInitialForbiddenZones() {
748 // Return copy of initial zones (rowboat and dock area)
749 return forbiddenZones.slice(0, 2).map(z => ({ ...z }))
750 }
751
752 // Snap zones for building placement
753 // Each zone has: id, position, angle (rotation), type, allowed buildings, occupied status
754 const snapZones = [
755 // Water edge zones (for docks, fishing huts) - all around the pond
756 { id: 'water_n', x: 0, z: -4.3, angle: Math.PI, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
757 { id: 'water_ne', x: 3.0, z: -3.0, angle: Math.PI * 0.75, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
758 { id: 'water_e', x: 4.3, z: 0, angle: Math.PI * 0.5, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
759 { id: 'water_se', x: 3.0, z: 3.0, angle: Math.PI * 0.25, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
760 { id: 'water_s', x: 0, z: 4.3, angle: 0, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
761 { id: 'water_sw', x: -3.0, z: 3.0, angle: -Math.PI * 0.25, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
762 { id: 'water_w', x: -4.3, z: 0, angle: -Math.PI * 0.5, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
763 { id: 'water_nw', x: -3.0, z: -3.0, angle: -Math.PI * 0.75, type: 'waterEdge', allowedBuildings: ['dock_wooden', 'fishing_hut', 'reeds'], occupied: false },
764
765 // Shore zones (for lighthouse, fence, onion house) - further from water
766 { id: 'shore_n', x: 0, z: -5.5, angle: Math.PI, type: 'shore', allowedBuildings: ['lighthouse', 'fence', 'onion_house'], occupied: false },
767 { id: 'shore_ne', x: 4.0, z: -4.0, angle: Math.PI * 0.75, type: 'shore', allowedBuildings: ['lighthouse', 'fence', 'onion_house'], occupied: false },
768 { id: 'shore_e', x: 5.5, z: 0, angle: Math.PI * 0.5, type: 'shore', allowedBuildings: ['lighthouse', 'fence', 'onion_house'], occupied: false },
769 { id: 'shore_se', x: 4.0, z: 4.0, angle: Math.PI * 0.25, type: 'shore', allowedBuildings: ['lighthouse', 'fence', 'onion_house'], occupied: false },
770 { id: 'shore_s', x: 0, z: 5.5, angle: 0, type: 'shore', allowedBuildings: ['lighthouse', 'fence', 'onion_house'], occupied: false },
771
772 // In-water zones (for reeds)
773 { id: 'water_inner_n', x: 0, z: -2.5, angle: Math.PI, type: 'water', allowedBuildings: ['reeds'], occupied: false },
774 { id: 'water_inner_e', x: 2.5, z: 0, angle: Math.PI * 0.5, type: 'water', allowedBuildings: ['reeds'], occupied: false },
775 { id: 'water_inner_s', x: 0, z: 2.5, angle: 0, type: 'water', allowedBuildings: ['reeds'], occupied: false },
776 { id: 'water_inner_w', x: -2.5, z: 0, angle: -Math.PI * 0.5, type: 'water', allowedBuildings: ['reeds'], occupied: false }
777 ]
778
779 // Get available snap zones for a building type
780 function getAvailableZones(buildingType) {
781 return snapZones.filter(zone =>
782 !zone.occupied && zone.allowedBuildings.includes(buildingType)
783 )
784 }
785
786 // Get zone by ID
787 function getZone(zoneId) {
788 return snapZones.find(z => z.id === zoneId)
789 }
790
791 // Mark a zone as occupied
792 function occupyZone(zoneId) {
793 const zone = getZone(zoneId)
794 if (zone) {
795 zone.occupied = true
796 }
797 }
798
799 // Find nearest valid zone to a point
800 function findNearestZone(x, z, buildingType, snapDistance = 1.5) {
801 const available = getAvailableZones(buildingType)
802 let nearest = null
803 let nearestDist = snapDistance
804
805 for (const zone of available) {
806 const dx = x - zone.x
807 const dz = z - zone.z
808 const dist = Math.sqrt(dx * dx + dz * dz)
809 if (dist < nearestDist) {
810 nearestDist = dist
811 nearest = zone
812 }
813 }
814
815 return nearest
816 }
817
818 return {
819 group,
820 water,
821 radius,
822 addRipple,
823 update,
824 isValidEmergenceSpot,
825 addForbiddenZone,
826 getInitialForbiddenZones,
827 snapZones,
828 getAvailableZones,
829 getZone,
830 occupyZone,
831 findNearestZone
832 }
833 }