zeroed-some/dougk / fc5951a

Browse files

reapproach koi

Authored by espadonne
SHA
fc5951abccc22b2693093ab052f9bb604748a507
Parents
3d099f7
Tree
5304778

2 changed files

StatusFile+-
M src/renderers/three/koi.js 93 167
M src/renderers/three/narwhal.js 8 6
src/renderers/three/koi.jsmodified
@@ -1,4 +1,4 @@
1
-// Koi fish - rapid flickering swimmers that react to bread
1
+// Koi fish - natural swimmers that react to bread
2
 import * as THREE from 'three'
2
 import * as THREE from 'three'
3
 
3
 
4
 export function createKoiSchool(scene, gradientMap, pondRadius) {
4
 export function createKoiSchool(scene, gradientMap, pondRadius) {
@@ -10,7 +10,7 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
10
   const koiColors = [
10
   const koiColors = [
11
     { body: 0xff6b35, spots: 0xffffff },  // Orange with white
11
     { body: 0xff6b35, spots: 0xffffff },  // Orange with white
12
     { body: 0xffffff, spots: 0xff4444 },  // White with red
12
     { body: 0xffffff, spots: 0xff4444 },  // White with red
13
-    { body: 0xffaa00, spots: 0x000000 },  // Gold with black
13
+    { body: 0xffaa00, spots: 0x222222 },  // Gold with black
14
     { body: 0xff3333, spots: 0xffffff },  // Red with white
14
     { body: 0xff3333, spots: 0xffffff },  // Red with white
15
     { body: 0xffd700, spots: 0xff6600 },  // Golden
15
     { body: 0xffd700, spots: 0xff6600 },  // Golden
16
   ]
16
   ]
@@ -18,33 +18,26 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
18
   for (let i = 0; i < koiCount; i++) {
18
   for (let i = 0; i < koiCount; i++) {
19
     const koi = createKoi(gradientMap, koiColors[i % koiColors.length])
19
     const koi = createKoi(gradientMap, koiColors[i % koiColors.length])
20
 
20
 
21
-    // Random starting position
21
+    // Random starting position and direction
22
     const angle = Math.random() * Math.PI * 2
22
     const angle = Math.random() * Math.PI * 2
23
-    const dist = Math.random() * pondRadius * 0.7 + pondRadius * 0.1
23
+    const dist = Math.random() * pondRadius * 0.6 + pondRadius * 0.1
24
+
24
     koi.group.position.set(
25
     koi.group.position.set(
25
       Math.cos(angle) * dist,
26
       Math.cos(angle) * dist,
26
-      -0.08, // Just below water surface
27
+      -0.08,
27
       Math.sin(angle) * dist
28
       Math.sin(angle) * dist
28
     )
29
     )
30
+
31
+    // Face a random direction to start
29
     koi.group.rotation.y = Math.random() * Math.PI * 2
32
     koi.group.rotation.y = Math.random() * Math.PI * 2
30
 
33
 
31
     koi.state = {
34
     koi.state = {
32
-      targetX: koi.group.position.x,
35
+      speed: 0.3 + Math.random() * 0.3,
33
-      targetZ: koi.group.position.z,
36
+      turnRate: 0,  // Current turning rate
34
-      baseSpeed: 0.8 + Math.random() * 1.2,
37
+      targetTurnRate: 0,
35
-      speed: 0.8 + Math.random() * 1.2,
38
+      turnTimer: Math.random() * 3,
36
-      turnSpeed: 2 + Math.random() * 3,
37
-      flickerPhase: Math.random() * Math.PI * 2,
38
       panicTimer: 0,
39
       panicTimer: 0,
39
-      panicMode: false,
40
+      flickerPhase: Math.random() * Math.PI * 2
40
-      wanderTimer: Math.random() * 3,
41
-      // Independent personality
42
-      restlessness: 0.3 + Math.random() * 0.7, // How often they change direction
43
-      curiosity: Math.random(), // Likelihood to investigate bread vs flee
44
-      sociability: Math.random() * 0.5, // How much they follow others
45
-      idleTimer: 0,
46
-      isIdle: false,
47
-      idleDuration: 0
48
     }
41
     }
49
 
42
 
50
     group.add(koi.group)
43
     group.add(koi.group)
@@ -66,71 +59,50 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
66
       gradientMap: gradientMap
59
       gradientMap: gradientMap
67
     })
60
     })
