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
22
 import * as THREE from 'three'
33
 
44
 export function createKoiSchool(scene, gradientMap, pondRadius) {
@@ -10,7 +10,7 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
1010
   const koiColors = [
1111
     { body: 0xff6b35, spots: 0xffffff },  // Orange with white
1212
     { body: 0xffffff, spots: 0xff4444 },  // White with red
13
-    { body: 0xffaa00, spots: 0x000000 },  // Gold with black
13
+    { body: 0xffaa00, spots: 0x222222 },  // Gold with black
1414
     { body: 0xff3333, spots: 0xffffff },  // Red with white
1515
     { body: 0xffd700, spots: 0xff6600 },  // Golden
1616
   ]
@@ -18,33 +18,26 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
1818
   for (let i = 0; i < koiCount; i++) {
1919
     const koi = createKoi(gradientMap, koiColors[i % koiColors.length])
2020
 
21
-    // Random starting position
21
+    // Random starting position and direction
2222
     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
+
2425
     koi.group.position.set(
2526
       Math.cos(angle) * dist,
26
-      -0.08, // Just below water surface
27
+      -0.08,
2728
       Math.sin(angle) * dist
2829
     )
30
+
31
+    // Face a random direction to start
2932
     koi.group.rotation.y = Math.random() * Math.PI * 2
3033
 
3134
     koi.state = {
32
-      targetX: koi.group.position.x,
33
-      targetZ: koi.group.position.z,
34
-      baseSpeed: 0.8 + Math.random() * 1.2,
35
-      speed: 0.8 + Math.random() * 1.2,
36
-      turnSpeed: 2 + Math.random() * 3,
37
-      flickerPhase: Math.random() * Math.PI * 2,
35
+      speed: 0.3 + Math.random() * 0.3,
36
+      turnRate: 0,  // Current turning rate
37
+      targetTurnRate: 0,
38
+      turnTimer: Math.random() * 3,
3839
       panicTimer: 0,
39
-      panicMode: false,
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
40
+      flickerPhase: Math.random() * Math.PI * 2
4841
     }
4942
 
5043
     group.add(koi.group)
@@ -66,71 +59,50 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
6659
       gradientMap: gradientMap
6760
     })
6861
 
69
-    // Body - elongated oval
70
-    const bodyGeom = new THREE.SphereGeometry(0.12, 6, 4)
71
-    bodyGeom.scale(2, 0.6, 0.8)
62
+    // Body - elongated oval, facing +Z
63
+    const bodyGeom = new THREE.SphereGeometry(0.1, 6, 4)
64
+    bodyGeom.scale(0.7, 0.5, 1.8) // Long along Z
7265
     const body = new THREE.Mesh(bodyGeom, bodyMaterial)
7366
     koiGroup.add(body)
7467
 
75
-    // Head
76
-    const headGeom = new THREE.SphereGeometry(0.08, 5, 4)
77
-    headGeom.scale(1.2, 0.8, 1)
68
+    // Head - at +Z end
69
+    const headGeom = new THREE.SphereGeometry(0.07, 5, 4)
70
+    headGeom.scale(0.9, 0.7, 1)
7871
     const head = new THREE.Mesh(headGeom, bodyMaterial)
79
-    head.position.set(0.2, 0, 0)
72
+    head.position.set(0, 0.01, 0.18)
8073
     koiGroup.add(head)
8174
 
82
-    // Tail fin
83
-    const tailGeom = new THREE.ConeGeometry(0.08, 0.18, 4)
75
+    // Tail fin - at -Z end
76
+    const tailGeom = new THREE.ConeGeometry(0.06, 0.15, 4)
77
+    tailGeom.rotateX(Math.PI / 2) // Point along Z
8478
     const tail = new THREE.Mesh(tailGeom, bodyMaterial)
85
-    tail.position.set(-0.28, 0, 0)
86
-    tail.rotation.z = Math.PI / 2
79
+    tail.position.set(0, 0, -0.22)
8780
     koiGroup.add(tail)
8881
 
89
-    // Spots (2-3 random spots)
82
+    // Spots on top
9083
     const spotCount = 2 + Math.floor(Math.random() * 2)
