JavaScript · 10540 bytes Raw Blame History
1 // Ollie the Octopus - curious inspector of the pond
2 import * as THREE from 'three'
3
4 export function createOllie(scene, gradientMap) {
5 const group = new THREE.Group()
6
7 // Color palette - purple theme
8 const bodyColor = 0x7b4b94 // Deep purple
9 const bellyColor = 0xb89bc9 // Lighter lavender
10 const suckerColor = 0xd4a5c9 // Pink-ish
11 const glassRimColor = 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 suckerMaterial = new THREE.MeshToonMaterial({
25 color: suckerColor,
26 gradientMap: gradientMap
27 })
28
29 const glassRimMaterial = new THREE.MeshToonMaterial({
30 color: glassRimColor,
31 gradientMap: gradientMap
32 })
33
34 const glassMaterial = new THREE.MeshBasicMaterial({
35 color: 0x88ccff,
36 transparent: true,
37 opacity: 0.3
38 })
39
40 // Head/Mantle - bulbous dome
41 const mantleGeom = new THREE.SphereGeometry(0.5, 10, 8)
42 mantleGeom.scale(1.2, 1.4, 1.0)
43 const mantle = new THREE.Mesh(mantleGeom, bodyMaterial)
44 mantle.position.y = 0.3
45 group.add(mantle)
46
47 // Lower mantle/body connector
48 const lowerMantleGeom = new THREE.SphereGeometry(0.4, 8, 6)
49 lowerMantleGeom.scale(1.1, 0.8, 1.0)
50 const lowerMantle = new THREE.Mesh(lowerMantleGeom, bellyMaterial)
51 lowerMantle.position.y = -0.1
52 group.add(lowerMantle)
53
54 // Eyes - big and expressive Wind Waker style
55 const eyeGeom = new THREE.SphereGeometry(0.12, 8, 6)
56 const eyeWhiteMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
57 const pupilMaterial = new THREE.MeshBasicMaterial({ color: 0x1a1a1a })
58
59 // Left eye
60 const leftEyeWhite = new THREE.Mesh(eyeGeom, eyeWhiteMaterial)
61 leftEyeWhite.position.set(0.25, 0.35, 0.35)
62 leftEyeWhite.scale.set(1, 1.2, 0.8)
63 group.add(leftEyeWhite)
64
65 const leftPupilGeom = new THREE.SphereGeometry(0.06, 6, 4)
66 const leftPupil = new THREE.Mesh(leftPupilGeom, pupilMaterial)
67 leftPupil.position.set(0.32, 0.35, 0.4)
68 group.add(leftPupil)
69
70 // Right eye
71 const rightEyeWhite = new THREE.Mesh(eyeGeom, eyeWhiteMaterial)
72 rightEyeWhite.position.set(0.25, 0.35, -0.35)
73 rightEyeWhite.scale.set(1, 1.2, 0.8)
74 group.add(rightEyeWhite)
75
76 const rightPupil = new THREE.Mesh(leftPupilGeom, pupilMaterial)
77 rightPupil.position.set(0.32, 0.35, -0.4)
78 group.add(rightPupil)
79
80 // Eye shines
81 const shineGeom = new THREE.SphereGeometry(0.03, 6, 4)
82 const shineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
83
84 const leftShine = new THREE.Mesh(shineGeom, shineMaterial)
85 leftShine.position.set(0.35, 0.4, 0.38)
86 group.add(leftShine)
87
88 const rightShine = new THREE.Mesh(shineGeom, shineMaterial)
89 rightShine.position.set(0.35, 0.4, -0.38)
90 group.add(rightShine)
91
92 // Create 8 tentacles
93 const tentacles = []
94 const tentacleGroup = new THREE.Group()
95 tentacleGroup.position.y = -0.3
96
97 for (let i = 0; i < 8; i++) {
98 const angle = (i / 8) * Math.PI * 2
99 const tentacle = createTentacle(bodyMaterial, suckerMaterial, gradientMap)
100 tentacle.position.x = Math.cos(angle) * 0.35
101 tentacle.position.z = Math.sin(angle) * 0.35
102 tentacle.rotation.y = -angle + Math.PI / 2
103 // Splay outward slightly
104 tentacle.rotation.z = 0.3
105 tentacles.push(tentacle)
106 tentacleGroup.add(tentacle)
107 }
108
109 group.add(tentacleGroup)
110
111 // Magnifying glass - attached to front-right tentacle (index 1)
112 const magGlassGroup = new THREE.Group()
113
114 // Handle
115 const handleGeom = new THREE.CylinderGeometry(0.02, 0.025, 0.3, 6)
116 const handle = new THREE.Mesh(handleGeom, glassRimMaterial)
117 handle.rotation.z = Math.PI / 2
118 handle.position.x = -0.15
119 magGlassGroup.add(handle)
120
121 // Rim
122 const rimGeom = new THREE.TorusGeometry(0.15, 0.02, 8, 16)
123 const rim = new THREE.Mesh(rimGeom, glassRimMaterial)
124 magGlassGroup.add(rim)
125
126 // Glass lens
127 const lensGeom = new THREE.CircleGeometry(0.14, 16)
128 const lens = new THREE.Mesh(lensGeom, glassMaterial)
129 lens.position.z = 0.01
130 magGlassGroup.add(lens)
131
132 // Position magnifying glass at end of front-right tentacle
133 magGlassGroup.position.set(0.9, -0.5, -0.3)
134 magGlassGroup.rotation.y = -0.5
135 group.add(magGlassGroup)
136
137 // Ollie starts hidden below the water
138 group.position.y = -3
139 group.visible = false
140
141 scene.add(group)
142
143 // State
144 const state = {
145 mode: 'waiting',
146 timer: 45 + Math.random() * 30, // First appearance in 45-75 seconds (after narwhal)
147 emergeX: 0,
148 emergeZ: 0,
149 surfaceTime: 0
150 }
151
152 function createTentacle(bodyMat, suckerMat, gradient) {
153 const tentacleObj = new THREE.Group()
154
155 // 3 segments, getting smaller
156 const segments = [
157 { radius: 0.08, length: 0.35 },
158 { radius: 0.06, length: 0.3 },
159 { radius: 0.04, length: 0.25 }
160 ]
161
162 let yOffset = 0
163 segments.forEach((seg, idx) => {
164 const segGeom = new THREE.CylinderGeometry(seg.radius * 0.7, seg.radius, seg.length, 6)
165 const segMesh = new THREE.Mesh(segGeom, bodyMat)
166 segMesh.position.y = yOffset - seg.length / 2
167 tentacleObj.add(segMesh)
168
169 // Add suckers on underside (only first two segments)
170 if (idx < 2) {
171 for (let s = 0; s < 2; s++) {
172 const suckerGeom = new THREE.SphereGeometry(0.015, 4, 4)
173 const sucker = new THREE.Mesh(suckerGeom, suckerMat)
174 sucker.position.set(-seg.radius * 0.8, yOffset - seg.length * 0.3 - s * 0.12, 0)
175 sucker.scale.set(1, 0.5, 1)
176 tentacleObj.add(sucker)
177 }
178 }
179
180 yOffset -= seg.length
181 })
182
183 // Curly tip
184 const tipGeom = new THREE.SphereGeometry(0.03, 6, 4)
185 tipGeom.scale(1, 1.5, 1)
186 const tip = new THREE.Mesh(tipGeom, bodyMat)
187 tip.position.y = yOffset - 0.03
188 tentacleObj.add(tip)
189
190 return tentacleObj
191 }
192
193 function startRumble(pond) {
194 state.mode = 'rumbling'
195 state.timer = 0
196
197 // Pick random spot in outer zone of pond (70-90% radius)
198 const angle = Math.random() * Math.PI * 2
199 const dist = Math.random() * pond.radius * 0.2 + pond.radius * 0.7
200 state.emergeX = Math.cos(angle) * dist
201 state.emergeZ = Math.sin(angle) * dist
202
203 group.position.x = state.emergeX
204 group.position.z = state.emergeZ
205 group.rotation.y = angle + Math.PI / 2
206 }
207
208 // Helper to smoothly interpolate angles
209 function lerpAngle(from, to, t) {
210 let diff = to - from
211 while (diff > Math.PI) diff -= Math.PI * 2
212 while (diff < -Math.PI) diff += Math.PI * 2
213 return from + diff * t
214 }
215
216 function update(delta, elapsed, pond, doug) {
217 state.timer += delta
218
219 // Calculate angle to face Doug
220 let angleToDoug = 0
221 if (doug) {
222 const dougPos = doug.getPosition()
223 const dx = dougPos.x - group.position.x
224 const dz = dougPos.z - group.position.z
225 angleToDoug = Math.atan2(dx, dz)
226 }
227
228 // Animate tentacles (always, when visible)
229 if (group.visible) {
230 tentacles.forEach((t, i) => {
231 const phase = i * (Math.PI / 4)
232 // Wave motion
233 t.rotation.x = 0.3 + Math.sin(elapsed * 2 + phase) * 0.25
234 t.rotation.z = 0.3 + Math.cos(elapsed * 1.5 + phase) * 0.15
235 })
236
237 // Magnifying glass sway
238 magGlassGroup.rotation.z = Math.sin(elapsed * 3) * 0.15
239 magGlassGroup.rotation.x = Math.sin(elapsed * 2.5) * 0.1
240 }
241
242 switch (state.mode) {
243 case 'waiting':
244 if (state.timer >= 75) {
245 startRumble(pond)
246 }
247 break
248
249 case 'rumbling':
250 // Create rumble ripples
251 if (state.timer < 2) {
252 if (Math.random() < delta * 6) {
253 const rx = state.emergeX + (Math.random() - 0.5) * 1.0
254 const rz = state.emergeZ + (Math.random() - 0.5) * 1.0
255 pond.addRipple(rx, rz)
256 }
257 } else {
258 state.mode = 'emerging'
259 state.timer = 0
260 group.visible = true
261 group.position.y = -2
262 group.rotation.y = angleToDoug
263 }
264 break
265
266 case 'emerging':
267 const emergeProgress = Math.min(state.timer / 1.8, 1)
268 const easeOut = 1 - Math.pow(1 - emergeProgress, 3)
269 group.position.y = -2 + easeOut * 2.2
270
271 // Slowly turn toward Doug - curious inspection
272 group.rotation.y = lerpAngle(group.rotation.y, angleToDoug, delta * 0.6)
273
274 // Gentle wobble during emerge
275 group.rotation.x = Math.sin(state.timer * 3) * 0.05
276 group.rotation.z = Math.cos(state.timer * 2.5) * 0.04
277
278 if (emergeProgress >= 1) {
279 state.mode = 'surfaced'
280 state.timer = 0
281 state.surfaceTime = 5 + Math.random() * 3 // Stay 5-8 seconds
282 }
283 break
284
285 case 'surfaced':
286 // Bob gently
287 group.position.y = 0.2 + Math.sin(elapsed * 2) * 0.05
288
289 // Track Doug with magnifying glass - curious inspection!
290 group.rotation.y = lerpAngle(group.rotation.y, angleToDoug, delta * 0.4)
291
292 // Gentle rocking
293 group.rotation.x = Math.sin(elapsed * 1.2) * 0.03
294 group.rotation.z = Math.cos(elapsed * 1.0) * 0.02
295
296 // Extra curious magnifying glass wobble when pointed at Doug
297 magGlassGroup.rotation.y = -0.5 + Math.sin(elapsed * 4) * 0.1
298
299 // Occasional ripples
300 if (Math.random() < delta * 0.4) {
301 pond.addRipple(
302 group.position.x + (Math.random() - 0.5) * 0.6,
303 group.position.z + (Math.random() - 0.5) * 0.6
304 )
305 }
306
307 if (state.timer >= state.surfaceTime) {
308 state.mode = 'submerging'
309 state.timer = 0
310 }
311 break
312
313 case 'submerging':
314 const submergeProgress = Math.min(state.timer / 1.5, 1)
315 const easeIn = Math.pow(submergeProgress, 2)
316 group.position.y = 0.2 - easeIn * 2.5
317
318 // Tentacles curl inward as submerging
319 tentacles.forEach((t, i) => {
320 const phase = i * (Math.PI / 4)
321 t.rotation.x = 0.3 + easeIn * 0.5 + Math.sin(elapsed * 2 + phase) * 0.15
322 })
323
324 // Add ripples as submerging
325 if (Math.random() < delta * 5) {
326 pond.addRipple(
327 group.position.x + (Math.random() - 0.5) * 0.8,
328 group.position.z + (Math.random() - 0.5) * 0.8
329 )
330 }
331
332 if (submergeProgress >= 1) {
333 state.mode = 'waiting'
334 state.timer = 0
335 group.visible = false
336 group.position.y = -3
337 group.rotation.x = 0
338 group.rotation.z = 0
339 }
340 break
341 }
342 }
343
344 return {
345 group,
346 update
347 }
348 }