68
 
61
 
69
-    // Body - elongated oval
62
+    // Body - elongated oval, facing +Z
70
-    const bodyGeom = new THREE.SphereGeometry(0.12, 6, 4)
63
+    const bodyGeom = new THREE.SphereGeometry(0.1, 6, 4)
71
-    bodyGeom.scale(2, 0.6, 0.8)
64
+    bodyGeom.scale(0.7, 0.5, 1.8) // Long along Z
72
     const body = new THREE.Mesh(bodyGeom, bodyMaterial)
65
     const body = new THREE.Mesh(bodyGeom, bodyMaterial)
73
     koiGroup.add(body)
66
     koiGroup.add(body)
74
 
67
 
75
-    // Head
68
+    // Head - at +Z end
76
-    const headGeom = new THREE.SphereGeometry(0.08, 5, 4)
69
+    const headGeom = new THREE.SphereGeometry(0.07, 5, 4)
77
-    headGeom.scale(1.2, 0.8, 1)
70
+    headGeom.scale(0.9, 0.7, 1)
78
     const head = new THREE.Mesh(headGeom, bodyMaterial)
71
     const head = new THREE.Mesh(headGeom, bodyMaterial)
79
-    head.position.set(0.2, 0, 0)
72
+    head.position.set(0, 0.01, 0.18)
80
     koiGroup.add(head)
73
     koiGroup.add(head)
81
 
74
 
82
-    // Tail fin
75
+    // Tail fin - at -Z end
83
-    const tailGeom = new THREE.ConeGeometry(0.08, 0.18, 4)
76
+    const tailGeom = new THREE.ConeGeometry(0.06, 0.15, 4)
77
+    tailGeom.rotateX(Math.PI / 2) // Point along Z
84
     const tail = new THREE.Mesh(tailGeom, bodyMaterial)
78
     const tail = new THREE.Mesh(tailGeom, bodyMaterial)
85
-    tail.position.set(-0.28, 0, 0)
79
+    tail.position.set(0, 0, -0.22)
86
-    tail.rotation.z = Math.PI / 2
87
     koiGroup.add(tail)
80
     koiGroup.add(tail)
88
 
81
 
89
-    // Spots (2-3 random spots)
82
+    // Spots on top
90
     const spotCount = 2 + Math.floor(Math.random() * 2)
83
     const spotCount = 2 + Math.floor(Math.random() * 2)
91
     for (let i = 0; i < spotCount; i++) {
84
     for (let i = 0; i < spotCount; i++) {
92
-      const spotGeom = new THREE.SphereGeometry(0.04 + Math.random() * 0.03, 4, 3)
85
+      const spotGeom = new THREE.SphereGeometry(0.03 + Math.random() * 0.02, 4, 3)
93
       const spot = new THREE.Mesh(spotGeom, spotMaterial)
86
       const spot = new THREE.Mesh(spotGeom, spotMaterial)
94
       spot.position.set(
87
       spot.position.set(
95
-        (Math.random() - 0.5) * 0.2,
88
+        (Math.random() - 0.5) * 0.06,
96
         0.04,
89
         0.04,
97
-        (Math.random() - 0.5) * 0.08
90
+        (Math.random() - 0.5) * 0.12
98
       )
91
       )
99
-      spot.scale.y = 0.5
92
+      spot.scale.y = 0.4
100
       koiGroup.add(spot)
93
       koiGroup.add(spot)
101
     }
94
     }
102
 
95
 
103
     // Dorsal fin
96
     // Dorsal fin
104
-    const dorsalGeom = new THREE.ConeGeometry(0.03, 0.08, 3)
97
+    const dorsalGeom = new THREE.ConeGeometry(0.02, 0.06, 3)
105
     const dorsal = new THREE.Mesh(dorsalGeom, bodyMaterial)
98
     const dorsal = new THREE.Mesh(dorsalGeom, bodyMaterial)
106
-    dorsal.position.set(-0.05, 0.06, 0)
99
+    dorsal.position.set(0, 0.055, -0.03)
107
-    dorsal.rotation.z = -0.3
108
     koiGroup.add(dorsal)