9184
     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)
9386
       const spot = new THREE.Mesh(spotGeom, spotMaterial)
9487
       spot.position.set(
95
-        (Math.random() - 0.5) * 0.2,
88
+        (Math.random() - 0.5) * 0.06,
9689
         0.04,
97
-        (Math.random() - 0.5) * 0.08
90
+        (Math.random() - 0.5) * 0.12
9891
       )
99
-      spot.scale.y = 0.5
92
+      spot.scale.y = 0.4
10093
       koiGroup.add(spot)
10194
     }
10295
 
10396
     // Dorsal fin
104
-    const dorsalGeom = new THREE.ConeGeometry(0.03, 0.08, 3)
97
+    const dorsalGeom = new THREE.ConeGeometry(0.02, 0.06, 3)
10598
     const dorsal = new THREE.Mesh(dorsalGeom, bodyMaterial)
106
-    dorsal.position.set(-0.05, 0.06, 0)
107
-    dorsal.rotation.z = -0.3
99
+    dorsal.position.set(0, 0.055, -0.03)
108100
     koiGroup.add(dorsal)
109101
 
110
-    // Scale down the whole koi
111
-    koiGroup.scale.setScalar(0.8)
102
+    // Scale the whole koi
103
+    koiGroup.scale.setScalar(0.9)
112104
 
113
-    return { group: koiGroup, body, 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
105
+    return { group: koiGroup, tail }
134106
   }
135107
 
136108
   function triggerPanic(x, z) {
@@ -141,25 +113,18 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
141113
       )
142114
 
143115
       if (dist < 1.5) {
144
-        koi.state.panicMode = true
145
-        koi.state.panicTimer = 0.8 + Math.random() * 0.6 // Shorter panic
146
-        koi.state.speed = koi.state.baseSpeed * 2 // Just double speed, not crazy fast
147
-
148
-        // Flee away from the bread - but keep it smooth
149
-        const fleeAngle = Math.atan2(
150
-          koi.group.position.z - z,
151
-          koi.group.position.x - x
116
+        koi.state.panicTimer = 1 + Math.random() * 0.5
117
+
118
+        // Turn away from the disturbance
119
+        const awayAngle = Math.atan2(
120
+          koi.group.position.x - x,
121
+          koi.group.position.z - z
152122
         )
153
-        const fleeDist = pondRadius * 0.5 + Math.random() * pondRadius * 0.3
154
-        koi.state.targetX = Math.cos(fleeAngle) * fleeDist
155
-        koi.state.targetZ = Math.sin(fleeAngle) * fleeDist
156
-
157
-        // Clamp to pond
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
-        }
123
+        // Set a strong turn toward the away direction
124
+        let turnNeeded = awayAngle - koi.group.rotation.y
125
+        while (turnNeeded > Math.PI) turnNeeded -= Math.PI * 2
126
+        while (turnNeeded < -Math.PI) turnNeeded += Math.PI * 2
127
+        koi.state.targetTurnRate = Math.sign(turnNeeded) * 3
163128
       }
164129
     }
165130
   }
