zeroed-some/dougk / b9b831c

Browse files

increase donny angle, add koi mechanics

Authored by espadonne
SHA
b9b831ca50357a6802a8c3a4222116ebf40eb5ed
Parents
f9199f4
Tree
68f45f6

3 changed files

StatusFile+-
M src/renderers/three/index.js 7 2
A src/renderers/three/koi.js 286 0
M src/renderers/three/narwhal.js 9 9
src/renderers/three/index.jsmodified
@@ -8,10 +8,11 @@ import { createDoug } from './duck.js'
88
 import { createPond } from './pond.js'
99
 import { BreadManager } from './bread.js'
1010
 import { createDonny } from './narwhal.js'
11
+import { createKoiSchool } from './koi.js'
1112
 import { unlockAudio } from './sounds.js'
1213
 
1314
 let scene, camera, renderer, composer, outlinePass
14
-let doug, pond, breadManager, donny
15
+let doug, pond, breadManager, donny, koiSchool
1516
 let clock
1617
 let animationId = null
1718
 
@@ -91,6 +92,7 @@ export function start(container) {
9192
   doug = createDoug(scene, toonGradient)
9293
   breadManager = new BreadManager(scene, toonGradient)
9394
   donny = createDonny(scene, toonGradient)
95
+  koiSchool = createKoiSchool(scene, toonGradient, pond.radius)
9496
 
9597
   // Post-processing
9698
   composer = new EffectComposer(renderer)
@@ -106,7 +108,7 @@ export function start(container) {
106108
   outlinePass.edgeThickness = 1.5
107109
   outlinePass.visibleEdgeColor.set(0x191410)
108110
   outlinePass.hiddenEdgeColor.set(0x191410)
109
-  outlinePass.selectedObjects = [doug.group, pond.group, donny.group]
111
+  outlinePass.selectedObjects = [doug.group, pond.group, donny.group, koiSchool.group]
110112
   composer.addPass(outlinePass)
111113
   composer.addPass(new OutputPass())
112114
 
@@ -131,6 +133,7 @@ export function start(container) {
131133
       const point = intersects[0].point
132134
       breadManager.spawnBread(point.x, point.z)
133135
       pond.addRipple(point.x, point.z)
136
+      koiSchool.triggerPanic(point.x, point.z)
134137
       outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
135138
     }
136139
   })
@@ -165,6 +168,7 @@ function animate() {
165168
   breadManager.update(delta, elapsed)
166169
   pond.update(delta, elapsed)
167170
   donny.update(delta, elapsed, pond, doug)
171
+  koiSchool.update(delta, elapsed)
168172
 
169173
   composer.render()
170174
 }
@@ -193,6 +197,7 @@ export function stop() {
193197
   doug = null
194198
   pond = null
195199
   donny = null
200
+  koiSchool = null
196201
   breadManager = null
197202
 }
198203
 