100
     koiGroup.add(dorsal)
109
 
101
 
110
-    // Scale down the whole koi
102
+    // Scale the whole koi
111
-    koiGroup.scale.setScalar(0.8)
103
+    koiGroup.scale.setScalar(0.9)
112
 
104
 
113
-    return { group: koiGroup, body, tail }
105
+    return { group: koiGroup, tail }
114
-  }
115
-
116
-  function pickNewTarget(koi, pondRadius, avoidX, avoidZ) {
117
-    let attempts = 0
118
-    let x, z
119
-
120
-    do {
121
-      const angle = Math.random() * Math.PI * 2
122
-      const dist = Math.random() * pondRadius * 0.7 + pondRadius * 0.1
123
-      x = Math.cos(angle) * dist
124
-      z = Math.sin(angle) * dist
125
-      attempts++
126
-    } while (
127
-      avoidX !== undefined &&
128
-      Math.hypot(x - avoidX, z - avoidZ) < 1.5 &&
129
-      attempts < 10
130
-    )
131
-
132
-    koi.state.targetX = x
133
-    koi.state.targetZ = z
134
   }
106
   }
135
 
107
 
136
   function triggerPanic(x, z) {
108
   function triggerPanic(x, z) {
@@ -141,25 +113,18 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
141
       )
113
       )
142
 
114
 
143
       if (dist < 1.5) {
115
       if (dist < 1.5) {
144
-        koi.state.panicMode = true
116
+        koi.state.panicTimer = 1 + Math.random() * 0.5
145
-        koi.state.panicTimer = 0.8 + Math.random() * 0.6 // Shorter panic
117
+
146
-        koi.state.speed = koi.state.baseSpeed * 2 // Just double speed, not crazy fast
118
+        // Turn away from the disturbance
147
-
119
+        const awayAngle = Math.atan2(
148
-        // Flee away from the bread - but keep it smooth
120
+          koi.group.position.x - x,
149
-        const fleeAngle = Math.atan2(
121
+          koi.group.position.z - z
150
-          koi.group.position.z - z,
151
-          koi.group.position.x - x
152
         )
122
         )
153
-        const fleeDist = pondRadius * 0.5 + Math.random() * pondRadius * 0.3
123
+        // Set a strong turn toward the away direction
154
-        koi.state.targetX = Math.cos(fleeAngle) * fleeDist
124
+        let turnNeeded = awayAngle - koi.group.rotation.y
155
-        koi.state.targetZ = Math.sin(fleeAngle) * fleeDist
125
+        while (turnNeeded > Math.PI) turnNeeded -= Math.PI * 2
156
-
126
+        while (turnNeeded < -Math.PI) turnNeeded += Math.PI * 2
157
-        // Clamp to pond
127
+        koi.state.targetTurnRate = Math.sign(turnNeeded) * 3
158
-        const targetDist = Math.hypot(koi.state.targetX, koi.state.targetZ)
159
-        if (targetDist > pondRadius * 0.85) {
160
-          koi.state.targetX *= (pondRadius * 0.85) / targetDist
161
-          koi.state.targetZ *= (pondRadius * 0.85) / targetDist
162
-        }
163
       }
128
       }
164
     }
129
     }
165
   }
130
   }