@@ -167,104 +132,65 @@ export function createKoiSchool(scene, gradientMap, pondRadius) {
167132
   function update(delta, elapsed) {
168133
     for (const koi of kois) {
169134
       const s = koi.state
135
+      const pos = koi.group.position
170136
 
171
-      // Update panic timer
172
-      if (s.panicMode) {
137
+      // Update panic
138
+      const isPanicked = s.panicTimer > 0
139
+      if (isPanicked) {
173140
         s.panicTimer -= delta
174
-        if (s.panicTimer <= 0) {
175
-          s.panicMode = false
176
-          s.speed = s.baseSpeed
177
-        }
178141
       }
179142
 
180
-      // Idle behavior - sometimes koi just stop and chill
181
-      if (s.isIdle) {
182
-        s.idleTimer -= delta
183
-        if (s.idleTimer <= 0) {
184
-          s.isIdle = false
185
-          pickNewTarget(koi, pondRadius)
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
143
+      // Decide turning behavior
144
+      s.turnTimer -= delta
145
+      if (s.turnTimer <= 0 && !isPanicked) {
146
+        // Occasionally change turn rate for natural wandering
147
+        s.targetTurnRate = (Math.random() - 0.5) * 1.5
148
+        s.turnTimer = 1 + Math.random() * 3
191149
       }
192150
 
193
-      // Wander behavior - each koi has its own rhythm
194
-      s.wanderTimer -= delta
195
-      if (s.wanderTimer <= 0 && !s.panicMode) {
196
-        // Random chance to go idle
197
-        if (Math.random() < 0.2) {
198
-          s.isIdle = true
199
-          s.idleTimer = 2 + Math.random() * 4
200
-          s.wanderTimer = 0.5
201
-          continue
202
-        }
203
-
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
151
+      // Check if heading toward pond edge
152
+      const distFromCenter = Math.hypot(pos.x, pos.z)
153
+      if (distFromCenter > pondRadius * 0.75) {
154
+        // Calculate angle to center
155
+        const toCenter = Math.atan2(-pos.x, -pos.z)
156
+        let turnNeeded = toCenter - koi.group.rotation.y
157
+        while (turnNeeded > Math.PI) turnNeeded -= Math.PI * 2
158
+        while (turnNeeded < -Math.PI) turnNeeded += Math.PI * 2
159
+
160
+        // Steer back toward center
161
+        s.targetTurnRate = Math.sign(turnNeeded) * 1.5
218162
       }
219163
 
220
-      // Move toward target - smooth, natural swimming
221
-      const dx = s.targetX - koi.group.position.x
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
164
+      // Smooth turn rate changes
165
+      s.turnRate += (s.targetTurnRate - s.turnRate) * delta * 2
248166
 
249
-      // Reached close to target - pick new one
250
-      if (dist < 0.3) {
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
-      }
167
+      // Apply rotation
168
+      koi.group.rotation.y += s.turnRate * delta
258169
 
259
-      // Gentle depth variation - natural swimming motion
260
-      koi.group.position.y = -0.08 + Math.sin(elapsed * 3 + s.flickerPhase) * 0.01
170
+      // Move forward (in the direction the fish is facing, which is +Z in local space)
171
+      const speed = isPanicked ? s.speed * 2.5 : s.speed
261172
 
262
-      // Keep in pond bounds - smooth turnaround
263
-      const currentDist = Math.hypot(koi.group.position.x, koi.group.position.z)
264
-      if (currentDist > pondRadius * 0.85) {
265
-        // Steer back toward center
266
-        s.targetX = (Math.random() - 0.5) * pondRadius * 0.5
267
-        s.targetZ = (Math.random() - 0.5) * pondRadius * 0.5
173
+      // Get forward direction from rotation
174
+      const forwardX = Math.sin(koi.group.rotation.y)
175
+      const forwardZ = Math.cos(koi.group.rotation.y)
176
+
177
+      pos.x += forwardX * speed * delta
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
268194
       }
269195
     }
270196
   }
src/renderers/three/narwhal.jsmodified
@@ -58,13 +58,15 @@ export function createDonny(scene, gradientMap) {
5858
   head.position.set(1.0, 0.25, 0)
5959
   group.add(head)
6060
 
61
-  // The magnificent tusk!
62
-  const tuskGeom = new THREE.ConeGeometry(0.06, 1.8, 6)
61
+  // The magnificent tusk! - positioned so base touches front of head
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)
6365
   const tusk = new THREE.Mesh(tuskGeom, tuskMaterial)
64
-  tusk.position.set(1.6, 0.35, 0)
65
-  tusk.rotation.z = -Math.PI / 2 + 0.15 // Pointing forward, slightly up
66
-  // Add spiral ridges (simplified with rotation)
67
-  tusk.rotation.y = 0.3
66
+  // Position at front of head
67
+  tusk.position.set(1.35, 0.4, 0)
68
+  // Rotate to point forward and slightly up
69
+  tusk.rotation.z = -Math.PI / 2 + 0.2
6870
   group.add(tusk)
6971
 
7072
   // Eyes