zeroed-some/dougk / aa974e3

Browse files

add donny narwhal

Authored by espadonne
SHA
aa974e349161d62c95c58e3efd3fe55d0f3306f2
Parents
f86d428
Tree
4eb783b

2 changed files

StatusFile+-
M src/renderers/three/index.js 6 2
A src/renderers/three/narwhal.js 284 0
src/renderers/three/index.jsmodified
@@ -7,10 +7,11 @@ import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
77
 import { createDoug } from './duck.js'
88
 import { createPond } from './pond.js'
99
 import { BreadManager } from './bread.js'
10
+import { createDonny } from './narwhal.js'
1011
 import { unlockAudio } from './sounds.js'
1112
 
1213
 let scene, camera, renderer, composer, outlinePass
13
-let doug, pond, breadManager
14
+let doug, pond, breadManager, donny
1415
 let clock
1516
 let animationId = null
1617
 
@@ -89,6 +90,7 @@ export function start(container) {
8990
   pond = createPond(scene, toonGradient)
9091
   doug = createDoug(scene, toonGradient)
9192
   breadManager = new BreadManager(scene, toonGradient)
93
+  donny = createDonny(scene, toonGradient)
9294
 
9395
   // Post-processing
9496
   composer = new EffectComposer(renderer)
@@ -104,7 +106,7 @@ export function start(container) {
104106
   outlinePass.edgeThickness = 1.5
105107
   outlinePass.visibleEdgeColor.set(0x191410)
106108
   outlinePass.hiddenEdgeColor.set(0x191410)
107
-  outlinePass.selectedObjects = [doug.group, pond.group]
109
+  outlinePass.selectedObjects = [doug.group, pond.group, donny.group]
108110
   composer.addPass(outlinePass)
109111
   composer.addPass(new OutputPass())
110112
 
@@ -162,6 +164,7 @@ function animate() {
162164
   doug.update(delta, elapsed, breadManager.getActiveBits(), pond)
163165
   breadManager.update(delta, elapsed)
164166
   pond.update(delta, elapsed)
167
+  donny.update(delta, elapsed, pond)
165168
 
166169
   composer.render()
167170
 }
@@ -189,6 +192,7 @@ export function stop() {
189192
   composer = null
190193
   doug = null
191194
   pond = null
195
+  donny = null
192196
   breadManager = null
193197
 }
194198
 
src/renderers/three/narwhal.jsadded
@@ -0,0 +1,284 @@
1
+// Donny the Narwhal - distinguished gentleman of the deep
2
+import * as THREE from 'three'
3
+
4
+export function createDonny(scene, gradientMap) {
5
+  const group = new THREE.Group()
6
+
7
+  // Color palette
8
+  const bodyColor = 0x7a9eb8 // Dusty blue-grey
9
+  const bellyColor = 0xc8d8e4 // Pale belly
10
+  const tuskColor = 0xf5f0e6 // Ivory
11
+  const monocleColor = 0xd4af37 // Gold
12
+
13
+  // Materials
14
+  const bodyMaterial = new THREE.MeshToonMaterial({
15
+    color: bodyColor,
16
+    gradientMap: gradientMap
17
+  })
18
+
19
+  const bellyMaterial = new THREE.MeshToonMaterial({
20
+    color: bellyColor,
21
+    gradientMap: gradientMap
22
+  })
23
+
24
+  const tuskMaterial = new THREE.MeshToonMaterial({
25
+    color: tuskColor,
26
+    gradientMap: gradientMap
27
+  })
28
+
29
+  const monocleMaterial = new THREE.MeshToonMaterial({
30
+    color: monocleColor,
31
+    gradientMap: gradientMap
32
+  })
33
+
34
+  const glassMaterial = new THREE.MeshBasicMaterial({
35
+    color: 0x88ccff,
36
+    transparent: true,
37
+    opacity: 0.3
38
+  })
39
+
40
+  // Main body - elongated oval
41
+  const bodyGeom = new THREE.SphereGeometry(0.5, 8, 6)
42
+  bodyGeom.scale(2.2, 0.7, 0.8)
43
+  const body = new THREE.Mesh(bodyGeom, bodyMaterial)
44
+  body.position.y = 0.1
45
+  group.add(body)
46
+
47
+  // Belly
48
+  const bellyGeom = new THREE.SphereGeometry(0.4, 8, 6)
49
+  bellyGeom.scale(1.8, 0.5, 0.7)
50
+  const belly = new THREE.Mesh(bellyGeom, bellyMaterial)
51
+  belly.position.set(0, -0.05, 0)
52
+  group.add(belly)
53
+
54
+  // Head bump
55
+  const headGeom = new THREE.SphereGeometry(0.35, 8, 6)
56
+  headGeom.scale(1.2, 1, 1)
57
+  const head = new THREE.Mesh(headGeom, bodyMaterial)
58
+  head.position.set(1.0, 0.25, 0)
59
+  group.add(head)
60
+
61
+  // The magnificent tusk!
62
+  const tuskGeom = new THREE.ConeGeometry(0.06, 1.8, 6)
63
+  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
68
+  group.add(tusk)
69
+
70
+  // Eyes
71
+  const eyeGeom = new THREE.SphereGeometry(0.08, 8, 6)
72
+  const eyeMaterial = new THREE.MeshBasicMaterial({ color: 0x1a1a1a })
73
+
74
+  const leftEye = new THREE.Mesh(eyeGeom, eyeMaterial)
75
+  leftEye.position.set(1.15, 0.38, 0.28)
76
+  group.add(leftEye)
77
+
78
+  const rightEye = new THREE.Mesh(eyeGeom, eyeMaterial)
79
+  rightEye.position.set(1.15, 0.38, -0.28)
80
+  group.add(rightEye)
81
+
82
+  // Eye shines
83
+  const shineGeom = new THREE.SphereGeometry(0.025, 6, 4)
84
+  const shineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
85
+
86
+  const leftShine = new THREE.Mesh(shineGeom, shineMaterial)
87
+  leftShine.position.set(1.18, 0.42, 0.3)
88
+  group.add(leftShine)
89
+
90
+  const rightShine = new THREE.Mesh(shineGeom, shineMaterial)
91
+  rightShine.position.set(1.18, 0.42, -0.26)
92
+  group.add(rightShine)
93
+
94
+  // THE MONOCLE - on right eye (our left when facing)
95
+  const monocleGroup = new THREE.Group()
96
+
97
+  // Monocle rim
98
+  const rimGeom = new THREE.TorusGeometry(0.12, 0.015, 8, 16)
99
+  const rim = new THREE.Mesh(rimGeom, monocleMaterial)
100
+  monocleGroup.add(rim)
101
+
102
+  // Monocle glass
103
+  const glassGeom = new THREE.CircleGeometry(0.11, 16)
104
+  const glass = new THREE.Mesh(glassGeom, glassMaterial)
105
+  glass.position.z = 0.01
106
+  monocleGroup.add(glass)
107
+
108
+  // Monocle chain attachment
109
+  const chainStartGeom = new THREE.SphereGeometry(0.02, 6, 4)
110
+  const chainStart = new THREE.Mesh(chainStartGeom, monocleMaterial)
111
+  chainStart.position.set(0, -0.12, 0)
112
+  monocleGroup.add(chainStart)
113
+
114
+  // Chain (simple dangling segments)
115
+  const chainMaterial = new THREE.MeshToonMaterial({
116
+    color: monocleColor,
117
+    gradientMap: gradientMap
118
+  })
119
+  for (let i = 0; i < 4; i++) {
120
+    const linkGeom = new THREE.TorusGeometry(0.018, 0.005, 4, 8)
121
+    const link = new THREE.Mesh(linkGeom, chainMaterial)
122
+    link.position.set(0, -0.16 - i * 0.05, 0)
123
+    link.rotation.x = i % 2 === 0 ? 0 : Math.PI / 2
124
+    monocleGroup.add(link)
125
+  }
126
+
127
+  monocleGroup.position.set(1.22, 0.38, -0.32)
128
+  monocleGroup.rotation.y = -0.3
129
+  group.add(monocleGroup)
130
+
131
+  // Flippers
132
+  const flipperGeom = new THREE.ConeGeometry(0.15, 0.5, 4)
133
+
134
+  const leftFlipper = new THREE.Mesh(flipperGeom, bodyMaterial)
135
+  leftFlipper.position.set(0.3, -0.1, 0.45)
136
+  leftFlipper.rotation.x = 0.5
137
+  leftFlipper.rotation.z = 2.2
138
+  group.add(leftFlipper)
139
+
140
+  const rightFlipper = new THREE.Mesh(flipperGeom, bodyMaterial)
141
+  rightFlipper.position.set(0.3, -0.1, -0.45)
142
+  rightFlipper.rotation.x = -0.5
143
+  rightFlipper.rotation.z = 2.2
144
+  group.add(rightFlipper)
145
+
146
+  // Tail flukes
147
+  const flukeGeom = new THREE.ConeGeometry(0.2, 0.4, 4)
148
+
149
+  const leftFluke = new THREE.Mesh(flukeGeom, bodyMaterial)
150
+  leftFluke.position.set(-1.2, 0.15, 0.15)
151
+  leftFluke.rotation.z = 1.8
152
+  leftFluke.rotation.y = 0.3
153
+  group.add(leftFluke)
154
+
155
+  const rightFluke = new THREE.Mesh(flukeGeom, bodyMaterial)
156
+  rightFluke.position.set(-1.2, 0.15, -0.15)
157
+  rightFluke.rotation.z = 1.8
158
+  rightFluke.rotation.y = -0.3
159
+  group.add(rightFluke)
160
+
161
+  // Donny starts hidden below the water
162
+  group.position.y = -3
163
+  group.visible = false
164
+
165
+  scene.add(group)
166
+
167
+  // State
168
+  const state = {
169
+    mode: 'waiting', // 'waiting', 'rumbling', 'emerging', 'surfaced', 'submerging'
170
+    timer: 30 + Math.random() * 30, // First appearance in 30-60 seconds
171
+    emergeX: 0,
172
+    emergeZ: 0,
173
+    surfaceTime: 0
174
+  }
175
+
176
+  function startRumble(pond) {
177
+    state.mode = 'rumbling'
178
+    state.timer = 0
179
+
180
+    // Pick random spot in the pond
181
+    const angle = Math.random() * Math.PI * 2
182
+    const dist = Math.random() * pond.radius * 0.5 + pond.radius * 0.2
183
+    state.emergeX = Math.cos(angle) * dist
184
+    state.emergeZ = Math.sin(angle) * dist
185
+
186
+    group.position.x = state.emergeX
187
+    group.position.z = state.emergeZ
188
+    group.rotation.y = angle + Math.PI / 2 // Face outward-ish
189
+  }
190
+
191
+  function update(delta, elapsed, pond) {
192
+    state.timer += delta
193
+
194
+    switch (state.mode) {
195
+      case 'waiting':
196
+        if (state.timer >= 60) {
197
+          startRumble(pond)
198
+        }
199
+        break
200
+
201
+      case 'rumbling':
202
+        // Create rumble ripples
203
+        if (state.timer < 2) {
204
+          if (Math.random() < delta * 8) {
205
+            const rx = state.emergeX + (Math.random() - 0.5) * 0.8
206
+            const rz = state.emergeZ + (Math.random() - 0.5) * 0.8
207
+            pond.addRipple(rx, rz)
208
+          }
209
+        } else {
210
+          state.mode = 'emerging'
211
+          state.timer = 0
212
+          group.visible = true
213
+          group.position.y = -2
214
+        }
215
+        break
216
+
217
+      case 'emerging':
218
+        // Rise from the water
219
+        const emergeProgress = Math.min(state.timer / 1.5, 1)
220
+        const easeOut = 1 - Math.pow(1 - emergeProgress, 3)
221
+        group.position.y = -2 + easeOut * 2.3 // Rise to 0.3 above water
222
+
223
+        // Gentle rocking as emerging
224
+        group.rotation.z = Math.sin(state.timer * 4) * 0.1
225
+
226
+        if (emergeProgress >= 1) {
227
+          state.mode = 'surfaced'
228
+          state.timer = 0
229
+          state.surfaceTime = 4 + Math.random() * 3 // Stay 4-7 seconds
230
+        }
231
+        break
232
+
233
+      case 'surfaced':
234
+        // Bob gently on the surface
235
+        group.position.y = 0.3 + Math.sin(elapsed * 2) * 0.08
236
+        group.rotation.z = Math.sin(elapsed * 1.5) * 0.05
237
+
238
+        // Gentle flipper animation
239
+        leftFlipper.rotation.z = 2.2 + Math.sin(elapsed * 3) * 0.15
240
+        rightFlipper.rotation.z = 2.2 + Math.sin(elapsed * 3 + 0.5) * 0.15
241
+
242
+        // Occasional ripples
243
+        if (Math.random() < delta * 0.5) {
244
+          pond.addRipple(
245
+            group.position.x + (Math.random() - 0.5) * 0.5,
246
+            group.position.z + (Math.random() - 0.5) * 0.5
247
+          )
248
+        }
249
+
250
+        if (state.timer >= state.surfaceTime) {
251
+          state.mode = 'submerging'
252
+          state.timer = 0
253
+        }
254
+        break
255
+
256
+      case 'submerging':
257
+        // Sink back down
258
+        const submergeProgress = Math.min(state.timer / 1.2, 1)
259
+        const easeIn = Math.pow(submergeProgress, 2)
260
+        group.position.y = 0.3 - easeIn * 2.5
261
+
262
+        // Add bubbles/ripples as submerging
263
+        if (Math.random() < delta * 4) {
264
+          pond.addRipple(
265
+            group.position.x + (Math.random() - 0.5) * 0.6,
266
+            group.position.z + (Math.random() - 0.5) * 0.6
267
+          )
268
+        }
269
+
270
+        if (submergeProgress >= 1) {
271
+          state.mode = 'waiting'
272
+          state.timer = 0
273
+          group.visible = false
274
+          group.position.y = -3
275
+        }
276
+        break
277
+    }
278
+  }
279
+
280
+  return {
281
+    group,
282
+    update
283
+  }
284
+}