@@ -167,104 +132,65 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
167
   function update(delta, elapsed) {
132
   function update(delta, elapsed) {
168
     for (const koi of kois) {
133
     for (const koi of kois) {
169
       const s = koi.state
134
       const s = koi.state
135
+      const pos = koi.group.position
170
 
136
 
171
-      // Update panic timer
137
+      // Update panic
172
-      if (s.panicMode) {
138
+      const isPanicked = s.panicTimer > 0
139
+      if (isPanicked) {
173
         s.panicTimer -= delta
140
         s.panicTimer -= delta
174
-        if (s.panicTimer <= 0) {
175
-          s.panicMode = false
176
-          s.speed = s.baseSpeed
177
-        }
178
       }
141
       }
179
 
142
 
180
-      // Idle behavior - sometimes koi just stop and chill
143
+      // Decide turning behavior
181
-      if (s.isIdle) {
144
+      s.turnTimer -= delta
182
-        s.idleTimer -= delta
145
+      if (s.turnTimer <= 0 && !isPanicked) {
183
-        if (s.idleTimer <= 0) {
146
+        // Occasionally change turn rate for natural wandering
184
-          s.isIdle = false
147
+        s.targetTurnRate = (Math.random() - 0.5) * 1.5
185
-          pickNewTarget(koi, pondRadius)
148
+        s.turnTimer = 1 + Math.random() * 3
186
-        }
187
-        // Gentle drifting while idle - still wiggle tail slowly
188
-        koi.tail.rotation.y = Math.sin(elapsed * 3 + s.flickerPhase) * 0.15
189
-        koi.group.position.y = -0.1 + Math.sin(elapsed * 2 + s.flickerPhase) * 0.008
190
-        continue
191
       }
149
       }
192
 
150
 
193
-      // Wander behavior - each koi has its own rhythm
151
+      // Check if heading toward pond edge
194
-      s.wanderTimer -= delta
152
+      const distFromCenter = Math.hypot(pos.x, pos.z)
195
-      if (s.wanderTimer <= 0 && !s.panicMode) {
153
+      if (distFromCenter > pondRadius * 0.75) {
196
-        // Random chance to go idle
154
+        // Calculate angle to center
197
-        if (Math.random() < 0.2) {
155
+        const toCenter = Math.atan2(-pos.x, -pos.z)
198
-          s.isIdle = true
156
+        let turnNeeded = toCenter - koi.group.rotation.y
199
-          s.idleTimer = 2 + Math.random() * 4
157
+        while (turnNeeded > Math.PI) turnNeeded -= Math.PI * 2
200
-          s.wanderTimer = 0.5
158
+        while (turnNeeded < -Math.PI) turnNeeded += Math.PI * 2
201
-          continue
159
+
202
-        }
160
+        // Steer back toward center
203
-
161
+        s.targetTurnRate = Math.sign(turnNeeded) * 1.5
204
-        // Sometimes follow another koi loosely (if sociable)
205
-        if (Math.random() < s.sociability && kois.length > 1) {
206
-          const otherKoi = kois[Math.floor(Math.random() * kois.length)]
207
-          if (otherKoi !== koi) {
208
-            // Head toward where they are, with some offset
209
-            s.targetX = otherKoi.group.position.x + (Math.random() - 0.5) * 2
210
-            s.targetZ = otherKoi.group.position.z + (Math.random() - 0.5) * 2
211
-          }
212
-        } else {
213
-          pickNewTarget(koi, pondRadius)
214
-        }
215
-
216
-        // Longer wander intervals for more natural movement
217
-        s.wanderTimer = 2 + Math.random() * 4
218
       }
162
       }
219
 
163
 
220
-      // Move toward target - smooth, natural swimming
164
+      // Smooth turn rate changes
221
-      const dx = s.targetX - koi.group.position.x
165
+      s.turnRate += (s.targetTurnRate - s.turnRate) * delta * 2
222
-      const dz = s.targetZ - koi.group.position.z
223
-      const dist = Math.hypot(dx, dz)
224
-
225
-      // Calculate target rotation - koi model faces +X, so use atan2(dz, dx)
226
-      const targetRot = Math.atan2(dz, dx)
227
-
228
-      // Very smooth rotation - fish don't turn sharply
229
-      let rotDiff = targetRot - koi.group.rotation.y
230
-      while (rotDiff > Math.PI) rotDiff -= Math.PI * 2
231
-      while (rotDiff < -Math.PI) rotDiff += Math.PI * 2
232
-
233
-      // Slower turn rate for natural movement
234
-      const turnRate = s.panicMode ? 2.5 : 1.2
235
-      koi.group.rotation.y += rotDiff * turnRate * delta
236
-
237
-      // Move in the direction koi is facing (model faces +X, so use cos/sin)
238
-      const moveSpeed = s.panicMode ? s.speed * 1.8 : s.speed * 0.5
239
-      const moveX = Math.cos(koi.group.rotation.y) * moveSpeed * delta
240
-      const moveZ = Math.sin(koi.group.rotation.y) * moveSpeed * delta
241
-      koi.group.position.x += moveX
242
-      koi.group.position.z += moveZ
243
-
244
-      // Tail wiggle - proportional to speed
245
-      const wiggleSpeed = s.panicMode ? 15 : 8
246
-      const wiggleAmount = s.panicMode ? 0.4 : 0.3
247
-      koi.tail.rotation.y = Math.sin(elapsed * wiggleSpeed + s.flickerPhase) * wiggleAmount
248
 
166
 
249
-      // Reached close to target - pick new one
167
+      // Apply rotation
250
-      if (dist < 0.3) {
168
+      koi.group.rotation.y += s.turnRate * delta
251
-        if (Math.random() < 0.25) {
252
-          s.isIdle = true
253
-          s.idleTimer = 1 + Math.random() * 3
254
-        } else {
255
-          pickNewTarget(koi, pondRadius)
256
-        }
257
-      }
258
 
169
 
259
-      // Gentle depth variation - natural swimming motion
170
+      // Move forward (in the direction the fish is facing, which is +Z in local space)
260
-      koi.group.position.y = -0.08 + Math.sin(elapsed * 3 + s.flickerPhase) * 0.01
171
+      const speed = isPanicked ? s.speed * 2.5 : s.speed
261
 
172
 
262
-      // Keep in pond bounds - smooth turnaround
173
+      // Get forward direction from rotation
263
-      const currentDist = Math.hypot(koi.group.position.x, koi.group.position.z)
174
+      const forwardX = Math.sin(koi.group.rotation.y)
264
-      if (currentDist > pondRadius * 0.85) {
175
+      const forwardZ = Math.cos(koi.group.rotation.y)
265
-        // Steer back toward center
176
+
266
-        s.targetX = (Math.random() - 0.5) * pondRadius * 0.5
177
+      pos.x += forwardX * speed * delta
267
-        s.targetZ = (Math.random() - 0.5) * pondRadius * 0.5
178
+      pos.z += forwardZ * speed * delta
179
+
180
+      // Tail wiggle - faster when moving fast
181
+      const wiggleSpeed = isPanicked ? 18 : 10
182
+      const wiggleAmount = isPanicked ? 0.4 : 0.25
183
+      koi.tail.rotation.y = Math.sin(elapsed * wiggleSpeed + s.flickerPhase) * wiggleAmount
184
+
185
+      // Gentle vertical bob
186
+      pos.y = -0.08 + Math.sin(elapsed * 2.5 + s.flickerPhase) * 0.008
187
+
188
+      // Hard clamp to pond bounds
189
+      const currentDist = Math.hypot(pos.x, pos.z)
190
+      if (currentDist > pondRadius * 0.88) {
191
+        const scale = (pondRadius * 0.85) / currentDist
192
+        pos.x *= scale
193
+        pos.z *= scale
268
       }
194
       }
269
     }
195
     }
270
   }
196
   }