src/renderers/three/koi.jsadded
@@ -0,0 +1,286 @@
1
+// Koi fish - rapid flickering swimmers that react to bread
2
+import * as THREE from 'three'
3
+
4
+export function createKoiSchool(scene, gradientMap, pondRadius) {
5
+  const group = new THREE.Group()
6
+  const kois = []
7
+  const koiCount = 5
8
+
9
+  // Koi color variations
10
+  const koiColors = [
11
+    { body: 0xff6b35, spots: 0xffffff },  // Orange with white
12
+    { body: 0xffffff, spots: 0xff4444 },  // White with red
13
+    { body: 0xffaa00, spots: 0x000000 },  // Gold with black
14
+    { body: 0xff3333, spots: 0xffffff },  // Red with white
15
+    { body: 0xffd700, spots: 0xff6600 },  // Golden
16
+  ]
17
+
18
+  for (let i = 0; i < koiCount; i++) {
19
+    const koi = createKoi(gradientMap, koiColors[i % koiColors.length])
20
+
21
+    // Random starting position
22
+    const angle = Math.random() * Math.PI * 2
23
+    const dist = Math.random() * pondRadius * 0.7 + pondRadius * 0.1
24
+    koi.group.position.set(
25
+      Math.cos(angle) * dist,
26
+      -0.08, // Just below water surface
27
+      Math.sin(angle) * dist
28
+    )
29
+    koi.group.rotation.y = Math.random() * Math.PI * 2
30
+
31
+    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,
38
+      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
48
+    }
49
+
50
+    group.add(koi.group)
51
+    kois.push(koi)
52
+  }
53
+
54
+  scene.add(group)
55
+
56
+  function createKoi(gradientMap, colors) {
57
+    const koiGroup = new THREE.Group()
58
+
59
+    const bodyMaterial = new THREE.MeshToonMaterial({
60
+      color: colors.body,
61
+      gradientMap: gradientMap
62
+    })
63
+
64
+    const spotMaterial = new THREE.MeshToonMaterial({
65
+      color: colors.spots,
66
+      gradientMap: gradientMap
67
+    })
68
+
69
+    // Body - elongated oval
70
+    const bodyGeom = new THREE.SphereGeometry(0.12, 6, 4)
71
+    bodyGeom.scale(2, 0.6, 0.8)
72
+    const body = new THREE.Mesh(bodyGeom, bodyMaterial)
73
+    koiGroup.add(body)
74
+
75
+    // Head
76
+    const headGeom = new THREE.SphereGeometry(0.08, 5, 4)
77
+    headGeom.scale(1.2, 0.8, 1)
78
+    const head = new THREE.Mesh(headGeom, bodyMaterial)
79
+    head.position.set(0.2, 0, 0)
80
+    koiGroup.add(head)
81
+
82
+    // Tail fin
83
+    const tailGeom = new THREE.ConeGeometry(0.08, 0.18, 4)
84
+    const tail = new THREE.Mesh(tailGeom, bodyMaterial)
85
+    tail.position.set(-0.28, 0, 0)
86
+    tail.rotation.z = Math.PI / 2
87
+    koiGroup.add(tail)
88
+
89
+    // Spots (2-3 random spots)
90
+    const spotCount = 2 + Math.floor(Math.random() * 2)
91
+    for (let i = 0; i < spotCount; i++) {
92
+      const spotGeom = new THREE.SphereGeometry(0.04 + Math.random() * 0.03, 4, 3)
93
+      const spot = new THREE.Mesh(spotGeom, spotMaterial)
94
+      spot.position.set(
95
+        (Math.random() - 0.5) * 0.2,
96
+        0.04,
97
+        (Math.random() - 0.5) * 0.08
98
+      )
99
+      spot.scale.y = 0.5
100
+      koiGroup.add(spot)
101
+    }
102
+
103
+    // Dorsal fin
104
+    const dorsalGeom = new THREE.ConeGeometry(0.03, 0.08, 3)
105
+    const dorsal = new THREE.Mesh(dorsalGeom, bodyMaterial)
106
+    dorsal.position.set(-0.05, 0.06, 0)
107
+    dorsal.rotation.z = -0.3
108
+    koiGroup.add(dorsal)
109
+
110
+    // Scale down the whole koi
111
+    koiGroup.scale.setScalar(0.8)
112
+
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
134
+  }
135
+
136
+  function triggerPanic(x, z) {
137
+    for (const koi of kois) {
138
+      const dist = Math.hypot(
139
+        koi.group.position.x - x,
140
+        koi.group.position.z - z
141
+      )
142
+
143
+      if (dist < 2) {
144
+        koi.state.panicMode = true
145
+        koi.state.panicTimer = 1.5 + Math.random() * 1
146
+        koi.state.speed = 4 + Math.random() * 2
147
+
148
+        // Flee away from the bread
149
+        const fleeAngle = Math.atan2(
150
+          koi.group.position.z - z,
151
+          koi.group.position.x - x
152
+        )
153
+        const fleeDist = pondRadius * 0.6 + Math.random() * pondRadius * 0.2
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
+        }
163
+      }
164
+    }
165
+  }
166
+
167
+  function update(delta, elapsed) {
168
+    for (const koi of kois) {
169
+      const s = koi.state
170
+
171
+      // Update panic timer
172
+      if (s.panicMode) {
173
+        s.panicTimer -= delta
174
+        if (s.panicTimer <= 0) {
175
+          s.panicMode = false
176
+          s.speed = s.baseSpeed
177
+        }
178
+      }
179
+
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
188
+        koi.tail.rotation.y = Math.sin(elapsed * 4 + s.flickerPhase) * 0.2
189
+        koi.group.position.y = -0.1 + Math.sin(elapsed * 3 + s.flickerPhase) * 0.01
190
+        continue
191
+      }
192
+
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.15) {
198
+          s.isIdle = true
199
+          s.idleTimer = 1 + Math.random() * 3
200
+          s.wanderTimer = 0.5
201
+          continue
202
+        }
203
+
204
+        // Sometimes follow another koi (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're going, with some offset
209
+            s.targetX = otherKoi.state.targetX + (Math.random() - 0.5) * 1.5
210
+            s.targetZ = otherKoi.state.targetZ + (Math.random() - 0.5) * 1.5
211
+          }
212
+        } else {
213
+          pickNewTarget(koi, pondRadius)
214
+        }
215
+
216
+        // Restless koi change direction more often
217
+        s.wanderTimer = (1 + Math.random() * 3) / s.restlessness
218
+      }
219
+
220
+      // Move toward target
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
+      if (dist > 0.15) {
226
+        // Calculate target rotation
227
+        const targetRot = Math.atan2(dx, dz)
228
+
229
+        // Smooth rotation - varies by individual
230
+        let rotDiff = targetRot - koi.group.rotation.y
231
+        while (rotDiff > Math.PI) rotDiff -= Math.PI * 2
232
+        while (rotDiff < -Math.PI) rotDiff += Math.PI * 2
233
+        koi.group.rotation.y += rotDiff * s.turnSpeed * delta
234
+
235
+        // Move forward - speed varies
236
+        const moveSpeed = s.panicMode ? s.speed * 2.5 : s.speed * (0.4 + s.restlessness * 0.4)
237
+        const moveX = Math.sin(koi.group.rotation.y) * moveSpeed * delta
238
+        const moveZ = Math.cos(koi.group.rotation.y) * moveSpeed * delta
239
+        koi.group.position.x += moveX
240
+        koi.group.position.z += moveZ
241
+
242
+        // Tail wiggle - faster when moving fast
243
+        const wiggleSpeed = s.panicMode ? 25 : 8 + s.restlessness * 8
244
+        koi.tail.rotation.y = Math.sin(elapsed * wiggleSpeed + s.flickerPhase) * 0.5
245
+      } else {
246
+        // Reached target - maybe idle, maybe pick new target
247
+        if (Math.random() < 0.3) {
248
+          s.isIdle = true
249
+          s.idleTimer = 0.5 + Math.random() * 2
250
+        } else {
251
+          pickNewTarget(koi, pondRadius)
252
+        }
253
+      }
254
+
255
+      // Flicker/shimmer effect - slight Y oscillation
256
+      koi.group.position.y = -0.08 + Math.sin(elapsed * 8 + s.flickerPhase) * 0.015
257
+
258
+      // Erratic depth changes when panicked
259
+      if (s.panicMode) {
260
+        koi.group.position.y += Math.sin(elapsed * 20 + s.flickerPhase) * 0.03
261
+        // Random direction jitters
262
+        if (Math.random() < delta * 5) {
263
+          koi.group.rotation.y += (Math.random() - 0.5) * 0.8
264
+          pickNewTarget(koi, pondRadius)
265
+        }
266
+      }
267
+
268
+      // Keep in pond bounds
269
+      const currentDist = Math.hypot(koi.group.position.x, koi.group.position.z)
270
+      if (currentDist > pondRadius * 0.9) {
271
+        const scale = (pondRadius * 0.85) / currentDist
272
+        koi.group.position.x *= scale
273
+        koi.group.position.z *= scale
274
+        // Turn back toward center
275
+        pickNewTarget(koi, pondRadius * 0.5)
276
+      }
277
+    }
278
+  }
279
+
280
+  return {
281
+    group,
282
+    update,
283
+    triggerPanic,
284
+    getKois: () => kois.map(k => k.group)
285
+  }
286
+}
src/renderers/three/narwhal.jsmodified
@@ -238,10 +238,10 @@ export function createDonny(scene, gradientMap) {
238238
         // Rise from the water
239239
         const emergeProgress = Math.min(state.timer / 1.5, 1)
240240
         const easeOut = 1 - Math.pow(1 - emergeProgress, 3)
241
-        group.position.y = -2 + easeOut * 1.8 // Rise so rear stays submerged
241
+        group.position.y = -2 + easeOut * 2.5 // Rise higher out of water
242242
 
243
-        // Tilt nose UP, rear submerged (positive X rotation)
244
-        group.rotation.x = 0.45 * easeOut
243
+        // Tilt nose UP ~55 degrees (0.96 radians)
244
+        group.rotation.x = 0.95 * easeOut
245245
 
246246
         // Slowly turn toward Doug - lugubrious, not laser tracking
247247
         group.rotation.y = lerpAngle(group.rotation.y, angleToDoug, delta * 0.5)
@@ -257,12 +257,12 @@ export function createDonny(scene, gradientMap) {
257257
         break
258258
 
259259
       case 'surfaced':
260
-        // Bob gently, rear stays in water
261
-        group.position.y = -0.2 + Math.sin(elapsed * 2) * 0.06
260
+        // Bob gently, positioned higher
261
+        group.position.y = 0.5 + Math.sin(elapsed * 2) * 0.06
262262
         group.rotation.z = Math.sin(elapsed * 1.5) * 0.04
263263
 
264
-        // Keep tilted - nose up, tail submerged
265
-        group.rotation.x = 0.4 + Math.sin(elapsed * 1.5) * 0.05
264
+        // Keep steep tilt ~55 degrees - nose up, tail in water
265
+        group.rotation.x = 0.95 + Math.sin(elapsed * 1.5) * 0.05
266266
         group.rotation.y = lerpAngle(group.rotation.y, angleToDoug, delta * 0.3)
267267
 
268268
         // Gentle flipper animation
@@ -287,10 +287,10 @@ export function createDonny(scene, gradientMap) {
287287
         // Sink back down
288288
         const submergeProgress = Math.min(state.timer / 1.2, 1)
289289
         const easeIn = Math.pow(submergeProgress, 2)
290
-        group.position.y = -0.2 - easeIn * 2
290
+        group.position.y = 0.5 - easeIn * 2.7
291291
 
292292
         // Tilt nose down as diving back under
293
-        group.rotation.x = 0.4 - easeIn * 0.6
293
+        group.rotation.x = 0.95 - easeIn * 1.2
294294
 
295295
         // Add bubbles/ripples as submerging
296296
         if (Math.random() < delta * 4) {