JavaScript · 59155 bytes Raw Blame History
1 // entities.js - All game entity classes
2
3 class Spider {
4 constructor (x, y) {
5 this.pos = createVector(x, y)
6 this.vel = createVector(0, 0)
7 this.acc = createVector(0, 0)
8 this.radius = 8
9 this.isAirborne = false
10 this.canJump = true
11 this.lastAnchorPoint = null
12 this.gravity = createVector(0, 0.3)
13 this.jumpPower = 12
14 this.maxSpeed = 15
15 this.munchRadius = 20
16 this.munchCooldown = 0
17 this.attachedObstacle = null // Track which obstacle spider is on
18 }
19
20 jump (targetX, targetY, chargeMultiplier = 1) {
21 if (!this.canJump || this.isAirborne) return
22
23 // DAWN PHASE: Check and consume stamina
24 if (gamePhase === 'DAWN') {
25 if (jumpStamina < jumpCost) {
26 // Not enough stamina to jump
27 isExhausted = true
28 return // Can't jump
29 }
30 // Consume stamina for jump
31 jumpStamina -= jumpCost
32 stats.totalJumps++
33 }
34
35 // PHASE 4B: Track wind jumps
36 if (windActive) {
37 stats.windJumps++
38 achievements.windRider.progress++
39 }
40
41 let direction = createVector(targetX - this.pos.x, targetY - this.pos.y)
42 let clickDistance = direction.mag()
43 direction.normalize()
44
45 // Apply charge multiplier if provided
46 let actualJumpPower = map(
47 clickDistance,
48 0,
49 200,
50 3,
51 this.jumpPower * chargeMultiplier
52 )
53 actualJumpPower = constrain(
54 actualJumpPower,
55 3,
56 this.jumpPower * chargeMultiplier
57 )
58 direction.mult(actualJumpPower)
59
60 this.vel = direction
61 this.isAirborne = true
62 this.canJump = false
63 this.lastAnchorPoint = this.pos.copy()
64 // Record jump time for touch debounce
65 if (typeof window !== 'undefined') {
66 window.lastJumpTime = millis()
67 }
68
69 // Check if we're jumping off a web strand
70 for (let strand of webStrands) {
71 if (strand === currentStrand) continue
72
73 if (this.checkStrandCollision(strand)) {
74 // Much simpler shimmy detection based on actual jump power used
75 let isShimmy = actualJumpPower < 6 // If we used less than half power, it's a shimmy
76
77 // Apply appropriate recoil based on movement type
78 if (isShimmy) {
79 // Trigger shimmy visual effect
80 this.shimmyEffect = 20
81
82 // NO recoil at all for shimmying - just tiny vibration
83 strand.vibrate(0.3)
84
85 // Tiny yellow particles
86 let p = new Particle(this.pos.x, this.pos.y)
87 p.color = color(255, 255, 100, 80)
88 p.vel = createVector(random(-0.3, 0.3), random(-0.3, 0.3))
89 p.size = 2
90 particles.push(p)
91 } else {
92 // Scale recoil based on actual jump power
93 let recoilForce = -(actualJumpPower / this.jumpPower) * 0.08 // Scale by power ratio
94 strand.applyRecoil(recoilForce)
95
96 // Create particles only for real jumps
97 for (let i = 0; i < 2; i++) {
98 let p = new Particle(this.pos.x, this.pos.y)
99 p.color = color(255, 255, 255, 120)
100 p.vel = createVector(random(-0.8, 0.8), random(1, 2))
101 p.size = 3
102 particles.push(p)
103 }
104 }
105
106 break
107 }
108 }
109 }
110
111 munch () {
112 if (this.munchCooldown > 0) return
113
114 isMunching = true
115 this.munchCooldown = 30
116
117 for (let i = flies.length - 1; i >= 0; i--) {
118 let fly = flies[i]
119 let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y)
120 if (d < this.munchRadius) {
121 fliesMunched++
122 webSilk = min(webSilk + 15, maxWebSilk)
123
124 for (let j = 0; j < 12; j++) {
125 let p = new Particle(fly.pos.x, fly.pos.y)
126 p.color = color(255, random(100, 255), 0)
127 particles.push(p)
128 }
129
130 flies.splice(i, 1)
131 break
132 }
133 }
134 }
135
136 update () {
137 // If attached to a moving obstacle, move with it
138 if (this.attachedObstacle && !this.isAirborne) {
139 // Calculate angle from obstacle center to spider
140 let angle = atan2(
141 this.pos.y - this.attachedObstacle.y,
142 this.pos.x - this.attachedObstacle.x
143 )
144 // Keep spider on the surface of the obstacle
145 this.pos.x =
146 this.attachedObstacle.x +
147 cos(angle) * (this.attachedObstacle.radius + this.radius)
148 this.pos.y =
149 this.attachedObstacle.y +
150 sin(angle) * (this.attachedObstacle.radius + this.radius)
151 }
152
153 if (this.isAirborne) {
154 this.acc.add(this.gravity)
155 this.attachedObstacle = null // Clear attachment when jumping
156 }
157
158 this.vel.add(this.acc)
159 this.vel.limit(this.maxSpeed)
160 this.pos.add(this.vel)
161 this.acc.mult(0)
162
163 if (this.munchCooldown > 0) {
164 this.munchCooldown--
165 if (this.munchCooldown === 0) {
166 isMunching = false
167 }
168 }
169
170 // Check ground collision
171 if (this.pos.y >= height - this.radius) {
172 this.pos.y = height - this.radius
173 this.land()
174 this.attachedObstacle = null
175 }
176
177 // Check wall collisions
178 if (this.pos.x <= this.radius || this.pos.x >= width - this.radius) {
179 this.pos.x = constrain(this.pos.x, this.radius, width - this.radius)
180 this.vel.x *= -0.5
181 }
182
183 // Check ceiling
184 if (this.pos.y <= this.radius) {
185 this.pos.y = this.radius
186 this.vel.y *= -0.5 // Bounce off ceiling, don't land
187 }
188
189 // Check home branch collision (one-way platform)
190 if (window.homeBranch && this.isAirborne && this.vel.y > 0.1) {
191 // Only when actually falling
192 let branch = window.homeBranch
193
194 // Check if spider is within branch X range
195 let branchStart = Math.min(branch.startX, branch.endX)
196 let branchEnd = Math.max(branch.startX, branch.endX)
197
198 // Since the branch angle is very small (0.05 radians ≈ 3 degrees),
199 // we can use a simpler approximation
200 if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
201 // Calculate position along branch (0 to 1)
202 let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
203 t = constrain(t, 0, 1)
204
205 // Branch visual thickness tapers from full at start to 35% at end
206 // This matches exactly how it's drawn in the bezier curves
207 let branchTopThickness = lerp(
208 branch.thickness * 0.9,
209 branch.thickness * 0.35,
210 t
211 )
212
213 // The branch is drawn centered at branch.y
214 // With small angle approximation: the top of the branch is at
215 let branchSurfaceY = branch.y - branchTopThickness
216
217 // Add slight angle correction (for small angles, tan ≈ sin ≈ angle in radians)
218 let angleCorrection = (this.pos.x - branchStart) * branch.angle
219 branchSurfaceY += angleCorrection
220
221 // Check if spider is crossing the branch from above
222 let prevY = this.pos.y - this.vel.y
223
224 if (
225 prevY <= branchSurfaceY && // Was above
226 this.pos.y + this.radius >= branchSurfaceY && // Now at or below
227 this.pos.y < branch.y + branch.thickness
228 ) {
229 // Not too far below
230
231 // Place spider on the branch surface
232 this.pos.y = branchSurfaceY - this.radius
233 this.land()
234 this.attachedObstacle = null
235 }
236 }
237 }
238
239 // Check obstacle collisions
240 for (let obstacle of obstacles) {
241 if (this.checkObstacleCollision(obstacle)) {
242 this.landOnObstacle(obstacle)
243 }
244 }
245
246 // Check web strand collisions
247 for (let strand of webStrands) {
248 if (strand === currentStrand) continue
249
250 if (this.isAirborne && this.checkStrandCollision(strand)) {
251 this.landOnStrand(strand)
252 }
253 }
254
255 // Check food box collisions
256 for (let i = foodBoxes.length - 1; i >= 0; i--) {
257 let box = foodBoxes[i]
258 if (
259 dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) <
260 this.radius + box.radius
261 ) {
262 box.collect()
263 foodBoxes.splice(i, 1)
264 }
265 }
266 }
267
268 checkObstacleCollision (obstacle) {
269 let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y)
270 return d < this.radius + obstacle.radius
271 }
272
273 checkStrandCollision (strand) {
274 if (!strand || !strand.start || !strand.end) return false
275 let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
276 return d < this.radius + 2
277 }
278
279 pointToLineDistance (point, lineStart, lineEnd) {
280 // Guard nulls
281 if (!lineStart || !lineEnd) {
282 return Infinity
283 }
284 let line = p5.Vector.sub(lineEnd, lineStart)
285 let lineLength = line.mag()
286 // If start and end coincide, distance is to the single point
287 if (lineLength === 0) {
288 return p5.Vector.dist(point, lineStart)
289 }
290 line.normalize()
291 let pointToStart = p5.Vector.sub(point, lineStart)
292 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
293 let closestPoint = p5.Vector.add(
294 lineStart,
295 p5.Vector.mult(line, projLength)
296 )
297 return p5.Vector.dist(point, closestPoint)
298 }
299
300 landOnObstacle (obstacle) {
301 // Only land if we're actually airborne
302 if (!this.isAirborne) return
303
304 let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
305 this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius)
306 this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius)
307 this.attachedObstacle = obstacle // Track which obstacle we're on
308 this.land()
309 }
310
311 landOnStrand (strand) {
312 // Only land if we're actually airborne
313 if (!this.isAirborne) return
314 if (!strand || !strand.start || !strand.end) return
315 let line = p5.Vector.sub(strand.end, strand.start)
316 let lineLength = line.mag()
317 if (lineLength === 0) {
318 // Degenerate strand; snap to start
319 this.pos = strand.start.copy
320 ? strand.start.copy()
321 : createVector(strand.start.x, strand.start.y)
322 } else {
323 line.normalize()
324 let pointToStart = p5.Vector.sub(this.pos, strand.start)
325 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
326 let closestPoint = p5.Vector.add(
327 strand.start,
328 p5.Vector.mult(line, projLength)
329 )
330 this.pos = closestPoint
331 }
332 this.attachedObstacle = null // Not on an obstacle
333 this.land()
334 }
335
336 land () {
337 this.vel.mult(0)
338 this.isAirborne = false
339 this.canJump = true
340
341 if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) {
342 // Ensure the strand has a valid end and a final node on landing
343 currentStrand.end = this.pos.copy()
344 if (!currentStrand.path || currentStrand.path.length === 0) {
345 currentStrand.path = [this.pos.copy()]
346 } else {
347 currentStrand.path.push(this.pos.copy())
348 }
349 webNodes.push(new WebNode(this.pos.x, this.pos.y))
350 }
351
352 currentStrand = null
353 isDeployingWeb = false
354 }
355
356 display () {
357 push()
358 translate(this.pos.x, this.pos.y)
359
360 if (isMunching && this.munchCooldown > 15) {
361 push()
362 fill(255, 100, 100, 150)
363 noStroke()
364 let munchSize = 15 + sin(frameCount * 0.5) * 5
365 arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE)
366 pop()
367 }
368
369 fill(20)
370 stroke(0)
371 strokeWeight(1)
372 ellipse(0, 0, this.radius * 2)
373
374 fill(40)
375 noStroke()
376 ellipse(0, -2, this.radius * 1.2, this.radius * 1.5)
377
378 if (gamePhase === 'NIGHT') {
379 fill(255, 100, 100)
380 } else {
381 fill(255, 0, 0)
382 }
383 ellipse(-3, -3, 3)
384 ellipse(3, -3, 3)
385
386 stroke(0)
387 strokeWeight(1.5)
388 for (let i = 0; i < 4; i++) {
389 let angle = PI / 6 + (i * PI) / 8
390 line(0, 0, cos(angle) * 12, sin(angle) * 8)
391 line(0, 0, -cos(angle) * 12, sin(angle) * 8)
392 }
393
394 if (webSilk < 20) {
395 fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50)
396 noStroke()
397 ellipse(0, -15, 8)
398 }
399
400 pop()
401 }
402 }
403
404 class Fly {
405 constructor () {
406 if (random() < 0.5) {
407 this.pos = createVector(
408 random() < 0.5 ? -20 : width + 20,
409 random(50, height - 100)
410 )
411 } else {
412 this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20)
413 }
414
415 this.vel = createVector(random(-2, 2), random(-1, 1))
416 this.acc = createVector(0, 0)
417 this.radius = 4
418 this.caught = false
419 this.stuck = false
420 this.wingPhase = random(TWO_PI)
421 this.wanderAngle = random(TWO_PI)
422 this.glowIntensity = random(150, 255)
423 this.touchedStrands = new Set()
424 this.slowedBy = new Set() // Track which strands are slowing us
425 this.baseSpeed = 3
426 this.currentSpeed = this.baseSpeed
427 }
428
429 update () {
430 if (this.stuck) {
431 // If stuck, check if we need to move with a drifting web
432 this.updatePositionOnWeb()
433 return
434 }
435
436 if (this.caught) {
437 this.vel.mult(0.95)
438 if (this.vel.mag() < 0.1) {
439 this.stuck = true
440 fliesCaught++
441 webSilk = min(webSilk + 5, maxWebSilk)
442 }
443 // While caught but not yet stuck, also follow the web
444 this.updatePositionOnWeb()
445 return
446 }
447
448 this.wanderAngle += random(-0.3, 0.3)
449 let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle))
450 wanderForce.mult(0.1)
451 this.acc.add(wanderForce)
452
453 // Apply current speed (which may be slowed)
454 this.vel.add(this.acc)
455 this.vel.limit(this.currentSpeed)
456 this.pos.add(this.vel)
457 this.acc.mult(0)
458
459 if (this.pos.x < -30) this.pos.x = width + 30
460 if (this.pos.x > width + 30) this.pos.x = -30
461 if (this.pos.y < -30) this.pos.y = height + 30
462 if (this.pos.y > height + 30) this.pos.y = -30
463
464 // Check web collisions
465 this.checkWebCollisions()
466 }
467
468 updatePositionOnWeb () {
469 // Find the web strand(s) this fly is attached to
470 for (let strand of webStrands) {
471 if (strand.broken) continue
472
473 // Check if fly is on this strand
474 let closestPoint = null
475 let closestDistance = Infinity
476
477 if (strand.path && strand.path.length > 1) {
478 for (let i = 0; i < strand.path.length - 1; i++) {
479 let p1 = strand.path[i]
480 let p2 = strand.path[i + 1]
481
482 // Find closest point on this segment
483 let line = p5.Vector.sub(p2, p1)
484 let lineLength = line.mag()
485 if (lineLength === 0) continue
486 line.normalize()
487
488 let pointToStart = p5.Vector.sub(this.pos, p1)
489 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
490
491 let projPoint = p5.Vector.add(p1, p5.Vector.mult(line, projLength))
492 let d = p5.Vector.dist(this.pos, projPoint)
493
494 if (d < closestDistance && d < this.radius + 5) {
495 closestDistance = d
496 closestPoint = projPoint
497 }
498 }
499 }
500
501 // If we found a close point on this strand, stick to it
502 if (closestPoint) {
503 // Move fly to follow the strand's movement
504 this.pos.x = closestPoint.x
505 this.pos.y = closestPoint.y
506
507 // Add small vibration when on a moving web
508 if (strand.vibration > 0) {
509 this.pos.x += random(-1, 1) * strand.vibration * 0.1
510 this.pos.y += random(-1, 1) * strand.vibration * 0.1
511 }
512 }
513 }
514 }
515
516 checkWebCollisions () {
517 let currentlyTouching = new Set()
518
519 for (let strand of webStrands) {
520 let touching = false
521
522 // Check collision with strand path
523 if (strand.path && strand.path.length > 1) {
524 // OPTIMIZATION: Skip every other point for collision detection
525 for (let i = 0; i < strand.path.length - 1; i += 2) {
526 let p1 = strand.path[i]
527 let p2 = strand.path[Math.min(i + 1, strand.path.length - 1)]
528 let d = this.pointToLineDistance(this.pos, p1, p2)
529 if (d < this.radius + 3) {
530 touching = true
531 break
532 }
533 }
534 } else if (strand.start && strand.end) {
535 // Fallback for strands without path
536 let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
537 if (d < this.radius + 3) {
538 touching = true
539 }
540 }
541
542 if (touching) {
543 currentlyTouching.add(strand)
544
545 // If this is a new strand we're touching
546 if (!this.touchedStrands.has(strand)) {
547 this.touchedStrands.add(strand)
548
549 // Vibrate the web when first touching
550 strand.vibrate(3)
551
552 // First strand slows us down
553 if (this.touchedStrands.size === 1) {
554 this.currentSpeed = this.baseSpeed * 0.4 // Slow to 40% speed
555 this.slowedBy.add(strand)
556
557 // Visual feedback - yellow particles for slowing
558 // LIMIT PARTICLES TO PREVENT FREEZE
559 let particleCount = Math.min(3, 100 - particles.length)
560 for (let j = 0; j < particleCount; j++) {
561 let p = new Particle(this.pos.x, this.pos.y)
562 p.color = color(255, 255, 0, 150)
563 p.vel = createVector(random(-1, 1), random(-1, 1))
564 p.size = 3
565 particles.push(p)
566 }
567 }
568 // Second strand catches us
569 else if (this.touchedStrands.size >= 2 && !this.caught) {
570 this.caught = true
571 this.currentSpeed = 0
572
573 // Stronger vibration when caught
574 strand.vibrate(8)
575
576 // FIX: OPTIMIZE NEARBY STRAND VIBRATION
577 // This is likely the main cause of the freeze - checking distances between all strands
578 // Use a more efficient method
579 propagateVibration(strand, 2)
580
581 // Create caught particles - LIMIT TO PREVENT FREEZE
582 let particleCount = Math.min(6, 100 - particles.length)
583 for (let j = 0; j < particleCount; j++) {
584 let p = new Particle(this.pos.x, this.pos.y)
585 p.color = color(255, 200, 0, 200)
586 p.vel = createVector(random(-2, 2), random(-2, 2))
587 particles.push(p)
588 }
589 }
590 }
591 }
592 }
593
594 // If we're no longer touching strands we were slowed by, speed back up
595 if (this.slowedBy.size > 0 && currentlyTouching.size === 0) {
596 this.currentSpeed = this.baseSpeed
597 this.slowedBy.clear()
598 }
599 }
600
601 pointToLineDistance (point, lineStart, lineEnd) {
602 // Add null checks
603 if (!point || !lineStart || !lineEnd) return Infinity
604
605 let dx = lineEnd.x - lineStart.x
606 let dy = lineEnd.y - lineStart.y
607 let lineLength = sqrt(dx * dx + dy * dy)
608
609 // If line has no length, return distance to point
610 if (lineLength < 0.01) {
611 return dist(point.x, point.y, lineStart.x, lineStart.y)
612 }
613
614 // Use optimized calculation without creating new vectors
615 let t =
616 ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) /
617 (lineLength * lineLength)
618 t = constrain(t, 0, 1)
619
620 let closestX = lineStart.x + t * dx
621 let closestY = lineStart.y + t * dy
622
623 return dist(point.x, point.y, closestX, closestY)
624 }
625
626 display () {
627 push()
628 translate(this.pos.x, this.pos.y)
629
630 // Show slowdown effect
631 if (this.slowedBy.size > 0 && !this.caught) {
632 stroke(255, 255, 0, 100)
633 strokeWeight(1)
634 noFill()
635 ellipse(0, 0, 20)
636 }
637
638 if (gamePhase === 'NIGHT') {
639 noStroke()
640 fill(255, 255, 150, this.glowIntensity * 0.3)
641 ellipse(0, 0, 30)
642 fill(255, 255, 100, this.glowIntensity * 0.5)
643 ellipse(0, 0, 20)
644 }
645
646 fill(30)
647 stroke(0)
648 strokeWeight(0.5)
649 ellipse(0, 0, this.radius * 2)
650
651 if (!this.stuck) {
652 // Wing animation slows down when slowed
653 let wingSpeed = this.slowedBy.size > 0 ? 0.25 : 0.5
654 this.wingPhase += wingSpeed
655 let wingSpread = sin(this.wingPhase) * 5
656
657 fill(255, 255, 255, 150)
658 noStroke()
659 ellipse(-wingSpread, 0, 6, 4)
660 ellipse(wingSpread, 0, 6, 4)
661 }
662
663 if (gamePhase === 'NIGHT') {
664 fill(255, 255, 100, this.glowIntensity)
665 noStroke()
666 ellipse(0, 2, 3)
667 }
668
669 pop()
670 }
671 }
672
673 class Obstacle {
674 constructor (x, y, radius, type) {
675 // Store original position for drift tracking
676 this.originalX = x
677 this.originalY = y
678 this.x = x
679 this.y = y
680 this.radius = radius
681 this.type = type || 'leaf'
682 this.rotation = random(TWO_PI)
683 this.leafPoints = []
684
685 // Movement properties for all types
686 this.bobOffset = random(TWO_PI)
687 this.bobSpeed = random(0.02, 0.04)
688 this.bobAmount = 0
689
690 // Type-specific initialization
691 if (this.type === 'balloon') {
692 this.bobAmount = 8 // Balloons bob more
693 this.balloonColors = [
694 color(255, 100, 100), // Red
695 color(100, 200, 255), // Blue
696 color(255, 200, 100) // Yellow
697 ]
698 this.balloonColor = random(this.balloonColors)
699 this.stringWave = 0
700 this.antLegPhase = random(TWO_PI)
701 } else if (this.type === 'beetle') {
702 this.bobAmount = 4
703 this.driftSpeed = random(0.15, 0.35)
704 this.driftAngle = random(TWO_PI)
705 this.driftChangeRate = random(0.005, 0.015)
706 this.wingPhase = random(TWO_PI)
707 this.beetleColor =
708 random() < 0.5
709 ? color(20, 60, 20) // Dark green
710 : color(40, 20, 60) // Purple
711 this.driftDistance = 0 // Track total drift
712 } else if (this.type === 'leaf') {
713 this.bobAmount = 2 // Leaves bob slightly
714 let numPoints = 8
715 for (let i = 0; i < numPoints; i++) {
716 let angle = (TWO_PI / numPoints) * i
717 let r = radius * random(0.7, 1.2)
718 if (i === 0 || i === numPoints / 2) r = radius * 1.3
719 this.leafPoints.push({ angle: angle, radius: r })
720 }
721 } else if (this.type === 'branch') {
722 // Keep for backwards compatibility
723 this.bobAmount = 0
724 }
725 }
726
727 update () {
728 // Bobbing motion for all types
729 let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount
730 this.y = this.originalY + bob
731
732 // Beetle-specific drift
733 if (this.type === 'beetle') {
734 // Store initial position if not set
735 if (!this.initialX) {
736 this.initialX = this.x
737 this.initialY = this.y
738 }
739
740 // Slowly change drift direction using Perlin noise
741 this.driftAngle +=
742 (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) -
743 0.5) *
744 0.1
745
746 // Apply drift to original position
747 this.originalX += cos(this.driftAngle) * this.driftSpeed
748 this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5
749
750 // Calculate total drift distance from initial position
751 this.driftDistance = dist(
752 this.originalX,
753 this.originalY,
754 this.initialX,
755 this.initialY
756 )
757
758 // Keep beetles on screen with soft boundaries
759 if (this.originalX < 80) {
760 this.driftAngle = random(-PI / 4, PI / 4)
761 this.originalX = 80
762 }
763 if (this.originalX > width - 80) {
764 this.driftAngle = random((3 * PI) / 4, (5 * PI) / 4)
765 this.originalX = width - 80
766 }
767 if (this.originalY < 80) {
768 this.driftAngle = random((-3 * PI) / 4, -PI / 4)
769 this.originalY = 80
770 }
771 if (this.originalY > height - 150) {
772 this.driftAngle = random(PI / 4, (3 * PI) / 4)
773 this.originalY = height - 150
774 }
775
776 // Update actual position (with bob already applied to y)
777 this.x = this.originalX
778
779 // Check if beetle has drifted too far and break attached strands
780 if (this.driftDistance > 100) {
781 this.breakAttachedStrands()
782 }
783 }
784
785 // Update animation phases
786 if (this.type === 'balloon') {
787 this.stringWave = sin(frameCount * 0.05 + this.bobOffset) * 0.1
788 this.antLegPhase += 0.1
789 } else if (this.type === 'beetle') {
790 this.wingPhase += 0.15
791 }
792
793 // For all moving obstacles, update any attached web strands
794 if (this.bobAmount > 0 || this.type === 'beetle') {
795 this.updateAttachedStrands()
796 }
797 }
798
799 updateAttachedStrands () {
800 // Update web strands that are connected to this obstacle
801 for (let strand of webStrands) {
802 // Check if strand starts at this obstacle
803 if (
804 dist(strand.start.x, strand.start.y, this.x, this.y) <
805 this.radius + 10
806 ) {
807 strand.start.x = this.x
808 strand.start.y = this.y
809 if (strand.path && strand.path.length > 0) {
810 strand.path[0].x = this.x
811 strand.path[0].y = this.y
812 }
813 }
814
815 // Check if strand ends at this obstacle
816 if (
817 strand.end &&
818 dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10
819 ) {
820 strand.end.x = this.x
821 strand.end.y = this.y
822 if (strand.path && strand.path.length > 0) {
823 strand.path[strand.path.length - 1].x = this.x
824 strand.path[strand.path.length - 1].y = this.y
825 }
826 }
827 }
828 }
829
830 breakAttachedStrands () {
831 // Break any strands attached to this beetle that has drifted too far
832 for (let strand of webStrands) {
833 let attachedToStart =
834 dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10
835 let attachedToEnd =
836 strand.end &&
837 dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10
838
839 if (attachedToStart || attachedToEnd) {
840 // Mark strand as broken
841 strand.broken = true
842
843 // Release any flies stuck to this strand
844 for (let fly of flies) {
845 if (fly.stuck || fly.caught) {
846 // Check if fly is touching this breaking strand
847 let touchingStrand = false
848 if (strand.path && strand.path.length > 1) {
849 for (let k = 0; k < strand.path.length - 1; k++) {
850 let p1 = strand.path[k]
851 let p2 = strand.path[k + 1]
852 let d = fly.pointToLineDistance(fly.pos, p1, p2)
853 if (d < fly.radius + 5) {
854 touchingStrand = true
855 break
856 }
857 }
858 }
859
860 // If fly was on this strand, release it
861 if (touchingStrand) {
862 fly.stuck = false
863 fly.caught = false
864 fly.currentSpeed = fly.baseSpeed
865 fly.touchedStrands.clear()
866 fly.slowedBy.clear()
867 // Give it a little downward velocity to start falling
868 fly.vel = createVector(random(-0.5, 0.5), 2)
869
870 // Create release particles
871 for (let j = 0; j < 3; j++) {
872 let p = new Particle(fly.pos.x, fly.pos.y)
873 p.color = color(255, 255, 100, 150)
874 p.vel = createVector(random(-1, 1), random(0, 2))
875 p.size = 2
876 particles.push(p)
877 }
878 }
879 }
880 }
881
882 // Create dramatic snap particles
883 let snapX = attachedToStart ? strand.start.x : strand.end.x
884 let snapY = attachedToStart ? strand.start.y : strand.end.y
885
886 // Red/pink particles for the snap
887 for (let i = 0; i < 8; i++) {
888 let p = new Particle(snapX, snapY)
889 p.color = color(255, random(100, 200), random(100, 150))
890 p.vel = createVector(random(-5, 5), random(-5, 2))
891 p.size = random(4, 8)
892 particles.push(p)
893 }
894
895 // White strand particles
896 for (let i = 0; i < 4; i++) {
897 let p = new Particle(snapX, snapY)
898 p.color = color(255, 255, 255)
899 p.vel = createVector(random(-3, 3), random(-3, 0))
900 p.size = 3
901 particles.push(p)
902 }
903
904 // Reset beetle drift after breaking strands
905 this.initialX = this.x
906 this.initialY = this.y
907 this.driftDistance = 0
908 }
909 }
910 }
911
912 display () {
913 push()
914 translate(this.x, this.y)
915
916 if (this.type === 'balloon') {
917 // Hot air balloon with canvas texture!
918 push()
919
920 // String/rope first (behind balloon)
921 stroke(80, 60, 40)
922 strokeWeight(1.5)
923 noFill()
924 beginShape()
925 for (let i = 0; i <= 10; i++) {
926 let t = i / 10
927 let stringX = sin(t * PI * 2 + this.stringWave) * 3
928 let stringY = t * 40 + this.radius
929 curveVertex(stringX, stringY)
930 }
931 endShape()
932
933 // Balloon shadow
934 noStroke()
935 fill(0, 0, 0, 30)
936 ellipse(5, 5, this.radius * 2.2, this.radius * 2.5)
937
938 // Main balloon with canvas panels
939 push()
940 // Draw vertical panels for that classic hot air balloon look
941 let numPanels = 8
942 for (let i = 0; i < numPanels; i++) {
943 let angle1 = (TWO_PI / numPanels) * i
944 let angle2 = (TWO_PI / numPanels) * (i + 1)
945
946 // Alternate panel colors for striped effect
947 if (i % 2 === 0) {
948 fill(
949 red(this.balloonColor),
950 green(this.balloonColor),
951 blue(this.balloonColor),
952 200
953 )
954 } else {
955 fill(
956 red(this.balloonColor) - 30,
957 green(this.balloonColor) - 30,
958 blue(this.balloonColor) - 30,
959 200
960 )
961 }
962
963 // Draw tapered panel (wider at middle, narrow at top/bottom)
964 beginShape()
965 // Top point
966 vertex(0, -this.radius * 1.2)
967 // Upper curve
968 bezierVertex(
969 cos(angle1) * this.radius * 0.3,
970 -this.radius * 0.9,
971 cos(angle1) * this.radius * 0.8,
972 -this.radius * 0.3,
973 cos(angle1) * this.radius * 1.1,
974 0
975 )
976 // Lower curve to bottom
977 bezierVertex(
978 cos(angle1) * this.radius * 0.9,
979 this.radius * 0.5,
980 cos(angle1) * this.radius * 0.4,
981 this.radius * 0.9,
982 0,
983 this.radius * 1.1
984 )
985 // Back up the other side
986 bezierVertex(
987 cos(angle2) * this.radius * 0.4,
988 this.radius * 0.9,
989 cos(angle2) * this.radius * 0.9,
990 this.radius * 0.5,
991 cos(angle2) * this.radius * 1.1,
992 0
993 )
994 bezierVertex(
995 cos(angle2) * this.radius * 0.8,
996 -this.radius * 0.3,
997 cos(angle2) * this.radius * 0.3,
998 -this.radius * 0.9,
999 0,
1000 -this.radius * 1.2
1001 )
1002 endShape(CLOSE)
1003 }
1004
1005 // Panel seams/ropes
1006 stroke(60, 40, 20, 100)
1007 strokeWeight(0.5)
1008 for (let i = 0; i < numPanels; i++) {
1009 let angle = (TWO_PI / numPanels) * i
1010 // Vertical seam lines
1011 beginShape()
1012 noFill()
1013 vertex(0, -this.radius * 1.2)
1014 bezierVertex(
1015 cos(angle) * this.radius * 0.3,
1016 -this.radius * 0.9,
1017 cos(angle) * this.radius * 0.8,
1018 -this.radius * 0.3,
1019 cos(angle) * this.radius * 1.1,
1020 0
1021 )
1022 bezierVertex(
1023 cos(angle) * this.radius * 0.9,
1024 this.radius * 0.5,
1025 cos(angle) * this.radius * 0.4,
1026 this.radius * 0.9,
1027 0,
1028 this.radius * 1.1
1029 )
1030 endShape()
1031 }
1032
1033 // Highlight on balloon
1034 noStroke()
1035 fill(255, 255, 255, 80)
1036 ellipse(
1037 -this.radius * 0.3,
1038 -this.radius * 0.5,
1039 this.radius * 0.6,
1040 this.radius * 0.7
1041 )
1042 pop()
1043
1044 // FLAME EFFECT!
1045 push()
1046 translate(0, this.radius - 5)
1047 // Flame glow
1048 noStroke()
1049 fill(255, 200, 0, 40 + sin(frameCount * 0.3) * 20)
1050 ellipse(0, 0, 25, 25)
1051 fill(255, 150, 0, 60 + sin(frameCount * 0.4) * 30)
1052 ellipse(0, 0, 15, 18)
1053 // Flame itself
1054 fill(255, 200, 0)
1055 push()
1056 let flameHeight = 8 + sin(frameCount * 0.5) * 3
1057 translate(0, -2)
1058 beginShape()
1059 vertex(-3, 0)
1060 bezierVertex(
1061 -3,
1062 -flameHeight * 0.7,
1063 -1,
1064 -flameHeight,
1065 0,
1066 -flameHeight * 1.2
1067 )
1068 bezierVertex(1, -flameHeight, 3, -flameHeight * 0.7, 3, 0)
1069 endShape(CLOSE)
1070 fill(255, 255, 200)
1071 ellipse(0, -flameHeight * 0.5, 3, 4)
1072 pop()
1073 pop()
1074
1075 // Basket
1076 push()
1077 translate(0, this.radius + 10)
1078 fill(101, 67, 33)
1079 stroke(80, 50, 20)
1080 strokeWeight(1)
1081 // Woven basket shape
1082 beginShape()
1083 vertex(-8, 0)
1084 vertex(8, 0)
1085 vertex(6, 10)
1086 vertex(-6, 10)
1087 endShape(CLOSE)
1088 // Basket weave pattern
1089 stroke(80, 50, 20, 150)
1090 for (let i = -6; i < 6; i += 2) {
1091 line(i, 1, i, 9)
1092 }
1093 for (let i = 2; i < 9; i += 2) {
1094 line(-6, i, 6, i)
1095 }
1096 // Basket rim
1097 stroke(60, 40, 20)
1098 strokeWeight(1.5)
1099 line(-8, 0, 8, 0)
1100 pop()
1101
1102 // Ant in basket (peeking over edge)
1103 push()
1104 translate(0, this.radius + 12)
1105 fill(20)
1106 noStroke()
1107 // Just ant head and antennae visible
1108 ellipse(0, -2, 6, 4) // Head peeking up
1109 // Antennae
1110 stroke(20)
1111 strokeWeight(0.5)
1112 line(-1, -3, -3, -6)
1113 line(1, -3, 3, -6)
1114 // Tiny ant arms gripping basket edge
1115 strokeWeight(1)
1116 line(-3, 0, -4, 2)
1117 line(3, 0, 4, 2)
1118 pop()
1119
1120 pop()
1121 } else if (this.type === 'beetle') {
1122 // Big floating beetle!
1123 push()
1124 rotate(this.rotation)
1125
1126 // Shadow
1127 noStroke()
1128 fill(0, 0, 0, 40)
1129 ellipse(3, 3, this.radius * 1.8, this.radius * 2.2)
1130
1131 // Wings - always visible and flapping since they're floating
1132 push()
1133 // Wing flap animation
1134 let wingAngle = sin(this.wingPhase) * 0.3
1135 let wingSpread = 15 + sin(this.wingPhase) * 10
1136
1137 // Left wing
1138 push()
1139 translate(-this.radius * 0.4, 0)
1140 rotate(-wingAngle)
1141 fill(255, 255, 255, 120)
1142 stroke(0, 0, 0, 100)
1143 strokeWeight(0.5)
1144 ellipse(-wingSpread * 0.7, 0, wingSpread * 1.2, 15)
1145 // Wing details
1146 noStroke()
1147 fill(200, 200, 200, 80)
1148 ellipse(-wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1149 pop()
1150
1151 // Right wing
1152 push()
1153 translate(this.radius * 0.4, 0)
1154 rotate(wingAngle)
1155 fill(255, 255, 255, 120)
1156 stroke(0, 0, 0, 100)
1157 strokeWeight(0.5)
1158 ellipse(wingSpread * 0.7, 0, wingSpread * 1.2, 15)
1159 // Wing details
1160 noStroke()
1161 fill(200, 200, 200, 80)
1162 ellipse(wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1163 pop()
1164
1165 // Extra glow at night
1166 if (gamePhase === 'NIGHT') {
1167 noStroke()
1168 fill(255, 255, 200, 30 + sin(this.wingPhase * 2) * 20)
1169 ellipse(0, 0, this.radius * 3, this.radius * 2)
1170 }
1171 pop()
1172
1173 // Main beetle body (on top of wings)
1174 fill(
1175 red(this.beetleColor),
1176 green(this.beetleColor),
1177 blue(this.beetleColor)
1178 )
1179 stroke(0)
1180 strokeWeight(2)
1181 ellipse(0, 0, this.radius * 1.6, this.radius * 2)
1182
1183 // Shell split line
1184 stroke(0)
1185 strokeWeight(1)
1186 line(0, -this.radius, 0, this.radius)
1187
1188 // Head
1189 fill(10)
1190 ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6)
1191
1192 // Spots/pattern
1193 noStroke()
1194 fill(0, 0, 0, 80)
1195 ellipse(-this.radius * 0.3, 0, this.radius * 0.4)
1196 ellipse(this.radius * 0.3, -this.radius * 0.2, this.radius * 0.3)
1197 ellipse(this.radius * 0.2, this.radius * 0.4, this.radius * 0.35)
1198 ellipse(-this.radius * 0.25, this.radius * 0.3, this.radius * 0.25)
1199
1200 // No legs - they're flying!
1201 // Just small leg stubs tucked under the body
1202 stroke(0)
1203 strokeWeight(1)
1204 // Tiny tucked legs
1205 line(
1206 -this.radius * 0.5,
1207 -this.radius * 0.2,
1208 -this.radius * 0.6,
1209 -this.radius * 0.1
1210 )
1211 line(
1212 this.radius * 0.5,
1213 -this.radius * 0.2,
1214 this.radius * 0.6,
1215 -this.radius * 0.1
1216 )
1217 line(
1218 -this.radius * 0.5,
1219 this.radius * 0.2,
1220 -this.radius * 0.6,
1221 this.radius * 0.1
1222 )
1223 line(
1224 this.radius * 0.5,
1225 this.radius * 0.2,
1226 this.radius * 0.6,
1227 this.radius * 0.1
1228 )
1229
1230 // Antennae
1231 strokeWeight(1)
1232 line(-3, -this.radius * 1.1, -8, -this.radius * 1.4)
1233 line(3, -this.radius * 1.1, 8, -this.radius * 1.4)
1234
1235 // Eyes (bigger and more prominent)
1236 fill(255, 0, 0)
1237 noStroke()
1238 ellipse(-5, -this.radius * 0.7, 5)
1239 ellipse(5, -this.radius * 0.7, 5)
1240 // Eye shine
1241 fill(255, 150, 150)
1242 ellipse(-4, -this.radius * 0.72, 2)
1243 ellipse(6, -this.radius * 0.72, 2)
1244
1245 pop()
1246 } else if (this.type === 'leaf') {
1247 // Original leaf code
1248 rotate(this.rotation)
1249
1250 if (gamePhase === 'NIGHT') {
1251 fill(20, 40, 20)
1252 stroke(10, 20, 10)
1253 } else {
1254 fill(34, 139, 34)
1255 stroke(25, 100, 25)
1256 }
1257 strokeWeight(2)
1258
1259 beginShape()
1260 for (let point of this.leafPoints) {
1261 let x = cos(point.angle) * point.radius
1262 let y = sin(point.angle) * point.radius
1263 curveVertex(x, y)
1264 }
1265 let firstPoint = this.leafPoints[0]
1266 curveVertex(
1267 cos(firstPoint.angle) * firstPoint.radius,
1268 sin(firstPoint.angle) * firstPoint.radius
1269 )
1270 let secondPoint = this.leafPoints[1]
1271 curveVertex(
1272 cos(secondPoint.angle) * secondPoint.radius,
1273 sin(secondPoint.angle) * secondPoint.radius
1274 )
1275 endShape()
1276
1277 stroke(25, 100, 25, 100)
1278 strokeWeight(1)
1279 line(0, -this.radius, 0, this.radius)
1280 line(0, 0, -this.radius / 2, -this.radius / 2)
1281 line(0, 0, this.radius / 2, -this.radius / 2)
1282 line(0, 0, -this.radius / 2, this.radius / 2)
1283 line(0, 0, this.radius / 2, this.radius / 2)
1284 } else if (this.type === 'branch') {
1285 // Keep old branch code for backwards compatibility
1286 rotate(this.rotation)
1287
1288 if (gamePhase === 'NIGHT') {
1289 stroke(40, 20, 0)
1290 fill(50, 25, 5)
1291 } else {
1292 stroke(101, 67, 33)
1293 fill(139, 90, 43)
1294 }
1295 strokeWeight(3)
1296
1297 push()
1298 strokeWeight(this.radius / 3)
1299 line(-this.radius, 0, this.radius, 0)
1300
1301 strokeWeight(2)
1302 line(-this.radius / 2, 0, -this.radius / 2 - 10, -10)
1303 line(this.radius / 3, 0, this.radius / 3 + 8, -8)
1304 line(0, 0, 5, -15)
1305
1306 stroke(80, 50, 20, 100)
1307 strokeWeight(1)
1308 for (let i = -this.radius; i < this.radius; i += 5) {
1309 line(i, -2, i + 2, 2)
1310 }
1311 pop()
1312
1313 noStroke()
1314 fill(255, 255, 255, 30)
1315 ellipse(0, 0, this.radius * 2)
1316 }
1317
1318 pop()
1319 }
1320 }
1321
1322 class FoodBox {
1323 constructor (x, y) {
1324 this.pos = createVector(x, y)
1325 this.radius = 10
1326 this.collected = false
1327 this.floatOffset = random(TWO_PI)
1328 this.silkValue = random(20, 35)
1329 this.glowPhase = random(TWO_PI)
1330 }
1331
1332 collect () {
1333 webSilk = min(webSilk + this.silkValue, maxWebSilk)
1334
1335 for (let i = 0; i < 8; i++) {
1336 particles.push(new Particle(this.pos.x, this.pos.y))
1337 }
1338 }
1339
1340 display () {
1341 push()
1342 let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3
1343 translate(this.pos.x, this.pos.y + floatY)
1344
1345 let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50
1346 noStroke()
1347 fill(255, 200, 100, glowIntensity * 0.3)
1348 ellipse(0, 0, 40)
1349 fill(255, 220, 150, glowIntensity * 0.5)
1350 ellipse(0, 0, 25)
1351
1352 rectMode(CENTER)
1353
1354 fill(0, 0, 0, 50)
1355 rect(2, 2, this.radius * 2, this.radius * 1.8, 3)
1356
1357 fill(139, 69, 19)
1358 stroke(100, 50, 0)
1359 strokeWeight(1)
1360 rect(0, 0, this.radius * 2, this.radius * 1.8, 3)
1361
1362 stroke(100, 50, 0)
1363 strokeWeight(1)
1364 line(-this.radius, 0, this.radius, 0)
1365 line(0, -this.radius * 0.9, 0, this.radius * 0.9)
1366
1367 noStroke()
1368 fill(255, 200, 100)
1369 ellipse(-5, -4, 4)
1370 ellipse(5, -4, 3)
1371 ellipse(-4, 5, 3)
1372 ellipse(4, 4, 4)
1373
1374 pop()
1375 }
1376 }
1377
1378 class Bird {
1379 constructor (pattern, isThief = false) {
1380 this.pattern = pattern // 'dive', 'swoop', 'glide', 'circle'
1381 this.isThief = isThief
1382 this.active = false
1383 this.attacking = false
1384 this.attackDelay = isThief ? 120 : random(30, 90) // MUCH shorter initial delay
1385
1386 // Position and movement
1387 this.x = random(width)
1388 this.y = -50 // Start above screen
1389 this.vx = 0
1390 this.vy = 0
1391 this.targetX = 0
1392 this.targetY = 0
1393 this.speed = 5 // Increased from 3
1394 this.angle = 0
1395 this.wingPhase = random(TWO_PI)
1396
1397 // Visual properties
1398 this.size = isThief ? 25 : 20
1399 this.color = isThief ? color(100, 50, 150) : color(50, 50, 50)
1400
1401 // Pattern-specific properties
1402 if (pattern === 'circle') {
1403 this.circleRadius = 150
1404 this.circleAngle = 0
1405 this.circleCenter = createVector(width / 2, height / 2)
1406 }
1407
1408 // Attack properties - MUCH MORE AGGRESSIVE
1409 this.diveSpeed = 12 // Increased from 8
1410 this.retreatSpeed = 6 // Increased from 4
1411 this.state = 'waiting' // 'waiting', 'approaching', 'attacking', 'retreating'
1412 this.consecutiveAttacks = 0 // Track multiple attacks
1413 this.maxConsecutiveAttacks = random(2, 4) // Each bird does 2-4 attacks before retreating
1414 }
1415
1416 update () {
1417 // Update wing animation
1418 this.wingPhase += 0.3 // Faster wing flapping
1419
1420 this.avoidObstacles()
1421
1422 // Countdown to attack - MUCH FASTER
1423 if (this.attackDelay > 0) {
1424 this.attackDelay--
1425 // Hover while waiting - more aggressive hovering
1426 this.y = -30 + sin(frameCount * 0.08) * 15
1427 this.x += sin(frameCount * 0.05) * 3
1428
1429 // Show warning when about to attack
1430 if (this.attackDelay < 30) {
1431 this.y = lerp(this.y, 50, 0.1) // Start moving into view
1432 }
1433 return
1434 }
1435
1436 // Activate after delay
1437 if (!this.active) {
1438 this.active = true
1439 this.state = 'approaching'
1440 this.updateTarget() // Set initial target
1441 }
1442
1443 // Execute movement pattern
1444 switch (this.pattern) {
1445 case 'dive':
1446 this.executeDivePattern()
1447 break
1448 case 'swoop':
1449 this.executeSwoopPattern()
1450 break
1451 case 'glide':
1452 this.executeGlidePattern()
1453 break
1454 case 'circle':
1455 this.executeCirclePattern()
1456 break
1457 }
1458
1459 // Check collisions
1460 this.checkCollisions()
1461
1462 // Keep on screen during approach
1463 if (this.state === 'approaching') {
1464 this.x = constrain(this.x, 20, width - 20)
1465 }
1466 }
1467
1468 updateTarget () {
1469 if (this.isThief) {
1470 // Target caught flies
1471 let caughtFlies = flies.filter(f => f.stuck || f.caught)
1472 if (caughtFlies.length > 0) {
1473 let target = random(caughtFlies)
1474 this.targetX = target.pos.x
1475 this.targetY = target.pos.y
1476 } else {
1477 this.active = false // No targets, deactivate
1478 return
1479 }
1480 } else {
1481 // Heavily favor targeting spider (90% chance)
1482 if (random() < 0.9) {
1483 // Target spider with prediction
1484 this.targetX = spider.pos.x + spider.vel.x * 10 // Predict where spider will be
1485 this.targetY = spider.pos.y + spider.vel.y * 10
1486 } else {
1487 // Occasionally target a web strand
1488 if (webStrands.length > 0) {
1489 let strand = random(webStrands.filter(s => !s.broken))
1490 if (strand && strand.path && strand.path.length > 0) {
1491 let point = random(strand.path)
1492 this.targetX = point.x
1493 this.targetY = point.y
1494 }
1495 }
1496 }
1497 }
1498 }
1499
1500 executeDivePattern () {
1501 if (this.state === 'approaching') {
1502 // Move into position above target
1503 let dx = this.targetX - this.x
1504 let dy = 50 - this.y
1505
1506 this.x += dx * 0.15
1507 this.y += dy * 0.15
1508
1509 // When in position, start diving
1510 if (abs(dx) < 50 && abs(dy) < 30) {
1511 this.state = 'attacking'
1512 this.attacking = true
1513 this.updateTarget()
1514 }
1515 } else if (this.state === 'attacking') {
1516 // FIX: Better tracking dive that reaches bottom
1517 let dx = this.targetX - this.x
1518 let dy = this.targetY - this.y
1519
1520 // Accelerate toward target with better tracking
1521 this.vx = dx * 0.1 // Better horizontal tracking
1522 this.vy = min(this.diveSpeed * 1.5, this.vy + 1.5) // Faster acceleration
1523
1524 this.x += this.vx
1525 this.y += this.vy
1526
1527 // Update target position while diving
1528 if (frameCount % 8 === 0) {
1529 this.targetX = spider.pos.x
1530 this.targetY = spider.pos.y
1531 }
1532
1533 // FIX: Extend dive range to reach bottom spiders
1534 // Check if we've reached the target OR the absolute bottom
1535 let reachedTarget = this.y > this.targetY - 10
1536 let reachedBottom = this.y > height - 20 // Go almost to canvas bottom
1537
1538 // FIX: Also check if we're very close horizontally for bottom edge spiders
1539 let closeToSpider = dist(this.x, this.y, spider.pos.x, spider.pos.y) < 50
1540
1541 if (reachedTarget || reachedBottom || closeToSpider) {
1542 // FIX: If spider is at bottom and we haven't hit it yet, do a horizontal sweep
1543 if (spider.pos.y > height - 30 && !closeToSpider && !this.sweeping) {
1544 this.sweeping = true
1545 this.y = spider.pos.y // Match spider height
1546 this.vy = 0 // Stop vertical movement
1547
1548 // Sweep horizontally toward spider
1549 let sweepDirection = spider.pos.x > this.x ? 1 : -1
1550 this.vx = sweepDirection * 8
1551
1552 // Continue sweep for a bit
1553 setTimeout(() => {
1554 this.sweeping = false
1555 this.state = 'retreating'
1556 this.attacking = false
1557 }, 500) // Sweep for 0.5 seconds
1558 } else if (!this.sweeping) {
1559 // Normal attack completion
1560 this.consecutiveAttacks++
1561
1562 if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1563 // Quick pull up and attack again
1564 this.state = 'approaching'
1565 this.attacking = false
1566 this.y = min(this.y, height - 50)
1567 this.updateTarget()
1568 } else {
1569 // Finally retreat
1570 this.state = 'retreating'
1571 this.attacking = false
1572 }
1573 }
1574 }
1575
1576 // Safety: Don't go below canvas
1577 if (this.y > height - 10) {
1578 this.y = height - 10
1579 if (!this.sweeping) {
1580 this.state = 'retreating'
1581 this.attacking = false
1582 }
1583 }
1584 } else if (this.state === 'retreating') {
1585 // Clear sweep flag
1586 this.sweeping = false
1587
1588 // Fly back up
1589 this.vy = -this.retreatSpeed
1590 this.y += this.vy
1591 this.x += sin(frameCount * 0.1) * 2
1592
1593 // Reset when off screen
1594 if (this.y < -50) {
1595 this.state = 'approaching'
1596 this.attackDelay = random(60, 120)
1597 this.x = random(width)
1598 this.consecutiveAttacks = 0
1599 this.maxConsecutiveAttacks = random(2, 4)
1600 }
1601 }
1602 }
1603
1604 executeSwoopPattern () {
1605 if (this.state === 'approaching') {
1606 if (this.x < 0) {
1607 this.x += 8
1608 this.y = height * 0.3 + sin(this.x * 0.03) * 50
1609 } else {
1610 this.state = 'attacking'
1611 this.attacking = true
1612 this.updateTarget()
1613 }
1614 } else if (this.state === 'attacking') {
1615 this.x += 9
1616
1617 // FIX: Adjust swoop pattern if spider is at bottom
1618 if (spider.pos.y > height - 50) {
1619 // Lower swoop pattern for bottom spiders
1620 this.y = height * 0.7 + sin(this.x * 0.03) * 50
1621 } else {
1622 // Normal swoop
1623 this.y = height * 0.3 + sin(this.x * 0.03) * 120
1624 }
1625
1626 // Track toward target when close
1627 if (abs(this.x - this.targetX) < 100) {
1628 let dy = this.targetY - this.y
1629 this.y += dy * 0.2
1630 }
1631
1632 // Avoid going below canvas
1633 this.y = min(this.y, height - 15)
1634
1635 // Exit screen
1636 if (this.x > width + 50) {
1637 this.consecutiveAttacks++
1638
1639 if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1640 this.x = -50
1641 this.state = 'approaching'
1642 this.updateTarget()
1643 } else {
1644 this.state = 'retreating'
1645 this.attacking = false
1646 }
1647 }
1648 } else if (this.state === 'retreating') {
1649 this.state = 'approaching'
1650 this.attackDelay = random(90, 150)
1651 this.x = -50
1652 this.consecutiveAttacks = 0
1653 this.maxConsecutiveAttacks = random(2, 4)
1654 }
1655 }
1656
1657 avoidObstacles () {
1658 // Check collision with all obstacles
1659 for (let obstacle of obstacles) {
1660 let d = dist(this.x, this.y, obstacle.x, obstacle.y)
1661
1662 // If too close to an obstacle, push away
1663 if (d < obstacle.radius + this.size + 10) {
1664 // Calculate push direction (away from obstacle)
1665 let pushX = (this.x - obstacle.x) / d
1666 let pushY = (this.y - obstacle.y) / d
1667
1668 // Apply push force
1669 this.x += pushX * 5
1670 this.y += pushY * 5
1671
1672 // If stuck for too long, teleport away
1673 if (this.stuckCounter === undefined) {
1674 this.stuckCounter = 0
1675 }
1676 this.stuckCounter++
1677
1678 if (this.stuckCounter > 30) {
1679 // Stuck for 0.5 seconds
1680 // Teleport to a safe position
1681 this.y = obstacle.y - obstacle.radius - 30
1682 this.x = obstacle.x + random(-50, 50)
1683 this.stuckCounter = 0
1684
1685 // If attacking, abort and retry
1686 if (this.state === 'attacking') {
1687 this.state = 'approaching'
1688 this.attacking = false
1689 }
1690 }
1691 } else {
1692 this.stuckCounter = 0 // Reset counter when not stuck
1693 }
1694 }
1695
1696 // Also check home branch collision
1697 if (window.homeBranch && this.y > window.homeBranch.y - 40) {
1698 // Check if bird is in branch X range
1699 let branchStart = Math.min(
1700 window.homeBranch.startX,
1701 window.homeBranch.endX
1702 )
1703 let branchEnd = Math.max(window.homeBranch.startX, window.homeBranch.endX)
1704
1705 if (this.x >= branchStart - 20 && this.x <= branchEnd + 20) {
1706 // Bird is too close to branch, push up
1707 this.y = window.homeBranch.y - 40
1708
1709 // If diving, abort dive
1710 if (this.state === 'attacking' && this.pattern === 'dive') {
1711 this.state = 'retreating'
1712 this.attacking = false
1713 }
1714 }
1715 }
1716 }
1717
1718 executeGlidePattern () {
1719 if (this.state === 'approaching') {
1720 // Glide in from top corner faster
1721 this.x += 5
1722 this.y += 2.5
1723
1724 if (this.y > height * 0.15) {
1725 this.state = 'attacking'
1726 this.attacking = true
1727 this.updateTarget()
1728 }
1729 } else if (this.state === 'attacking') {
1730 // Glide toward target aggressively
1731 let dx = this.targetX - this.x
1732 let dy = this.targetY - this.y
1733 let dist = sqrt(dx * dx + dy * dy)
1734
1735 if (dist > 10) {
1736 this.x += (dx / dist) * 7 // Much faster glide
1737 this.y += (dy / dist) * 7
1738 }
1739
1740 // Pass through and maybe attack again
1741 if (this.y > height - 100 || this.x < -50 || this.x > width + 50) {
1742 this.consecutiveAttacks++
1743
1744 if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1745 // Reset for another pass
1746 this.state = 'approaching'
1747 this.x = random() < 0.5 ? -50 : width + 50
1748 this.y = random(50, 150)
1749 this.updateTarget()
1750 } else {
1751 this.state = 'retreating'
1752 this.attacking = false
1753 }
1754 }
1755 } else if (this.state === 'retreating') {
1756 // Continue off screen
1757 this.x += this.vx
1758 this.y += this.vy
1759
1760 // Reset
1761 if (this.y > height + 50 || this.x < -100 || this.x > width + 100) {
1762 this.state = 'approaching'
1763 this.attackDelay = random(120, 180)
1764 this.x = random() < 0.5 ? -50 : width + 50
1765 this.y = random(50, 150)
1766 this.vx = this.x < width / 2 ? 5 : -5
1767 this.vy = 2.5
1768 this.consecutiveAttacks = 0
1769 this.maxConsecutiveAttacks = random(2, 4)
1770 }
1771 }
1772 }
1773
1774 executeCirclePattern () {
1775 if (this.state === 'approaching') {
1776 // Move to circle start position
1777 let startX = this.circleCenter.x + cos(0) * this.circleRadius
1778 let startY = this.circleCenter.y + sin(0) * this.circleRadius
1779
1780 let dx = startX - this.x
1781 let dy = startY - this.y
1782
1783 this.x += dx * 0.1
1784 this.y += dy * 0.1
1785
1786 if (abs(dx) < 20 && abs(dy) < 20) {
1787 this.state = 'attacking'
1788 this.attacking = true
1789 this.circleAngle = 0
1790 }
1791 } else if (this.state === 'attacking') {
1792 // Circle around center FASTER
1793 this.circleAngle += 0.08 // Faster circling
1794 this.x = this.circleCenter.x + cos(this.circleAngle) * this.circleRadius
1795 this.y = this.circleCenter.y + sin(this.circleAngle) * this.circleRadius
1796
1797 // More frequent dives toward center
1798 if (frameCount % 60 === 0) {
1799 // Every second instead of every 2 seconds
1800 this.circleRadius = max(30, this.circleRadius - 50)
1801 } else {
1802 this.circleRadius = min(150, this.circleRadius + 2)
1803 }
1804
1805 // Complete circle faster
1806 if (this.circleAngle > TWO_PI * 1.5) {
1807 // 1.5 circles instead of 2
1808 this.state = 'retreating'
1809 this.attacking = false
1810 }
1811 } else if (this.state === 'retreating') {
1812 // Fly away
1813 this.y -= 7
1814
1815 if (this.y < -50) {
1816 this.state = 'approaching'
1817 this.attackDelay = random(150, 240)
1818 this.x = random(width)
1819 }
1820 }
1821 }
1822
1823 checkCollisions () {
1824 // FIX: Increased collision radius for more generous hit detection
1825 let collisionDistance = this.size + spider.radius + 5 // Added 5 pixel buffer
1826
1827 // Check collision with spider
1828 if (
1829 this.attacking &&
1830 dist(this.x, this.y, spider.pos.x, spider.pos.y) < collisionDistance
1831 ) {
1832 // Hit spider!
1833 if (gamePhase === 'DAWN') {
1834 // Calculate damage
1835 let damage = 20 // Base damage
1836
1837 // If spider has no stamina, GAME OVER!
1838 if (jumpStamina <= 0) {
1839 triggerGameOver('Exhausted spider caught by bird!')
1840 return
1841 }
1842
1843 // Otherwise, reduce stamina
1844 jumpStamina = max(0, jumpStamina - damage)
1845 stats.birdHitsTaken++
1846
1847 // Knockback effect
1848 spider.vel.x = (spider.pos.x - this.x) * 0.3
1849 spider.vel.y = -3
1850 spider.isAirborne = true
1851
1852 // Red damage particles
1853 for (let i = 0; i < 12; i++) {
1854 let p = new Particle(spider.pos.x, spider.pos.y)
1855 p.color = color(255, 50, 50)
1856 p.vel = createVector(random(-4, 4), random(-4, 1))
1857 p.size = random(4, 8)
1858 particles.push(p)
1859 }
1860
1861 // Screen shake effect
1862 if (typeof screenShake !== 'undefined') {
1863 screenShake = 10
1864 }
1865
1866 // Warning notifications - but limited to prevent spam
1867 if (notifications.length < 3) {
1868 // Limit notifications
1869 if (jumpStamina <= 20) {
1870 notifications.push(
1871 new Notification('CRITICAL STAMINA!', color(255, 50, 50))
1872 )
1873 } else if (jumpStamina <= 40) {
1874 notifications.push(
1875 new Notification('Low stamina - find cover!', color(255, 150, 50))
1876 )
1877 }
1878 }
1879 }
1880
1881 // Bird bounces off
1882 this.state = 'retreating'
1883 this.attacking = false
1884 }
1885
1886 // Check collision with web strands
1887 if (this.attacking) {
1888 for (let strand of webStrands) {
1889 if (!strand.broken && strand.path) {
1890 for (let point of strand.path) {
1891 if (dist(this.x, this.y, point.x, point.y) < this.size) {
1892 // Bird breaks the strand!
1893 strand.broken = true
1894 stats.strandsLostInNight++
1895
1896 // Particles
1897 for (let i = 0; i < 5; i++) {
1898 let p = new Particle(point.x, point.y)
1899 p.color = color(255, 255, 255)
1900 p.vel = createVector(random(-2, 2), random(-2, 2))
1901 particles.push(p)
1902 }
1903 break
1904 }
1905 }
1906 }
1907 }
1908 }
1909
1910 // Thief bird steals flies
1911 if (this.isThief && this.attacking) {
1912 for (let i = flies.length - 1; i >= 0; i--) {
1913 let fly = flies[i]
1914 if (
1915 (fly.stuck || fly.caught) &&
1916 dist(this.x, this.y, fly.pos.x, fly.pos.y) < this.size + 10
1917 ) {
1918 // Steal the fly!
1919 flies.splice(i, 1)
1920
1921 // Purple particles for theft
1922 for (let j = 0; j < 6; j++) {
1923 let p = new Particle(fly.pos.x, fly.pos.y)
1924 p.color = color(200, 100, 255)
1925 p.vel = createVector(random(-2, 2), random(-2, 2))
1926 particles.push(p)
1927 }
1928
1929 // Thief escapes after stealing
1930 this.state = 'retreating'
1931 this.attacking = false
1932 this.active = false // Deactivate thief after successful theft
1933 break
1934 }
1935 }
1936 }
1937 }
1938
1939 display () {
1940 push()
1941 translate(this.x, this.y)
1942
1943 // Show if bird is stuck (for debugging)
1944 if (this.stuckCounter > 15) {
1945 // Flash red when stuck
1946 push()
1947 noFill()
1948 stroke(255, 0, 0, 100)
1949 strokeWeight(2)
1950 ellipse(0, 0, this.size * 3)
1951 pop()
1952 }
1953
1954 // Rotate based on movement
1955 if (this.state === 'attacking' && this.pattern === 'dive') {
1956 rotate(PI / 2) // Point down when diving
1957 } else if (this.vx !== 0) {
1958 rotate(atan2(this.vy, this.vx))
1959 }
1960
1961 // Shadow
1962 push()
1963 noStroke()
1964 fill(0, 0, 0, 30)
1965 ellipse(5, 5, this.size * 2)
1966 pop()
1967
1968 // Wings
1969 let wingSpread = sin(this.wingPhase) * this.size * 0.8
1970
1971 // Wing shadows
1972 noStroke()
1973 fill(0, 0, 0, 40)
1974 ellipse(-wingSpread + 2, 2, this.size * 1.5, this.size * 0.5)
1975 ellipse(wingSpread + 2, 2, this.size * 1.5, this.size * 0.5)
1976
1977 // Wings
1978 fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80))
1979 ellipse(-wingSpread, 0, this.size * 1.5, this.size * 0.5)
1980 ellipse(wingSpread, 0, this.size * 1.5, this.size * 0.5)
1981
1982 // Body
1983 fill(this.isThief ? color(100, 50, 150) : color(50, 50, 50))
1984 ellipse(0, 0, this.size * 0.8, this.size)
1985
1986 // Head
1987 fill(this.isThief ? color(80, 40, 120) : color(30, 30, 30))
1988 ellipse(0, -this.size * 0.4, this.size * 0.5)
1989
1990 // Eye
1991 fill(this.isThief ? color(255, 100, 255) : color(255, 100, 100))
1992 noStroke()
1993 ellipse(3, -this.size * 0.4, 4)
1994
1995 // Beak
1996 fill(this.isThief ? color(200, 150, 50) : color(200, 150, 0))
1997 triangle(
1998 this.size * 0.25,
1999 -this.size * 0.4,
2000 this.size * 0.45,
2001 -this.size * 0.35,
2002 this.size * 0.25,
2003 -this.size * 0.3
2004 )
2005
2006 // Tail feathers
2007 fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80))
2008 for (let i = -1; i <= 1; i++) {
2009 push()
2010 translate(-this.size * 0.3, this.size * 0.3)
2011 rotate(i * 0.2)
2012 ellipse(0, 0, this.size * 0.3, this.size * 0.8)
2013 pop()
2014 }
2015
2016 // Warning indicator if attacking
2017 if (this.attacking && frameCount % 20 < 10) {
2018 noFill()
2019 stroke(255, 100, 100, 150)
2020 strokeWeight(2)
2021 ellipse(0, 0, this.size * 2.5)
2022 }
2023
2024 pop()
2025 }
2026 }
2027
2028 class Particle {
2029 constructor (x, y) {
2030 this.pos = createVector(x, y)
2031 this.vel = createVector(random(-3, 3), random(-5, -2))
2032 this.lifetime = 255
2033 this.color = color(255, random(200, 255), random(100, 200))
2034 this.size = 6 // Default size
2035 }
2036
2037 update () {
2038 this.vel.y += 0.2
2039 this.pos.add(this.vel)
2040 this.lifetime -= 8
2041 }
2042
2043 display () {
2044 push()
2045 noStroke()
2046 fill(red(this.color), green(this.color), blue(this.color), this.lifetime)
2047 ellipse(this.pos.x, this.pos.y, this.size)
2048 pop()
2049 }
2050
2051 isDead () {
2052 return this.lifetime <= 0
2053 }
2054 }