src/renderers/three/narwhal.jsmodified
@@ -58,13 +58,15 @@ export function createDonny(scene, gradientMap) {
58
   head.position.set(1.0, 0.25, 0)
58
   head.position.set(1.0, 0.25, 0)
59
   group.add(head)
59
   group.add(head)
60
 
60
 
61
-  // The magnificent tusk!
61
+  // The magnificent tusk! - positioned so base touches front of head
62
-  const tuskGeom = new THREE.ConeGeometry(0.06, 1.8, 6)
62
+  const tuskGeom = new THREE.ConeGeometry(0.05, 1.4, 6)
63
+  // Shift geometry so base is at origin, tip extends in +Y
64
+  tuskGeom.translate(0, 0.7, 0)
63
   const tusk = new THREE.Mesh(tuskGeom, tuskMaterial)
65
   const tusk = new THREE.Mesh(tuskGeom, tuskMaterial)
64
-  tusk.position.set(1.6, 0.35, 0)
66
+  // Position at front of head
65
-  tusk.rotation.z = -Math.PI / 2 + 0.15 // Pointing forward, slightly up
67
+  tusk.position.set(1.35, 0.4, 0)
66
-  // Add spiral ridges (simplified with rotation)
68
+  // Rotate to point forward and slightly up
67
-  tusk.rotation.y = 0.3
69
+  tusk.rotation.z = -Math.PI / 2 + 0.2
68
   group.add(tusk)
70
   group.add(tusk)
69
 
71
 
70
   // Eyes
72
   // Eyes