JavaScript · 73810 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 // FIX: Only check if we ACTUALLY don't have enough stamina
26 if (jumpStamina < jumpCost) {
27 isExhausted = true
28 // Only show notification if we're truly out
29 if (jumpStamina < 5 && notifications.length < 3) {
30 notifications.push(
31 new Notification('NO STAMINA!', color(255, 50, 50))
32 )
33 }
34 return
35 }
36 jumpStamina -= jumpCost
37 // Delay stamina regen after each jump during DAWN
38 staminaRegenCooldown = 60 // 1s at 60fps
39 }
40
41 // PHASE 4B: Track wind jumps
42 if (windActive) {
43 stats.windJumps++
44 achievements.windRider.progress++
45 }
46
47 let direction = createVector(targetX - this.pos.x, targetY - this.pos.y)
48 let clickDistance = direction.mag()
49 direction.normalize()
50
51 // Apply charge multiplier if provided
52 let actualJumpPower = map(
53 clickDistance,
54 0,
55 200,
56 3,
57 this.jumpPower * chargeMultiplier
58 )
59 actualJumpPower = constrain(
60 actualJumpPower,
61 3,
62 this.jumpPower * chargeMultiplier
63 )
64 direction.mult(actualJumpPower)
65
66 this.vel = direction
67 this.isAirborne = true
68 this.canJump = false
69 // Count every successful jump (lifetime stat)
70 if (typeof stats !== 'undefined') {
71 stats.totalJumps++
72 }
73
74 // FIX: Ensure lastAnchorPoint is set to edge, not center
75 if (!this.lastAnchorPoint) {
76 // If no anchor point set yet, use current position
77 this.lastAnchorPoint = this.pos.copy()
78 }
79 // Record jump time for touch debounce
80 if (typeof window !== 'undefined') {
81 window.lastJumpTime = millis()
82 }
83
84 // Check if we're jumping off a web strand
85 for (let strand of webStrands) {
86 if (strand === currentStrand) continue
87
88 if (this.checkStrandCollision(strand)) {
89 // Much simpler shimmy detection based on actual jump power used
90 let isShimmy = actualJumpPower < 6 // If we used less than half power, it's a shimmy
91
92 // Apply appropriate recoil based on movement type
93 if (isShimmy) {
94 // Trigger shimmy visual effect
95 this.shimmyEffect = 20
96
97 // NO recoil at all for shimmying - just tiny vibration
98 strand.vibrate(0.3)
99
100 // Tiny yellow particles
101 let p = new Particle(this.pos.x, this.pos.y)
102 p.color = color(255, 255, 100, 80)
103 p.vel = createVector(random(-0.3, 0.3), random(-0.3, 0.3))
104 p.size = 2
105 particles.push(p)
106 } else {
107 // Scale recoil based on actual jump power
108 let recoilForce = -(actualJumpPower / this.jumpPower) * 0.08 // Scale by power ratio
109 strand.applyRecoil(recoilForce)
110
111 // Create particles only for real jumps
112 for (let i = 0; i < 2; i++) {
113 let p = new Particle(this.pos.x, this.pos.y)
114 p.color = color(255, 255, 255, 120)
115 p.vel = createVector(random(-0.8, 0.8), random(1, 2))
116 p.size = 3
117 particles.push(p)
118 }
119 }
120
121 break
122 }
123 }
124 }
125
126 munch () {
127 if (this.munchCooldown > 0) return
128
129 isMunching = true
130 this.munchCooldown = this.munchCooldownMax || 30 // Use upgrade value if available
131
132 for (let i = flies.length - 1; i >= 0; i--) {
133 let fly = flies[i]
134 let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y)
135 if (d < this.munchRadius) {
136 fliesMunched++
137 stats.fliesMunchedInCurrentNight++ // Track for achievements
138
139 // POINTS: Award points for munching
140 let munchPoints = 4 // Base points for munch
141 if (fly.type === 'golden') munchPoints = 8
142 if (fly.type === 'queen') munchPoints = 10
143 if (fly.type === 'moth') munchPoints = 6
144 playerPoints += munchPoints
145 // totalFliesCaught++ // Add to lifetime counter
146
147 webSilk = min(webSilk + 15, maxWebSilk)
148
149 // PHASE 3: Metabolize upgrade effect
150 if (upgrades.metabolize && upgrades.metabolize.level > 0) {
151 // Heal nearby broken strands
152 for (let strand of webStrands) {
153 if (strand.broken) {
154 let distToStrand = Infinity
155 if (strand.path && strand.path.length > 0) {
156 for (let point of strand.path) {
157 let d = dist(this.pos.x, this.pos.y, point.x, point.y)
158 if (d < distToStrand) distToStrand = d
159 }
160 }
161
162 if (distToStrand < 100) {
163 strand.broken = false
164 strand.strength = 0.5 // Heal to half strength
165
166 // Green healing particles
167 for (let j = 0; j < 5; j++) {
168 let p = new Particle(this.pos.x, this.pos.y)
169 p.color = color(100, 255, 100)
170 p.vel = createVector(random(-2, 2), random(-2, 2))
171 p.size = 3
172 particles.push(p)
173 }
174
175 break // Only heal one strand per munch
176 }
177 }
178 }
179 }
180
181 for (let j = 0; j < 12; j++) {
182 let p = new Particle(fly.pos.x, fly.pos.y)
183 p.color = color(255, random(100, 255), 0)
184 particles.push(p)
185 }
186
187 flies.splice(i, 1)
188 break
189 }
190 }
191 }
192
193 update () {
194 // If attached to a moving obstacle, move with it
195 if (this.attachedObstacle && !this.isAirborne) {
196 // Calculate angle from obstacle center to spider
197 let angle = atan2(
198 this.pos.y - this.attachedObstacle.y,
199 this.pos.x - this.attachedObstacle.x
200 )
201 // Keep spider on the surface of the obstacle
202 this.pos.x =
203 this.attachedObstacle.x +
204 cos(angle) * (this.attachedObstacle.radius + this.radius)
205 this.pos.y =
206 this.attachedObstacle.y +
207 sin(angle) * (this.attachedObstacle.radius + this.radius)
208 }
209
210 if (this.isAirborne) {
211 this.acc.add(this.gravity)
212 this.attachedObstacle = null // Clear attachment when jumping
213 }
214
215 this.vel.add(this.acc)
216 this.vel.limit(this.maxSpeed)
217 this.pos.add(this.vel)
218 this.acc.mult(0)
219
220 if (this.munchCooldown > 0) {
221 this.munchCooldown--
222 if (this.munchCooldown === 0) {
223 isMunching = false
224 }
225 }
226
227 // Check ground collision
228 if (this.pos.y >= height - this.radius) {
229 this.pos.y = height - this.radius
230 this.land()
231 this.attachedObstacle = null
232 }
233
234 // Check wall collisions
235 if (this.pos.x <= this.radius || this.pos.x >= width - this.radius) {
236 this.pos.x = constrain(this.pos.x, this.radius, width - this.radius)
237 this.vel.x *= -0.5
238 }
239
240 // Check ceiling
241 if (this.pos.y <= this.radius) {
242 this.pos.y = this.radius
243 this.vel.y *= -0.5 // Bounce off ceiling, don't land
244 }
245
246 // Check home branch collision (one-way platform)
247 if (window.homeBranch && this.isAirborne && this.vel.y > 0.1) {
248 let branch = window.homeBranch
249
250 // Calculate the actual geometric bounds
251 let leftX = Math.min(branch.startX, branch.endX)
252 let rightX = Math.max(branch.startX, branch.endX)
253
254 // IMPORTANT: Extend the check zone beyond the mathematical end
255 // The branch visually extends past endX due to strokeWeight and bezier curves
256 let checkPadding = 20 // Add padding for the visual overhang
257
258 if (
259 this.pos.x >= leftX - checkPadding &&
260 this.pos.x <= rightX + checkPadding
261 ) {
262 // Calculate normalized position along branch
263 let t
264
265 if (branch.side === 'left') {
266 // Left branch: startX is base, endX is tip
267 t = (this.pos.x - branch.startX) / (branch.endX - branch.startX)
268 } else {
269 // Right branch: startX is base, endX is tip (but startX > endX)
270 t = (branch.startX - this.pos.x) / (branch.startX - branch.endX)
271 }
272
273 // CRITICAL FIX: Allow t to exceed 1 for the tip overhang
274 // The visual branch extends past the mathematical endpoint
275 let maxT = 1.15 // Allow 15% overshoot for visual overhang
276 t = constrain(t, 0, maxT)
277
278 // Calculate thickness
279 let visualThickness
280 if (t > 1.0) {
281 // Past the mathematical end - use minimum tip thickness
282 visualThickness = branch.thickness * 0.35
283 } else {
284 // Normal taper
285 visualThickness = lerp(branch.thickness, branch.thickness * 0.35, t)
286 }
287
288 // The branch is drawn centered at branch.y; compute top/bottom with rotation
289 let rotationOffset = this.pos.x * branch.angle
290 let branchTopY = branch.y - visualThickness + rotationOffset
291 let branchBottomY = branch.y + visualThickness + rotationOffset
292
293 // Check collision
294 let prevY = this.pos.y - this.vel.y
295
296 // One-way platform collision
297 if (
298 prevY <= branchTopY && // Was above
299 this.pos.y + this.radius >= branchTopY && // Now at or below
300 this.pos.y - this.radius < branchBottomY // Not completely below the rotated bottom
301 ) {
302 // Not completely below
303
304 // Land on branch
305 this.pos.y = branchTopY - this.radius
306 this.land()
307 this.attachedObstacle = null
308 }
309 }
310 }
311
312 // Check obstacle collisions
313 for (let obstacle of obstacles) {
314 if (this.checkObstacleCollision(obstacle)) {
315 this.landOnObstacle(obstacle)
316 }
317 }
318
319 // Check web strand collisions
320 for (let strand of webStrands) {
321 if (strand === currentStrand) continue
322
323 if (this.isAirborne && this.checkStrandCollision(strand)) {
324 this.landOnStrand(strand)
325 }
326 }
327
328 // Check food box collisions
329 for (let i = foodBoxes.length - 1; i >= 0; i--) {
330 let box = foodBoxes[i]
331 if (
332 dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) <
333 this.radius + box.radius
334 ) {
335 box.collect()
336 foodBoxes.splice(i, 1)
337 }
338 }
339 }
340
341 checkObstacleCollision (obstacle) {
342 let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y)
343 return d < this.radius + obstacle.radius
344 }
345
346 checkStrandCollision (strand) {
347 if (!strand || !strand.start || !strand.end) return false
348 let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
349 return d < this.radius + 2
350 }
351
352 pointToLineDistance (point, lineStart, lineEnd) {
353 // Guard nulls
354 if (!lineStart || !lineEnd) {
355 return Infinity
356 }
357 let line = p5.Vector.sub(lineEnd, lineStart)
358 let lineLength = line.mag()
359 // If start and end coincide, distance is to the single point
360 if (lineLength === 0) {
361 return p5.Vector.dist(point, lineStart)
362 }
363 line.normalize()
364 let pointToStart = p5.Vector.sub(point, lineStart)
365 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
366 let closestPoint = p5.Vector.add(
367 lineStart,
368 p5.Vector.mult(line, projLength)
369 )
370 return p5.Vector.dist(point, closestPoint)
371 }
372
373 landOnObstacle (obstacle) {
374 // Only land if we're actually airborne
375 if (!this.isAirborne) return
376
377 // Calculate angle from obstacle center to spider
378 let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
379
380 // Place spider on the edge of the circular collision boundary
381 this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius)
382 this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius)
383
384 // FIX: Set anchor point at the edge, not center
385 this.lastAnchorPoint = createVector(
386 obstacle.x + cos(angle) * obstacle.radius,
387 obstacle.y + sin(angle) * obstacle.radius
388 )
389
390 this.attachedObstacle = obstacle
391 this.land()
392 }
393
394 landOnStrand (strand) {
395 // Only land if we're actually airborne
396 if (!this.isAirborne) return
397 if (!strand || !strand.start || !strand.end) return
398 let line = p5.Vector.sub(strand.end, strand.start)
399 let lineLength = line.mag()
400 if (lineLength === 0) {
401 // Degenerate strand; snap to start
402 this.pos = strand.start.copy
403 ? strand.start.copy()
404 : createVector(strand.start.x, strand.start.y)
405 } else {
406 line.normalize()
407 let pointToStart = p5.Vector.sub(this.pos, strand.start)
408 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
409 let closestPoint = p5.Vector.add(
410 strand.start,
411 p5.Vector.mult(line, projLength)
412 )
413 this.pos = closestPoint
414 }
415 this.attachedObstacle = null // Not on an obstacle
416 this.land()
417 }
418
419 land () {
420 this.vel.mult(0)
421 this.isAirborne = false
422 this.canJump = true
423
424 // FIX: Check if we're actually landing on something valid
425 let landedOnSomething = false
426 let landingPoint = null // Store where we're landing for anchor
427 let landingObstacle = null // NEW: Track which obstacle we're landing on
428
429 // Check if on ground
430 if (this.pos.y >= height - this.radius - 5) {
431 landedOnSomething = true
432 landingPoint = createVector(this.pos.x, height)
433 }
434
435 // Check if on an obstacle
436 if (!landedOnSomething) {
437 for (let obstacle of obstacles) {
438 if (this.checkObstacleCollision(obstacle)) {
439 landedOnSomething = true
440 landingObstacle = obstacle // NEW: Store the obstacle
441 // Calculate edge point for anchor
442 let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
443 landingPoint = createVector(
444 obstacle.x + cos(angle) * obstacle.radius,
445 obstacle.y + sin(angle) * obstacle.radius
446 )
447 break
448 }
449 }
450 }
451
452 // Check if on a web strand
453 if (!landedOnSomething) {
454 for (let strand of webStrands) {
455 if (
456 strand !== currentStrand &&
457 !strand.broken &&
458 this.checkStrandCollision(strand)
459 ) {
460 landedOnSomething = true
461 // For web strands, use spider position as anchor
462 landingPoint = this.pos.copy()
463 break
464 }
465 }
466 }
467
468 // Check if on home branch
469 if (!landedOnSomething && window.homeBranch) {
470 let branch = window.homeBranch
471 let branchStart = Math.min(branch.startX, branch.endX)
472 let branchEnd = Math.max(branch.startX, branch.endX)
473
474 if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
475 let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
476 t = constrain(t, 0, 1)
477 let branchTopThickness = lerp(
478 branch.thickness * 0.9,
479 branch.thickness * 0.35,
480 t
481 )
482 let branchSurfaceY = branch.y - branchTopThickness
483 let angleCorrection = (this.pos.x - branchStart) * branch.angle
484 branchSurfaceY += angleCorrection
485
486 if (abs(this.pos.y - branchSurfaceY) < this.radius + 10) {
487 landedOnSomething = true
488 landingPoint = createVector(this.pos.x, branchSurfaceY)
489 }
490 }
491 }
492
493 // FIX: If we're deploying web but didn't land on anything valid, destroy the web
494 if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) {
495 if (landedOnSomething && landingPoint) {
496 // Valid landing - finalize the web at the landing point
497 currentStrand.end = landingPoint.copy() // Use edge point, not spider center
498
499 // POINTS: Award 1 point for successful web strand
500 playerPoints += 1
501 stats.strandsCreated++ // Track for stats
502
503 // NEW: Track obstacle attachment for the end point
504 if (landingObstacle) {
505 currentStrand.endObstacle = landingObstacle
506 currentStrand.endAngle = atan2(
507 landingPoint.y - landingObstacle.y,
508 landingPoint.x - landingObstacle.x
509 )
510 }
511
512 if (!currentStrand.path || currentStrand.path.length === 0) {
513 currentStrand.path = [landingPoint.copy()]
514 } else {
515 currentStrand.path.push(landingPoint.copy())
516 }
517
518 let newNode = new WebNode(landingPoint.x, landingPoint.y)
519 // NEW: Track node attachment
520 if (landingObstacle) {
521 newNode.attachedObstacle = landingObstacle
522 newNode.attachmentAngle = currentStrand.endAngle
523 }
524 webNodes.push(newNode)
525
526 // Update last anchor for next web
527 this.lastAnchorPoint = landingPoint.copy()
528 } else {
529 // Invalid landing in mid-air - destroy the web!
530 if (
531 webStrands.length > 0 &&
532 webStrands[webStrands.length - 1] === currentStrand
533 ) {
534 webStrands.pop() // Remove the invalid strand
535
536 // Create poof particles
537 for (let i = 0; i < 8; i++) {
538 let p = new Particle(this.pos.x, this.pos.y)
539 p.color = color(255, 255, 255, 150)
540 p.vel = createVector(random(-3, 3), random(-3, 3))
541 p.size = 4
542 particles.push(p)
543 }
544
545 // Notification
546 if (notifications.length < 3) {
547 notifications.push(
548 new Notification('Web needs anchor point!', color(255, 150, 150))
549 )
550 }
551 }
552 }
553 } else if (landedOnSomething && landingPoint) {
554 // Update last anchor point even when not deploying web
555 this.lastAnchorPoint = landingPoint.copy()
556 }
557
558 currentStrand = null
559 isDeployingWeb = false
560 }
561
562 display () {
563 push()
564 translate(this.pos.x, this.pos.y)
565
566 if (isMunching && this.munchCooldown > 15) {
567 push()
568 fill(255, 100, 100, 150)
569 noStroke()
570 let munchSize = 15 + sin(frameCount * 0.5) * 5
571 arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE)
572 pop()
573 }
574
575 fill(20)
576 stroke(0)
577 strokeWeight(1)
578 ellipse(0, 0, this.radius * 2)
579
580 fill(40)
581 noStroke()
582 ellipse(0, -2, this.radius * 1.2, this.radius * 1.5)
583
584 if (gamePhase === 'NIGHT') {
585 fill(255, 100, 100)
586 } else {
587 fill(255, 0, 0)
588 }
589 ellipse(-3, -3, 3)
590 ellipse(3, -3, 3)
591
592 stroke(0)
593 strokeWeight(1.5)
594 for (let i = 0; i < 4; i++) {
595 let angle = PI / 6 + (i * PI) / 8
596 line(0, 0, cos(angle) * 12, sin(angle) * 8)
597 line(0, 0, -cos(angle) * 12, sin(angle) * 8)
598 }
599
600 if (webSilk < 20) {
601 fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50)
602 noStroke()
603 ellipse(0, -15, 8)
604 }
605
606 pop()
607 }
608 }
609
610 class Fly {
611 constructor (type = 'regular') {
612 this.type = type
613 if (random() < 0.5) {
614 this.pos = createVector(
615 random() < 0.5 ? -20 : width + 20,
616 random(50, height - 100)
617 )
618 } else {
619 this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20)
620 }
621
622 this.vel = createVector(random(-2, 2), random(-1, 1))
623 this.acc = createVector(0, 0)
624 this.radius = 4
625 this.caught = false
626 this.stuck = false
627 this.wingPhase = random(TWO_PI)
628 this.wanderAngle = random(TWO_PI)
629 this.glowIntensity = random(150, 255)
630 this.touchedStrands = new Set()
631 this.slowedBy = new Set() // Track which strands are slowing us
632 this.baseSpeed = 3
633 this.currentSpeed = this.baseSpeed
634 }
635
636 update () {
637 if (this.stuck) {
638 this.updatePositionOnWeb()
639 return
640 }
641
642 if (this.caught) {
643 this.vel.mult(0.95)
644 if (this.vel.mag() < 0.1) {
645 this.stuck = true
646 fliesCaught++
647 totalFliesCaught++ // Add to lifetime counter
648
649 if (typeof stats !== 'undefined') {
650 stats.totalFliesCaught++
651 if (this.type === 'golden') stats.goldenCaught++
652 else if (this.type === 'queen') stats.queensCaught++
653 else if (this.type === 'moth') stats.mothsCaught++
654 else stats.regularCaught++
655 }
656
657 // POINTS: Award points for catching fly
658 let catchPoints = 2 // Base points for catch
659 if (this.type === 'golden') catchPoints = 4
660 if (this.type === 'queen') catchPoints = 5
661 if (this.type === 'moth') catchPoints = 3
662 playerPoints += catchPoints
663
664 webSilk = min(webSilk + 5, maxWebSilk)
665 }
666 this.updatePositionOnWeb()
667 return
668 }
669
670 this.wanderAngle += random(-0.3, 0.3)
671 let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle))
672 wanderForce.mult(0.1)
673 this.acc.add(wanderForce)
674
675 // Apply current speed (which may be slowed)
676 this.vel.add(this.acc)
677 this.vel.limit(this.currentSpeed)
678 this.pos.add(this.vel)
679 this.acc.mult(0)
680
681 if (this.pos.x < -30) this.pos.x = width + 30
682 if (this.pos.x > width + 30) this.pos.x = -30
683 if (this.pos.y < -30) this.pos.y = height + 30
684 if (this.pos.y > height + 30) this.pos.y = -30
685
686 // Check web collisions
687 this.checkWebCollisions()
688 }
689
690 updatePositionOnWeb () {
691 // Find the web strand(s) this fly is attached to
692 for (let strand of webStrands) {
693 if (strand.broken) continue
694
695 // Check if fly is on this strand
696 let closestPoint = null
697 let closestDistance = Infinity
698
699 if (strand.path && strand.path.length > 1) {
700 for (let i = 0; i < strand.path.length - 1; i++) {
701 let p1 = strand.path[i]
702 let p2 = strand.path[i + 1]
703
704 // Find closest point on this segment
705 let line = p5.Vector.sub(p2, p1)
706 let lineLength = line.mag()
707 if (lineLength === 0) continue
708 line.normalize()
709
710 let pointToStart = p5.Vector.sub(this.pos, p1)
711 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
712
713 let projPoint = p5.Vector.add(p1, p5.Vector.mult(line, projLength))
714 let d = p5.Vector.dist(this.pos, projPoint)
715
716 if (d < closestDistance && d < this.radius + 5) {
717 closestDistance = d
718 closestPoint = projPoint
719 }
720 }
721 }
722
723 // If we found a close point on this strand, stick to it
724 if (closestPoint) {
725 // Move fly to follow the strand's movement
726 this.pos.x = closestPoint.x
727 this.pos.y = closestPoint.y
728
729 // Add small vibration when on a moving web
730 if (strand.vibration > 0) {
731 this.pos.x += random(-1, 1) * strand.vibration * 0.1
732 this.pos.y += random(-1, 1) * strand.vibration * 0.1
733 }
734 }
735 }
736 }
737
738 checkWebCollisions () {
739 let currentlyTouching = new Set()
740
741 for (let strand of webStrands) {
742 let touching = false
743
744 // Check collision with strand path
745 if (strand.path && strand.path.length > 1) {
746 // OPTIMIZATION: Skip every other point for collision detection
747 for (let i = 0; i < strand.path.length - 1; i += 2) {
748 let p1 = strand.path[i]
749 let p2 = strand.path[Math.min(i + 1, strand.path.length - 1)]
750 let d = this.pointToLineDistance(this.pos, p1, p2)
751 if (d < this.radius + 3) {
752 touching = true
753 break
754 }
755 }
756 } else if (strand.start && strand.end) {
757 // Fallback for strands without path
758 let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
759 if (d < this.radius + 3) {
760 touching = true
761 }
762 }
763
764 if (touching) {
765 currentlyTouching.add(strand)
766
767 // If this is a new strand we're touching
768 if (!this.touchedStrands.has(strand)) {
769 this.touchedStrands.add(strand)
770
771 // Vibrate the web when first touching
772 strand.vibrate(3)
773
774 // First strand slows us down
775 if (this.touchedStrands.size === 1) {
776 this.currentSpeed = this.baseSpeed * 0.4 // Slow to 40% speed
777 this.slowedBy.add(strand)
778
779 // Visual feedback - yellow particles for slowing
780 // LIMIT PARTICLES TO PREVENT FREEZE
781 let particleCount = Math.min(3, 100 - particles.length)
782 for (let j = 0; j < particleCount; j++) {
783 let p = new Particle(this.pos.x, this.pos.y)
784 p.color = color(255, 255, 0, 150)
785 p.vel = createVector(random(-1, 1), random(-1, 1))
786 p.size = 3
787 particles.push(p)
788 }
789 }
790 // Second strand catches us
791 else if (this.touchedStrands.size >= 2 && !this.caught) {
792 this.caught = true
793 this.currentSpeed = 0
794
795 // Stronger vibration when caught
796 strand.vibrate(8)
797
798 // FIX: OPTIMIZE NEARBY STRAND VIBRATION
799 // This is likely the main cause of the freeze - checking distances between all strands
800 // Use a more efficient method
801 propagateVibration(strand, 2)
802
803 // Create caught particles - LIMIT TO PREVENT FREEZE
804 let particleCount = Math.min(6, 100 - particles.length)
805 for (let j = 0; j < particleCount; j++) {
806 let p = new Particle(this.pos.x, this.pos.y)
807 p.color = color(255, 200, 0, 200)
808 p.vel = createVector(random(-2, 2), random(-2, 2))
809 particles.push(p)
810 }
811 }
812 }
813 }
814 }
815
816 // If we're no longer touching strands we were slowed by, speed back up
817 if (this.slowedBy.size > 0 && currentlyTouching.size === 0) {
818 this.currentSpeed = this.baseSpeed
819 this.slowedBy.clear()
820 }
821 }
822
823 pointToLineDistance (point, lineStart, lineEnd) {
824 // Add null checks
825 if (!point || !lineStart || !lineEnd) return Infinity
826
827 let dx = lineEnd.x - lineStart.x
828 let dy = lineEnd.y - lineStart.y
829 let lineLength = sqrt(dx * dx + dy * dy)
830
831 // If line has no length, return distance to point
832 if (lineLength < 0.01) {
833 return dist(point.x, point.y, lineStart.x, lineStart.y)
834 }
835
836 // Use optimized calculation without creating new vectors
837 let t =
838 ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) /
839 (lineLength * lineLength)
840 t = constrain(t, 0, 1)
841
842 let closestX = lineStart.x + t * dx
843 let closestY = lineStart.y + t * dy
844
845 return dist(point.x, point.y, closestX, closestY)
846 }
847
848 display () {
849 push()
850 translate(this.pos.x, this.pos.y)
851
852 // Show slowdown effect
853 if (this.slowedBy.size > 0 && !this.caught) {
854 stroke(255, 255, 0, 100)
855 strokeWeight(1)
856 noFill()
857 ellipse(0, 0, 20)
858 }
859
860 // ENHANCED: Special golden fly glow
861 if (this.type === 'golden') {
862 // Multiple layers of golden glow for visibility
863 noStroke()
864 // Outermost glow - very faint but wide
865 fill(255, 215, 0, 15)
866 ellipse(0, 0, 80)
867 // Mid glow
868 fill(255, 200, 0, 25)
869 ellipse(0, 0, 60)
870 // Inner glow
871 fill(255, 185, 0, 40)
872 ellipse(0, 0, 40)
873 // Core glow
874 fill(255, 170, 0, 60)
875 ellipse(0, 0, 25)
876
877 // Pulsing effect
878 let pulse = sin(frameCount * 0.1) * 0.3 + 0.7
879 fill(255, 215, 0, 80 * pulse)
880 ellipse(0, 0, 20)
881 } else if (gamePhase === 'NIGHT') {
882 // Regular firefly glow
883 noStroke()
884 fill(255, 255, 150, this.glowIntensity * 0.3)
885 ellipse(0, 0, 30)
886 fill(255, 255, 100, this.glowIntensity * 0.5)
887 ellipse(0, 0, 20)
888 }
889
890 // Body color based on type
891 if (this.type === 'golden') {
892 fill(255, 215, 0) // Gold body
893 stroke(200, 150, 0)
894 } else if (this.type === 'moth') {
895 fill(60, 40, 30) // Brown moth
896 stroke(40, 20, 10)
897 } else if (this.type === 'queen') {
898 fill(150, 50, 200) // Purple queen
899 stroke(100, 0, 150)
900 } else {
901 fill(30) // Regular black
902 stroke(0)
903 }
904
905 strokeWeight(0.5)
906 ellipse(0, 0, this.radius * 2)
907
908 if (!this.stuck) {
909 // Wing animation slows down when slowed
910 let wingSpeed = this.slowedBy.size > 0 ? 0.25 : 0.5
911 this.wingPhase += wingSpeed
912 let wingSpread = sin(this.wingPhase) * 5
913
914 // Wing color based on type
915 if (this.type === 'golden') {
916 fill(255, 240, 150, 200) // Golden translucent wings
917 } else if (this.type === 'moth') {
918 fill(120, 100, 80, 150) // Brown moth wings
919 } else if (this.type === 'queen') {
920 fill(200, 150, 255, 180) // Purple queen wings
921 } else {
922 fill(255, 255, 255, 150) // Regular white wings
923 }
924
925 noStroke()
926 ellipse(-wingSpread, 0, 6, 4)
927 ellipse(wingSpread, 0, 6, 4)
928 }
929
930 // Special markings for special types
931 if (this.type === 'golden') {
932 // Golden sparkle on body
933 fill(255, 255, 200)
934 noStroke()
935 ellipse(0, 0, 2)
936 } else if (this.type === 'queen') {
937 // Crown marking
938 stroke(255, 215, 0)
939 strokeWeight(1)
940 line(-2, -3, -1, -5)
941 line(0, -3, 0, -5)
942 line(2, -3, 1, -5)
943 } else if (gamePhase === 'NIGHT') {
944 // Regular glow abdomen
945 fill(255, 255, 100, this.glowIntensity)
946 noStroke()
947 ellipse(0, 2, 3)
948 }
949
950 pop()
951 }
952 }
953
954 class Obstacle {
955 constructor (x, y, radius, type) {
956 // Store original position for drift tracking
957 this.originalX = x
958 this.originalY = y
959 this.x = x
960 this.y = y
961 this.radius = radius
962 this.type = type || 'leaf'
963 this.rotation = random(TWO_PI)
964 this.leafPoints = []
965
966 // Movement properties for all types
967 this.bobOffset = random(TWO_PI)
968 this.bobSpeed = random(0.02, 0.04)
969 this.bobAmount = 0
970
971 // Wind effect properties
972 this.windSway = 0 // Current sway amount
973 this.windSwayTarget = 0 // Target sway for smooth animation
974 this.windBob = 0 // Additional vertical movement from wind
975 this.basketSwing = 0 // For balloon basket swinging
976
977 // Type-specific initialization
978 if (this.type === 'balloon') {
979 this.bobAmount = 8 // Balloons bob more
980 this.balloonColors = [
981 color(255, 100, 100), // Red
982 color(100, 200, 255), // Blue
983 color(255, 200, 100) // Yellow
984 ]
985 this.balloonColor = random(this.balloonColors)
986 // Remove complex properties - we don't need them for simple balloon
987 } else if (this.type === 'beetle') {
988 this.bobAmount = 4
989 this.driftSpeed = random(0.15, 0.35)
990 this.driftAngle = random(TWO_PI)
991 this.driftChangeRate = random(0.005, 0.015)
992 this.wingPhase = random(TWO_PI)
993 this.beetleColor =
994 random() < 0.5
995 ? color(20, 60, 20) // Dark green
996 : color(40, 20, 60) // Purple
997 this.driftDistance = 0 // Track total drift
998 } else if (this.type === 'leaf') {
999 this.bobAmount = 2 // Leaves bob slightly
1000 let numPoints = 8
1001 for (let i = 0; i < numPoints; i++) {
1002 let angle = (TWO_PI / numPoints) * i
1003 let r = radius * random(0.7, 1.2)
1004 if (i === 0 || i === numPoints / 2) r = radius * 1.3
1005 this.leafPoints.push({ angle: angle, radius: r })
1006 }
1007 }
1008 }
1009
1010 update () {
1011 // Bobbing motion for all types
1012 let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount
1013 this.y = this.originalY + bob
1014
1015 // ENHANCED: Apply wind effects
1016 if (windActive) {
1017 // Different wind responses by type
1018 if (this.type === 'balloon') {
1019 // Balloons are highly affected by wind
1020 this.windSwayTarget = cos(windDirection) * windStrength * 15 // Strong horizontal push
1021 this.windBob =
1022 sin(frameCount * 0.04 + this.bobOffset) * windStrength * 3 // Extra vertical movement
1023
1024 // Basket swings opposite to balloon movement (pendulum effect)
1025 this.basketSwing =
1026 -this.windSway * 0.5 + sin(frameCount * 0.06) * windStrength * 0.3
1027
1028 // Actually move the balloon
1029 this.originalX += cos(windDirection) * windStrength * 0.08
1030
1031 // Keep on screen with stronger resistance at edges
1032 if (this.originalX < 50) {
1033 this.originalX = 50
1034 this.windSwayTarget *= -0.5 // Bounce back effect
1035 }
1036 if (this.originalX > width - 50) {
1037 this.originalX = width - 50
1038 this.windSwayTarget *= -0.5
1039 }
1040 } else if (this.type === 'beetle') {
1041 // Beetles resist but still affected
1042 this.windSwayTarget = cos(windDirection) * windStrength * 3
1043 // Fight against wind
1044 this.driftAngle -= cos(windDirection) * windStrength * 0.01
1045 } else if (this.type === 'leaf') {
1046 // Leaves flutter in wind
1047 this.windSwayTarget = cos(windDirection) * windStrength * 5
1048 this.rotation += windStrength * 0.02 // Spin faster
1049 }
1050 } else {
1051 // No wind, return to normal
1052 this.windSwayTarget = 0
1053 this.windBob = 0
1054 this.basketSwing = 0
1055 }
1056
1057 // Smooth sway animation
1058 this.windSway = lerp(this.windSway, this.windSwayTarget, 0.1)
1059
1060 // Apply wind sway to position
1061 this.x = this.originalX + this.windSway
1062
1063 // Apply wind bob to vertical position
1064 this.y = this.originalY + bob + this.windBob
1065
1066 // Beetle-specific drift
1067 if (this.type === 'beetle') {
1068 // Store initial position if not set
1069 if (!this.initialX) {
1070 this.initialX = this.x
1071 this.initialY = this.y
1072 }
1073
1074 // Slowly change drift direction using Perlin noise
1075 this.driftAngle +=
1076 (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) -
1077 0.5) *
1078 0.1
1079
1080 // Apply drift to original position
1081 this.originalX += cos(this.driftAngle) * this.driftSpeed
1082 this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5
1083
1084 // Calculate total drift distance from initial position
1085 this.driftDistance = dist(
1086 this.originalX,
1087 this.originalY,
1088 this.initialX,
1089 this.initialY
1090 )
1091
1092 // Keep beetles on screen with soft boundaries
1093 if (this.originalX < 80) {
1094 this.driftAngle = random(-PI / 4, PI / 4)
1095 this.originalX = 80
1096 }
1097 if (this.originalX > width - 80) {
1098 this.driftAngle = random((3 * PI) / 4, (5 * PI) / 4)
1099 this.originalX = width - 80
1100 }
1101 if (this.originalY < 80) {
1102 this.driftAngle = random((-3 * PI) / 4, -PI / 4)
1103 this.originalY = 80
1104 }
1105 if (this.originalY > height - 150) {
1106 this.driftAngle = random(PI / 4, (3 * PI) / 4)
1107 this.originalY = height - 150
1108 }
1109
1110 // Update actual position (with bob already applied to y)
1111 this.x = this.originalX
1112
1113 // Check if beetle has drifted too far and break attached strands
1114 if (this.driftDistance > 100) {
1115 this.breakAttachedStrands()
1116 }
1117
1118 // Update wing animation
1119 this.wingPhase += 0.15
1120 }
1121
1122 // For all moving obstacles, update any attached web strands
1123 if (this.bobAmount > 0 || this.type === 'beetle') {
1124 this.updateAttachedStrands()
1125 }
1126 }
1127
1128 updateAttachedStrands () {
1129 // Update web strands that are connected to this obstacle
1130 for (let strand of webStrands) {
1131 if (!strand || strand.broken) continue
1132
1133 // Check if strand starts at this obstacle
1134 if (strand.startObstacle === this) {
1135 // Update the start position to maintain the attachment
1136 let angle = strand.startAngle // Use stored angle
1137 strand.start.x = this.x + cos(angle) * this.radius
1138 strand.start.y = this.y + sin(angle) * this.radius
1139
1140 // Update path if it exists
1141 if (strand.path && strand.path.length > 0) {
1142 strand.path[0].x = strand.start.x
1143 strand.path[0].y = strand.start.y
1144 }
1145 }
1146
1147 // Check if strand ends at this obstacle
1148 if (strand.endObstacle === this) {
1149 // Update the end position to maintain the attachment
1150 let angle = strand.endAngle // Use stored angle
1151 strand.end.x = this.x + cos(angle) * this.radius
1152 strand.end.y = this.y + sin(angle) * this.radius
1153
1154 // Update path if it exists
1155 if (strand.path && strand.path.length > 0) {
1156 strand.path[strand.path.length - 1].x = strand.end.x
1157 strand.path[strand.path.length - 1].y = strand.end.y
1158 }
1159 }
1160 }
1161
1162 // Also update web nodes attached to this obstacle
1163 for (let node of webNodes) {
1164 if (node.attachedObstacle === this) {
1165 let angle = node.attachmentAngle
1166 node.x = this.x + cos(angle) * this.radius
1167 node.y = this.y + sin(angle) * this.radius
1168 }
1169 }
1170 }
1171
1172 breakAttachedStrands () {
1173 // Check for strands attached to this obstacle's edge
1174 for (let strand of webStrands) {
1175 // Check if attached to edge (not center)
1176 let startDist = dist(strand.start.x, strand.start.y, this.x, this.y)
1177 let attachedToStart =
1178 startDist >= this.radius - 5 && startDist <= this.radius + 15
1179
1180 let attachedToEnd = false
1181 if (strand.end) {
1182 let endDist = dist(strand.end.x, strand.end.y, this.x, this.y)
1183 attachedToEnd =
1184 endDist >= this.radius - 5 && endDist <= this.radius + 15
1185 }
1186
1187 if (attachedToStart || attachedToEnd) {
1188 // Mark strand as broken
1189 strand.broken = true
1190
1191 // Release any flies stuck to this strand
1192 for (let fly of flies) {
1193 if (fly.stuck || fly.caught) {
1194 // Check if fly is touching this breaking strand
1195 let touchingStrand = false
1196 if (strand.path && strand.path.length > 1) {
1197 for (let k = 0; k < strand.path.length - 1; k++) {
1198 let p1 = strand.path[k]
1199 let p2 = strand.path[k + 1]
1200 let d = fly.pointToLineDistance(fly.pos, p1, p2)
1201 if (d < fly.radius + 5) {
1202 touchingStrand = true
1203 break
1204 }
1205 }
1206 }
1207
1208 // If fly was on this strand, release it
1209 if (touchingStrand) {
1210 fly.stuck = false
1211 fly.caught = false
1212 fly.currentSpeed = fly.baseSpeed
1213 fly.touchedStrands.clear()
1214 fly.slowedBy.clear()
1215 // Give it a little downward velocity to start falling
1216 fly.vel = createVector(random(-0.5, 0.5), 2)
1217
1218 // Create release particles
1219 for (let j = 0; j < 3; j++) {
1220 let p = new Particle(fly.pos.x, fly.pos.y)
1221 p.color = color(255, 255, 100, 150)
1222 p.vel = createVector(random(-1, 1), random(0, 2))
1223 p.size = 2
1224 particles.push(p)
1225 }
1226 }
1227 }
1228 }
1229
1230 // Create dramatic snap particles
1231 let snapX = attachedToStart ? strand.start.x : strand.end.x
1232 let snapY = attachedToStart ? strand.start.y : strand.end.y
1233
1234 // Red/pink particles for the snap
1235 for (let i = 0; i < 8; i++) {
1236 let p = new Particle(snapX, snapY)
1237 p.color = color(255, random(100, 200), random(100, 150))
1238 p.vel = createVector(random(-5, 5), random(-5, 2))
1239 p.size = random(4, 8)
1240 particles.push(p)
1241 }
1242
1243 // White strand particles
1244 for (let i = 0; i < 4; i++) {
1245 let p = new Particle(snapX, snapY)
1246 p.color = color(255, 255, 255)
1247 p.vel = createVector(random(-3, 3), random(-3, 0))
1248 p.size = 3
1249 particles.push(p)
1250 }
1251
1252 // Reset beetle drift after breaking strands
1253 this.initialX = this.x
1254 this.initialY = this.y
1255 this.driftDistance = 0
1256 }
1257 }
1258 }
1259
1260 display () {
1261 push()
1262 translate(this.x, this.y)
1263
1264 if (this.type === 'balloon') {
1265 // ============================================
1266 // HOT AIR BALLOON WITH CANVAS TEXTURE
1267 // ============================================
1268 push()
1269
1270 // ENHANCED: Tilt balloon based on wind
1271 if (windActive) {
1272 rotate(this.windSway * 0.01) // Slight tilt in wind direction
1273 }
1274
1275 // Balloon shadow
1276 noStroke()
1277 fill(0, 0, 0, 30)
1278 ellipse(5, 5, this.radius * 2.1)
1279
1280 // Main balloon with canvas panel texture
1281 // Draw vertical panels like a real hot air balloon
1282 let numPanels = 8
1283 for (let i = 0; i < numPanels; i++) {
1284 push()
1285
1286 // Rotate for each panel
1287 rotate((TWO_PI / numPanels) * i)
1288
1289 // Alternate panel colors for classic hot air balloon look
1290 if (i % 2 === 0) {
1291 fill(
1292 red(this.balloonColor),
1293 green(this.balloonColor),
1294 blue(this.balloonColor)
1295 )
1296 } else {
1297 // Slightly darker alternate panels
1298 fill(
1299 red(this.balloonColor) * 0.9,
1300 green(this.balloonColor) * 0.9,
1301 blue(this.balloonColor) * 0.9
1302 )
1303 }
1304
1305 // Draw panel as pie slice
1306 noStroke()
1307 arc(
1308 0,
1309 0,
1310 this.radius * 2,
1311 this.radius * 2,
1312 -PI / numPanels,
1313 PI / numPanels,
1314 PIE
1315 )
1316
1317 pop()
1318 }
1319
1320 // Add panel seams (the ropes/stitching between panels)
1321 stroke(60, 40, 20, 110)
1322 strokeWeight(1)
1323 for (let i = 0; i < numPanels; i++) {
1324 let angle = (TWO_PI / numPanels) * i
1325 let x1 = cos(angle) * this.radius * 0.2
1326 let y1 = sin(angle) * this.radius * 0.2
1327 let x2 = cos(angle) * this.radius * 0.95
1328 let y2 = sin(angle) * this.radius * 0.95
1329 line(x1, y1, x2, y2)
1330 }
1331
1332 // Add circular reinforcement bands
1333 noFill()
1334 stroke(80, 50, 30, 80)
1335 strokeWeight(1.5)
1336 ellipse(0, 0, this.radius * 1.4)
1337 ellipse(0, 0, this.radius * 0.8)
1338
1339 // Matte fabric shading (subtle, non-glossy)
1340 noStroke()
1341 // Soft radial shading toward top-left to imply ambient light without specular shine
1342 for (
1343 let r = this.radius * 1.2;
1344 r > this.radius * 0.2;
1345 r -= this.radius * 0.15
1346 ) {
1347 fill(255, 255, 255, 8) // very low alpha
1348 ellipse(-this.radius * 0.25, -this.radius * 0.35, r * 0.25, r * 0.18)
1349 }
1350 // Global matte overlay to reduce plastic look
1351 noStroke()
1352 fill(230, 210, 190, 18)
1353 ellipse(0, 0, this.radius * 2, this.radius * 2)
1354
1355 // Bottom opening of balloon (where flame goes)
1356 fill(40, 20, 10)
1357 ellipse(0, this.radius * 0.9, this.radius * 0.4, this.radius * 0.15)
1358
1359 // Support ropes from balloon to basket
1360 stroke(80, 60, 40)
1361 strokeWeight(2)
1362 // Four support ropes
1363 line(-this.radius * 0.3, this.radius * 0.85, -8, this.radius + 20)
1364 line(this.radius * 0.3, this.radius * 0.85, 8, this.radius + 20)
1365 line(-this.radius * 0.15, this.radius * 0.9, -4, this.radius + 20)
1366 line(this.radius * 0.15, this.radius * 0.9, 4, this.radius + 20)
1367
1368 // FLAME EFFECT (between balloon and basket)
1369 push()
1370 translate(0, this.radius + 10)
1371
1372 // Flame glow
1373 noStroke()
1374 fill(255, 200, 0, 30 + sin(frameCount * 0.2) * 20)
1375 ellipse(0, 0, 30, 30)
1376 fill(255, 150, 0, 50 + sin(frameCount * 0.3) * 30)
1377 ellipse(0, 0, 20, 25)
1378
1379 // Animated flame
1380 push()
1381 let flameHeight = 12 + sin(frameCount * 0.4) * 4
1382 let flameWave = sin(frameCount * 0.3) * 2
1383
1384 // Outer flame (orange)
1385 fill(255, 150, 0)
1386 beginShape()
1387 vertex(-5, 5)
1388 bezierVertex(
1389 -5 + flameWave,
1390 -flameHeight * 0.5,
1391 -2 + flameWave,
1392 -flameHeight * 0.8,
1393 flameWave,
1394 -flameHeight
1395 )
1396 bezierVertex(
1397 2 + flameWave,
1398 -flameHeight * 0.8,
1399 5 + flameWave,
1400 -flameHeight * 0.5,
1401 5,
1402 5
1403 )
1404 endShape(CLOSE)
1405
1406 // Inner flame (yellow/white)
1407 fill(255, 255, 150)
1408 beginShape()
1409 vertex(-2, 5)
1410 bezierVertex(
1411 -2 + flameWave * 0.5,
1412 -flameHeight * 0.3,
1413 -1 + flameWave * 0.5,
1414 -flameHeight * 0.5,
1415 flameWave * 0.5,
1416 -flameHeight * 0.7
1417 )
1418 bezierVertex(
1419 1 + flameWave * 0.5,
1420 -flameHeight * 0.5,
1421 2 + flameWave * 0.5,
1422 -flameHeight * 0.3,
1423 2,
1424 5
1425 )
1426 endShape(CLOSE)
1427
1428 // Flame tip (bright white)
1429 fill(255, 255, 255)
1430 ellipse(flameWave * 0.5, -flameHeight * 0.5, 3, 5)
1431 pop()
1432
1433 pop()
1434
1435 // BIGGER, MORE DETAILED BASKET WITH SWING
1436 push()
1437 translate(0, this.radius + 25)
1438
1439 // ENHANCED: Apply basket swing
1440 if (windActive) {
1441 rotate(this.basketSwing * 0.02)
1442 }
1443
1444 // Basket shadow
1445 noStroke()
1446 fill(0, 0, 0, 20)
1447 rect(-11, 2, 22, 15, 2)
1448
1449 // Main basket - bigger to see ant better
1450 fill(101, 67, 33)
1451 stroke(80, 50, 20)
1452 strokeWeight(1.5)
1453 rect(-10, 0, 20, 14, 2) // Bigger basket
1454
1455 // Woven basket texture
1456 stroke(80, 50, 20, 150)
1457 strokeWeight(1)
1458 // Vertical weaves
1459 for (let i = -8; i < 8; i += 3) {
1460 line(i, 1, i, 13)
1461 }
1462 // Horizontal weaves
1463 for (let i = 3; i < 12; i += 3) {
1464 line(-9, i, 9, i)
1465 }
1466
1467 // Basket rim (thicker, more pronounced)
1468 stroke(60, 40, 20)
1469 strokeWeight(2)
1470 line(-10, 0, 10, 0)
1471
1472 // Corner reinforcements
1473 fill(80, 50, 20)
1474 noStroke()
1475 ellipse(-9, 0, 3)
1476 ellipse(9, 0, 3)
1477
1478 pop()
1479
1480 // DETAILED ANT PILOT (bigger, more visible)
1481 push()
1482 translate(0, this.radius + 28)
1483
1484 // ENHANCED: Ant holds on tighter in wind
1485 if (windActive) {
1486 rotate(-this.basketSwing * 0.01) // Ant leans opposite to basket
1487 }
1488
1489 // Ant body
1490 fill(20)
1491 noStroke()
1492 ellipse(0, 0, 8, 5) // Thorax
1493 ellipse(0, -3, 6, 5) // Head
1494 ellipse(0, 3, 7, 6) // Abdomen
1495
1496 // Ant eyes
1497 fill(255, 100, 100)
1498 ellipse(-2, -3, 2)
1499 ellipse(2, -3, 2)
1500
1501 // Antennae
1502 stroke(20)
1503 strokeWeight(1)
1504 line(-1, -5, -3, -8)
1505 line(1, -5, 3, -8)
1506
1507 // Little ant arms holding basket edge
1508 strokeWeight(1.5)
1509 line(-3, 0, -6, -3)
1510 line(3, 0, 6, -3)
1511
1512 // Ant legs visible over basket edge
1513 line(-2, 2, -4, 5)
1514 line(2, 2, 4, 5)
1515
1516 // Optional: Tiny pilot goggles
1517 stroke(100, 50, 0)
1518 strokeWeight(1)
1519 noFill()
1520 ellipse(-2, -3, 3)
1521 ellipse(2, -3, 3)
1522 line(-0.5, -3, 0.5, -3)
1523
1524 pop()
1525
1526 // Sandbags hanging from basket (optional detail)
1527 push()
1528 translate(0, this.radius + 25)
1529
1530 // ENHANCED: Sandbags swing in wind
1531 if (windActive) {
1532 rotate(this.basketSwing * 0.03)
1533 }
1534
1535 fill(80, 60, 40)
1536 noStroke()
1537 ellipse(-12, 10, 4, 5)
1538 ellipse(12, 10, 4, 5)
1539 // Sandbag ropes
1540 stroke(60, 40, 20)
1541 strokeWeight(0.5)
1542 line(-10, 7, -12, 10)
1543 line(10, 7, 12, 10)
1544 pop()
1545
1546 pop()
1547 } else if (this.type === 'beetle') {
1548 push()
1549 rotate(this.rotation)
1550
1551 // Shadow
1552 noStroke()
1553 fill(0, 0, 0, 40)
1554 ellipse(3, 3, this.radius * 1.8, this.radius * 2.2)
1555
1556 // Wings - always visible and flapping since they're floating
1557 push()
1558 // Wing flap animation
1559 let wingAngle = sin(this.wingPhase) * 0.3
1560 let wingSpread = 15 + sin(this.wingPhase) * 10
1561
1562 // Left wing
1563 push()
1564 translate(-this.radius * 0.4, 0)
1565 rotate(-wingAngle)
1566 fill(255, 255, 255, 120)
1567 stroke(0, 0, 0, 100)
1568 strokeWeight(0.5)
1569 ellipse(-wingSpread * 0.7, 0, wingSpread * 1.2, 15)
1570 // Wing details
1571 noStroke()
1572 fill(200, 200, 200, 80)
1573 ellipse(-wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1574 pop()
1575
1576 // Right wing
1577 push()
1578 translate(this.radius * 0.4, 0)
1579 rotate(wingAngle)
1580 fill(255, 255, 255, 120)
1581 stroke(0, 0, 0, 100)
1582 strokeWeight(0.5)
1583 ellipse(wingSpread * 0.7, 0, wingSpread * 1.2, 15)
1584 // Wing details
1585 noStroke()
1586 fill(200, 200, 200, 80)
1587 ellipse(wingSpread * 0.6, 0, wingSpread * 0.8, 10)
1588 pop()
1589
1590 // Extra glow at night
1591 if (gamePhase === 'NIGHT') {
1592 noStroke()
1593 fill(255, 255, 200, 30 + sin(this.wingPhase * 2) * 20)
1594 ellipse(0, 0, this.radius * 3, this.radius * 2)
1595 }
1596 pop()
1597
1598 // Main beetle body (on top of wings)
1599 fill(
1600 red(this.beetleColor),
1601 green(this.beetleColor),
1602 blue(this.beetleColor)
1603 )
1604 stroke(0)
1605 strokeWeight(2)
1606 ellipse(0, 0, this.radius * 1.6, this.radius * 2)
1607
1608 // Shell split line
1609 stroke(0)
1610 strokeWeight(1)
1611 line(0, -this.radius, 0, this.radius)
1612
1613 // Head
1614 fill(10)
1615 ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6)
1616
1617 // Spots/pattern
1618 noStroke()
1619 fill(0, 0, 0, 80)
1620 ellipse(-this.radius * 0.3, 0, this.radius * 0.4)
1621 ellipse(this.radius * 0.3, -this.radius * 0.2, this.radius * 0.3)
1622 ellipse(this.radius * 0.2, this.radius * 0.4, this.radius * 0.35)
1623 ellipse(-this.radius * 0.25, this.radius * 0.3, this.radius * 0.25)
1624
1625 // Tiny tucked legs
1626 stroke(0)
1627 strokeWeight(1)
1628 line(
1629 -this.radius * 0.5,
1630 -this.radius * 0.2,
1631 -this.radius * 0.6,
1632 -this.radius * 0.1
1633 )
1634 line(
1635 this.radius * 0.5,
1636 -this.radius * 0.2,
1637 this.radius * 0.6,
1638 -this.radius * 0.1
1639 )
1640 line(
1641 -this.radius * 0.5,
1642 this.radius * 0.2,
1643 -this.radius * 0.6,
1644 this.radius * 0.1
1645 )
1646 line(
1647 this.radius * 0.5,
1648 this.radius * 0.2,
1649 this.radius * 0.6,
1650 this.radius * 0.1
1651 )
1652
1653 // Antennae
1654 strokeWeight(1)
1655 line(-3, -this.radius * 1.1, -8, -this.radius * 1.4)
1656 line(3, -this.radius * 1.1, 8, -this.radius * 1.4)
1657
1658 // Eyes
1659 fill(255, 0, 0)
1660 noStroke()
1661 ellipse(-5, -this.radius * 0.7, 5)
1662 ellipse(5, -this.radius * 0.7, 5)
1663 // Eye shine
1664 fill(255, 150, 150)
1665 ellipse(-4, -this.radius * 0.72, 2)
1666 ellipse(6, -this.radius * 0.72, 2)
1667
1668 pop()
1669 } else if (this.type === 'leaf') {
1670 rotate(this.rotation)
1671
1672 if (gamePhase === 'NIGHT') {
1673 fill(20, 40, 20)
1674 stroke(10, 20, 10)
1675 } else {
1676 fill(34, 139, 34)
1677 stroke(25, 100, 25)
1678 }
1679 strokeWeight(2)
1680
1681 beginShape()
1682 for (let point of this.leafPoints) {
1683 let x = cos(point.angle) * point.radius
1684 let y = sin(point.angle) * point.radius
1685 curveVertex(x, y)
1686 }
1687 let firstPoint = this.leafPoints[0]
1688 curveVertex(
1689 cos(firstPoint.angle) * firstPoint.radius,
1690 sin(firstPoint.angle) * firstPoint.radius
1691 )
1692 let secondPoint = this.leafPoints[1]
1693 curveVertex(
1694 cos(secondPoint.angle) * secondPoint.radius,
1695 sin(secondPoint.angle) * secondPoint.radius
1696 )
1697 endShape()
1698
1699 stroke(25, 100, 25, 100)
1700 strokeWeight(1)
1701 line(0, -this.radius, 0, this.radius)
1702 line(0, 0, -this.radius / 2, -this.radius / 2)
1703 line(0, 0, this.radius / 2, -this.radius / 2)
1704 line(0, 0, -this.radius / 2, this.radius / 2)
1705 line(0, 0, this.radius / 2, this.radius / 2)
1706 }
1707
1708 pop()
1709 }
1710 }
1711
1712 class FoodBox {
1713 constructor (x, y) {
1714 this.pos = createVector(x, y)
1715 this.radius = 10
1716 this.collected = false
1717 this.floatOffset = random(TWO_PI)
1718 this.silkValue = random(20, 35)
1719 this.glowPhase = random(TWO_PI)
1720 }
1721
1722 collect () {
1723 webSilk = min(webSilk + this.silkValue, maxWebSilk)
1724
1725 for (let i = 0; i < 8; i++) {
1726 particles.push(new Particle(this.pos.x, this.pos.y))
1727 }
1728 }
1729
1730 display () {
1731 push()
1732 let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3
1733 translate(this.pos.x, this.pos.y + floatY)
1734
1735 let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50
1736 noStroke()
1737 fill(255, 200, 100, glowIntensity * 0.3)
1738 ellipse(0, 0, 40)
1739 fill(255, 220, 150, glowIntensity * 0.5)
1740 ellipse(0, 0, 25)
1741
1742 rectMode(CENTER)
1743
1744 fill(0, 0, 0, 50)
1745 rect(2, 2, this.radius * 2, this.radius * 1.8, 3)
1746
1747 fill(139, 69, 19)
1748 stroke(100, 50, 0)
1749 strokeWeight(1)
1750 rect(0, 0, this.radius * 2, this.radius * 1.8, 3)
1751
1752 stroke(100, 50, 0)
1753 strokeWeight(1)
1754 line(-this.radius, 0, this.radius, 0)
1755 line(0, -this.radius * 0.9, 0, this.radius * 0.9)
1756
1757 noStroke()
1758 fill(255, 200, 100)
1759 ellipse(-5, -4, 4)
1760 ellipse(5, -4, 3)
1761 ellipse(-4, 5, 3)
1762 ellipse(4, 4, 4)
1763
1764 pop()
1765 }
1766 }
1767
1768 class Bird {
1769 constructor (pattern, isThief = false) {
1770 this.pattern = pattern // 'dive', 'swoop', 'glide', 'circle'
1771 this.isThief = isThief
1772 this.active = false
1773 this.attacking = false
1774 this.attackDelay = isThief ? 120 : random(30, 90) // MUCH shorter initial delay
1775
1776 // Position and movement
1777 this.x = random(width)
1778 this.y = -50 // Start above screen
1779 this.vx = 0
1780 this.vy = 0
1781 this.targetX = 0
1782 this.targetY = 0
1783 this.speed = 5 // Increased from 3
1784 this.angle = 0
1785 this.wingPhase = random(TWO_PI)
1786
1787 // Visual properties
1788 this.size = isThief ? 25 : 20
1789 this.color = isThief ? color(100, 50, 150) : color(50, 50, 50)
1790
1791 // Pattern-specific properties
1792 if (pattern === 'circle') {
1793 this.circleRadius = 150
1794 this.circleAngle = 0
1795 this.circleCenter = createVector(width / 2, height / 2)
1796 }
1797
1798 // Attack properties - MUCH MORE AGGRESSIVE
1799 this.diveSpeed = 12 // Increased from 8
1800 this.retreatSpeed = 6 // Increased from 4
1801 this.state = 'waiting' // 'waiting', 'approaching', 'attacking', 'retreating'
1802 this.consecutiveAttacks = 0 // Track multiple attacks
1803 this.maxConsecutiveAttacks = random(2, 4) // Each bird does 2-4 attacks before retreating
1804 }
1805
1806 update () {
1807 // Update wing animation
1808 this.wingPhase += 0.3 // Faster wing flapping
1809
1810 this.avoidObstacles()
1811
1812 // Countdown to attack - MUCH FASTER
1813 if (this.attackDelay > 0) {
1814 this.attackDelay--
1815 // Hover while waiting - more aggressive hovering
1816 this.y = -30 + sin(frameCount * 0.08) * 15
1817 this.x += sin(frameCount * 0.05) * 3
1818
1819 // Show warning when about to attack
1820 if (this.attackDelay < 30) {
1821 this.y = lerp(this.y, 50, 0.1) // Start moving into view
1822 }
1823 return
1824 }
1825
1826 // Activate after delay
1827 if (!this.active) {
1828 this.active = true
1829 this.state = 'approaching'
1830 this.updateTarget() // Set initial target
1831 }
1832
1833 // Execute movement pattern
1834 switch (this.pattern) {
1835 case 'dive':
1836 this.executeDivePattern()
1837 break
1838 case 'swoop':
1839 this.executeSwoopPattern()
1840 break
1841 case 'glide':
1842 this.executeGlidePattern()
1843 break
1844 case 'circle':
1845 this.executeCirclePattern()
1846 break
1847 }
1848
1849 // Check collisions
1850 this.checkCollisions()
1851
1852 // Keep on screen during approach
1853 if (this.state === 'approaching') {
1854 this.x = constrain(this.x, 20, width - 20)
1855 }
1856 }
1857
1858 updateTarget () {
1859 if (this.isThief) {
1860 // Target caught flies
1861 let caughtFlies = flies.filter(f => f.stuck || f.caught)
1862 if (caughtFlies.length > 0) {
1863 let target = random(caughtFlies)
1864 this.targetX = target.pos.x
1865 this.targetY = target.pos.y
1866 } else {
1867 this.active = false // No targets, deactivate
1868 return
1869 }
1870 } else {
1871 // Heavily favor targeting spider (90% chance)
1872 if (random() < 0.9) {
1873 // Target spider with prediction
1874 this.targetX = spider.pos.x + spider.vel.x * 10 // Predict where spider will be
1875 this.targetY = spider.pos.y + spider.vel.y * 10
1876 } else {
1877 // Occasionally target a web strand
1878 if (webStrands.length > 0) {
1879 let strand = random(webStrands.filter(s => !s.broken))
1880 if (strand && strand.path && strand.path.length > 0) {
1881 let point = random(strand.path)
1882 this.targetX = point.x
1883 this.targetY = point.y
1884 }
1885 }
1886 }
1887 }
1888 }
1889
1890 executeDivePattern () {
1891 if (this.state === 'approaching') {
1892 // Move into position above target
1893 let dx = this.targetX - this.x
1894 let dy = 50 - this.y
1895
1896 this.x += dx * 0.15
1897 this.y += dy * 0.15
1898
1899 // When in position, start diving
1900 if (abs(dx) < 50 && abs(dy) < 30) {
1901 this.state = 'attacking'
1902 this.attacking = true
1903 // Initialize diveFrames and pullUpY for new attack
1904 this.diveFrames = 0
1905 // Calculate pullUpY: spider.pos.y + spider.radius + 8, but not below canvas
1906 let candidatePullUpY = spider.pos.y + spider.radius + 8
1907 this.pullUpY = Math.min(candidatePullUpY, height - 12)
1908 this.updateTarget()
1909 }
1910 } else if (this.state === 'attacking') {
1911 // Track how many frames we've been attacking
1912 if (typeof this.diveFrames !== 'number') this.diveFrames = 0
1913 this.diveFrames++
1914
1915 let dx = this.targetX - this.x
1916 let dy = this.targetY - this.y
1917
1918 // Accelerate toward target with better tracking
1919 this.vx = dx * 0.1 // Better horizontal tracking
1920 this.vy = min(this.diveSpeed * 1.5, this.vy + 1.5) // Faster acceleration
1921
1922 this.x += this.vx
1923 this.y += this.vy
1924
1925 // Update target position while diving
1926 if (frameCount % 8 === 0) {
1927 this.targetX = spider.pos.x
1928 // Keep steering intent downward; don't let target sit above our per-dive floor
1929 this.targetY =
1930 typeof this.pullUpY === 'number'
1931 ? Math.min(spider.pos.y, this.pullUpY - 4)
1932 : spider.pos.y
1933 }
1934
1935 // Only consider bailing out after a minimum number of dive frames
1936 let canBailOut = this.diveFrames > 22
1937 // Use pullUpY for stable bailout check
1938 let reachedPullUpY =
1939 typeof this.pullUpY === 'number' ? this.y > this.pullUpY : false
1940 let reachedBottom = this.y > height - 20 // Go almost to canvas bottom
1941 const hitCollision =
1942 dist(this.x, this.y, spider.pos.x, spider.pos.y) <=
1943 this.size * 0.5 + spider.radius
1944 const nearButNotHit =
1945 !hitCollision &&
1946 abs(this.x - spider.pos.x) < 30 &&
1947 abs(this.y - spider.pos.y) < 24
1948
1949 if (canBailOut && (hitCollision || reachedPullUpY || reachedBottom)) {
1950 // Convert near-miss near the floor into a sweep instead of an early bail
1951 if (
1952 !hitCollision &&
1953 reachedPullUpY &&
1954 spider.pos.y > height - 30 &&
1955 !this.sweeping
1956 ) {
1957 this.sweeping = true
1958 this.y = spider.pos.y // lock to spider height
1959 this.vy = 0
1960 const sweepDirection = spider.pos.x > this.x ? 1 : -1
1961 this.vx = sweepDirection * 8
1962 setTimeout(() => {
1963 this.sweeping = false
1964 this.state = 'retreating'
1965 this.attacking = false
1966 this.diveFrames = 0
1967 this.pullUpY = null
1968 }, 500)
1969 return // skip normal bailout path
1970 }
1971 // If spider is at bottom and we haven't hit it yet, do a horizontal sweep
1972 if (spider.pos.y > height - 30 && !hitCollision && !this.sweeping) {
1973 this.sweeping = true
1974 this.y = spider.pos.y // Match spider height
1975 this.vy = 0 // Stop vertical movement
1976
1977 // Sweep horizontally toward spider
1978 let sweepDirection = spider.pos.x > this.x ? 1 : -1
1979 this.vx = sweepDirection * 8
1980
1981 // Continue sweep for a bit
1982 setTimeout(() => {
1983 this.sweeping = false
1984 this.state = 'retreating'
1985 this.attacking = false
1986 this.diveFrames = 0
1987 this.pullUpY = null
1988 }, 500) // Sweep for 0.5 seconds
1989 } else if (!this.sweeping) {
1990 // Normal attack completion
1991 this.consecutiveAttacks++
1992
1993 if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
1994 // Quick pull up and attack again
1995 this.state = 'approaching'
1996 this.attacking = false
1997 this.y = min(this.y, height - 50)
1998 this.diveFrames = 0
1999 this.pullUpY = null
2000 this.updateTarget()
2001 } else {
2002 // Finally retreat
2003 this.state = 'retreating'
2004 this.attacking = false
2005 this.diveFrames = 0
2006 this.pullUpY = null
2007 }
2008 }
2009 }
2010
2011 // Safety: Don't go below canvas
2012 if (this.y > height - 10) {
2013 this.y = height - 10
2014 if (!this.sweeping) {
2015 this.state = 'retreating'
2016 this.attacking = false
2017 this.diveFrames = 0
2018 this.pullUpY = null
2019 }
2020 }
2021 } else if (this.state === 'retreating') {
2022 // Clear sweep flag
2023 this.sweeping = false
2024 this.diveFrames = 0
2025 this.pullUpY = null
2026
2027 // Fly back up
2028 this.vy = -this.retreatSpeed
2029 this.y += this.vy
2030 this.x += sin(frameCount * 0.1) * 2
2031
2032 // Reset when off screen
2033 if (this.y < -50) {
2034 this.state = 'approaching'
2035 this.attackDelay = random(60, 120)
2036 this.x = random(width)
2037 this.consecutiveAttacks = 0
2038 this.maxConsecutiveAttacks = random(2, 4)
2039 this.diveFrames = 0
2040 this.pullUpY = null
2041 }
2042 }
2043 }
2044
2045 executeSwoopPattern () {
2046 if (this.state === 'approaching') {
2047 if (this.x < 0) {
2048 this.x += 8
2049 this.y = height * 0.3 + sin(this.x * 0.03) * 50
2050 } else {
2051 this.state = 'attacking'
2052 this.attacking = true
2053 this.updateTarget()
2054 }
2055 } else if (this.state === 'attacking') {
2056 this.x += 9
2057
2058 // FIX: Adjust swoop pattern if spider is at bottom
2059 if (spider.pos.y > height - 50) {
2060 // Lower swoop pattern for bottom spiders
2061 this.y = height * 0.7 + sin(this.x * 0.03) * 50
2062 } else {
2063 // Normal swoop
2064 this.y = height * 0.3 + sin(this.x * 0.03) * 120
2065 }
2066
2067 // Track toward target when close
2068 if (abs(this.x - this.targetX) < 100) {
2069 let dy = this.targetY - this.y
2070 this.y += dy * 0.2
2071 }
2072
2073 // Avoid going below canvas
2074 this.y = min(this.y, height - 15)
2075
2076 // Exit screen
2077 if (this.x > width + 50) {
2078 this.consecutiveAttacks++
2079
2080 if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
2081 this.x = -50
2082 this.state = 'approaching'
2083 this.updateTarget()
2084 } else {
2085 this.state = 'retreating'
2086 this.attacking = false
2087 }
2088 }
2089 } else if (this.state === 'retreating') {
2090 this.state = 'approaching'
2091 this.attackDelay = random(90, 150)
2092 this.x = -50
2093 this.consecutiveAttacks = 0
2094 this.maxConsecutiveAttacks = random(2, 4)
2095 }
2096 }
2097
2098 avoidObstacles () {
2099 // Check collision with all obstacles
2100 for (let obstacle of obstacles) {
2101 let d = dist(this.x, this.y, obstacle.x, obstacle.y)
2102
2103 // If too close to an obstacle, push away
2104 if (d < obstacle.radius + this.size + 10) {
2105 // Calculate push direction (away from obstacle)
2106 let pushX = (this.x - obstacle.x) / d
2107 let pushY = (this.y - obstacle.y) / d
2108
2109 // Apply push force
2110 this.x += pushX * 5
2111 this.y += pushY * 5
2112
2113 // If stuck for too long, teleport away
2114 if (this.stuckCounter === undefined) {
2115 this.stuckCounter = 0
2116 }
2117 this.stuckCounter++
2118
2119 if (this.stuckCounter > 30) {
2120 // Stuck for 0.5 seconds
2121 // Teleport to a safe position
2122 this.y = obstacle.y - obstacle.radius - 30
2123 this.x = obstacle.x + random(-50, 50)
2124 this.stuckCounter = 0
2125
2126 // If attacking, abort and retry
2127 if (this.state === 'attacking') {
2128 this.state = 'approaching'
2129 this.attacking = false
2130 }
2131 }
2132 } else {
2133 this.stuckCounter = 0 // Reset counter when not stuck
2134 }
2135 }
2136
2137 // Also check home branch collision
2138 if (window.homeBranch && this.y > window.homeBranch.y - 40) {
2139 // Check if bird is in branch X range
2140 let branchStart = Math.min(
2141 window.homeBranch.startX,
2142 window.homeBranch.endX
2143 )
2144 let branchEnd = Math.max(window.homeBranch.startX, window.homeBranch.endX)
2145
2146 if (this.x >= branchStart - 20 && this.x <= branchEnd + 20) {
2147 // Bird is too close to branch, push up
2148 this.y = window.homeBranch.y - 40
2149
2150 // If diving, abort dive
2151 if (this.state === 'attacking' && this.pattern === 'dive') {
2152 this.state = 'retreating'
2153 this.attacking = false
2154 }
2155 }
2156 }
2157 }
2158
2159 executeGlidePattern () {
2160 if (this.state === 'approaching') {
2161 // Glide in from top corner faster
2162 this.x += 5
2163 this.y += 2.5
2164
2165 if (this.y > height * 0.15) {
2166 this.state = 'attacking'
2167 this.attacking = true
2168 this.updateTarget()
2169 }
2170 } else if (this.state === 'attacking') {
2171 // Glide toward target aggressively
2172 let dx = this.targetX - this.x
2173 let dy = this.targetY - this.y
2174 let dist = sqrt(dx * dx + dy * dy)
2175
2176 if (dist > 10) {
2177 this.x += (dx / dist) * 7 // Much faster glide
2178 this.y += (dy / dist) * 7
2179 }
2180
2181 // Pass through and maybe attack again
2182 if (this.y > height - 100 || this.x < -50 || this.x > width + 50) {
2183 this.consecutiveAttacks++
2184
2185 if (this.consecutiveAttacks < this.maxConsecutiveAttacks) {
2186 // Reset for another pass
2187 this.state = 'approaching'
2188 this.x = random() < 0.5 ? -50 : width + 50
2189 this.y = random(50, 150)
2190 this.updateTarget()
2191 } else {
2192 this.state = 'retreating'
2193 this.attacking = false
2194 }
2195 }
2196 } else if (this.state === 'retreating') {
2197 // Continue off screen
2198 this.x += this.vx
2199 this.y += this.vy
2200
2201 // Reset
2202 if (this.y > height + 50 || this.x < -100 || this.x > width + 100) {
2203 this.state = 'approaching'
2204 this.attackDelay = random(120, 180)
2205 this.x = random() < 0.5 ? -50 : width + 50
2206 this.y = random(50, 150)
2207 this.vx = this.x < width / 2 ? 5 : -5
2208 this.vy = 2.5
2209 this.consecutiveAttacks = 0
2210 this.maxConsecutiveAttacks = random(2, 4)
2211 }
2212 }
2213 }
2214
2215 executeCirclePattern () {
2216 if (this.state === 'approaching') {
2217 // Move to circle start position
2218 let startX = this.circleCenter.x + cos(0) * this.circleRadius
2219 let startY = this.circleCenter.y + sin(0) * this.circleRadius
2220
2221 let dx = startX - this.x
2222 let dy = startY - this.y
2223
2224 this.x += dx * 0.1
2225 this.y += dy * 0.1
2226
2227 if (abs(dx) < 20 && abs(dy) < 20) {
2228 this.state = 'attacking'
2229 this.attacking = true
2230 this.circleAngle = 0
2231 }
2232 } else if (this.state === 'attacking') {
2233 // Circle around center FASTER
2234 this.circleAngle += 0.08 // Faster circling
2235 this.x = this.circleCenter.x + cos(this.circleAngle) * this.circleRadius
2236 this.y = this.circleCenter.y + sin(this.circleAngle) * this.circleRadius
2237
2238 // More frequent dives toward center
2239 if (frameCount % 60 === 0) {
2240 // Every second instead of every 2 seconds
2241 this.circleRadius = max(30, this.circleRadius - 50)
2242 } else {
2243 this.circleRadius = min(150, this.circleRadius + 2)
2244 }
2245
2246 // Complete circle faster
2247 if (this.circleAngle > TWO_PI * 1.5) {
2248 // 1.5 circles instead of 2
2249 this.state = 'retreating'
2250 this.attacking = false
2251 }
2252 } else if (this.state === 'retreating') {
2253 // Fly away
2254 this.y -= 7
2255
2256 if (this.y < -50) {
2257 this.state = 'approaching'
2258 this.attackDelay = random(150, 240)
2259 this.x = random(width)
2260 }
2261 }
2262 }
2263
2264 checkCollisions () {
2265 // FIX: Increased collision radius for more generous hit detection
2266 let collisionDistance = this.size + spider.radius + 5 // Added 5 pixel buffer
2267
2268 // Check collision with spider
2269 if (
2270 this.attacking &&
2271 dist(this.x, this.y, spider.pos.x, spider.pos.y) < collisionDistance
2272 ) {
2273 // Hit spider!
2274 if (gamePhase === 'DAWN') {
2275 // Calculate damage
2276 let damage = 20 // Base damage
2277
2278 // If spider has no stamina, GAME OVER!
2279 if (jumpStamina <= 0) {
2280 triggerGameOver('Oof')
2281 return
2282 }
2283
2284 // Otherwise, reduce stamina
2285 jumpStamina = max(0, jumpStamina - damage)
2286 stats.birdHitsTaken++
2287
2288 // Knockback effect
2289 spider.vel.x = (spider.pos.x - this.x) * 0.3
2290 spider.vel.y = -3
2291 spider.isAirborne = true
2292
2293 // Red damage particles
2294 for (let i = 0; i < 12; i++) {
2295 let p = new Particle(spider.pos.x, spider.pos.y)
2296 p.color = color(255, 50, 50)
2297 p.vel = createVector(random(-4, 4), random(-4, 1))
2298 p.size = random(4, 8)
2299 particles.push(p)
2300 }
2301
2302 // Screen shake effect
2303 if (typeof screenShake !== 'undefined') {
2304 screenShake = 10
2305 }
2306
2307 // Warning notifications - but limited to prevent spam
2308 if (notifications.length < 3) {
2309 if (jumpStamina <= 20) {
2310 notifications.push(
2311 new Notification('CRITICAL STAMINA!', color(255, 50, 50))
2312 )
2313 } else if (jumpStamina <= 40) {
2314 notifications.push(
2315 new Notification('Low stamina - find cover!', color(255, 150, 50))
2316 )
2317 }
2318 }
2319
2320 // POINTS: Award 2 points for surviving a bird attack
2321 playerPoints += 2
2322 }
2323
2324 // Bird bounces off
2325 this.state = 'retreating'
2326 this.attacking = false
2327 }
2328
2329 // Check collision with web strands
2330 if (this.attacking) {
2331 for (let strand of webStrands) {
2332 if (!strand.broken && strand.path) {
2333 for (let point of strand.path) {
2334 if (dist(this.x, this.y, point.x, point.y) < this.size) {
2335 // Bird breaks the strand!
2336 strand.broken = true
2337 stats.strandsLostInNight++
2338
2339 // Particles
2340 for (let i = 0; i < 5; i++) {
2341 let p = new Particle(point.x, point.y)
2342 p.color = color(255, 255, 255)
2343 p.vel = createVector(random(-2, 2), random(-2, 2))
2344 particles.push(p)
2345 }
2346 break
2347 }
2348 }
2349 }
2350 }
2351 }
2352
2353 // Thief bird steals flies
2354 if (this.isThief && this.attacking) {
2355 for (let i = flies.length - 1; i >= 0; i--) {
2356 let fly = flies[i]
2357 if (
2358 (fly.stuck || fly.caught) &&
2359 dist(this.x, this.y, fly.pos.x, fly.pos.y) < this.size + 10
2360 ) {
2361 // Steal the fly!
2362 flies.splice(i, 1)
2363
2364 // Purple particles for theft
2365 for (let j = 0; j < 6; j++) {
2366 let p = new Particle(fly.pos.x, fly.pos.y)
2367 p.color = color(200, 100, 255)
2368 p.vel = createVector(random(-2, 2), random(-2, 2))
2369 particles.push(p)
2370 }
2371
2372 // Thief escapes after stealing
2373 this.state = 'retreating'
2374 this.attacking = false
2375 this.active = false // Deactivate thief after successful theft
2376 break
2377 }
2378 }
2379 }
2380 }
2381
2382 display () {
2383 push()
2384 translate(this.x, this.y)
2385
2386 // Show if bird is stuck (for debugging)
2387 if (this.stuckCounter > 15) {
2388 // Flash red when stuck
2389 push()
2390 noFill()
2391 stroke(255, 0, 0, 100)
2392 strokeWeight(2)
2393 ellipse(0, 0, this.size * 3)
2394 pop()
2395 }
2396
2397 // Rotate based on movement
2398 if (this.state === 'attacking' && this.pattern === 'dive') {
2399 rotate(PI / 2) // Point down when diving
2400 } else if (this.vx !== 0) {
2401 rotate(atan2(this.vy, this.vx))
2402 }
2403
2404 // Shadow
2405 push()
2406 noStroke()
2407 fill(0, 0, 0, 30)
2408 ellipse(5, 5, this.size * 2)
2409 pop()
2410
2411 // Wings
2412 let wingSpread = sin(this.wingPhase) * this.size * 0.8
2413
2414 // Wing shadows
2415 noStroke()
2416 fill(0, 0, 0, 40)
2417 ellipse(-wingSpread + 2, 2, this.size * 1.5, this.size * 0.5)
2418 ellipse(wingSpread + 2, 2, this.size * 1.5, this.size * 0.5)
2419
2420 // Wings
2421 fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80))
2422 ellipse(-wingSpread, 0, this.size * 1.5, this.size * 0.5)
2423 ellipse(wingSpread, 0, this.size * 1.5, this.size * 0.5)
2424
2425 // Body
2426 fill(this.isThief ? color(100, 50, 150) : color(50, 50, 50))
2427 ellipse(0, 0, this.size * 0.8, this.size)
2428
2429 // Head
2430 fill(this.isThief ? color(80, 40, 120) : color(30, 30, 30))
2431 ellipse(0, -this.size * 0.4, this.size * 0.5)
2432
2433 // Eye
2434 fill(this.isThief ? color(255, 100, 255) : color(255, 100, 100))
2435 noStroke()
2436 ellipse(3, -this.size * 0.4, 4)
2437
2438 // Beak
2439 fill(this.isThief ? color(200, 150, 50) : color(200, 150, 0))
2440 triangle(
2441 this.size * 0.25,
2442 -this.size * 0.4,
2443 this.size * 0.45,
2444 -this.size * 0.35,
2445 this.size * 0.25,
2446 -this.size * 0.3
2447 )
2448
2449 // Tail feathers
2450 fill(this.isThief ? color(120, 70, 180) : color(80, 80, 80))
2451 for (let i = -1; i <= 1; i++) {
2452 push()
2453 translate(-this.size * 0.3, this.size * 0.3)
2454 rotate(i * 0.2)
2455 ellipse(0, 0, this.size * 0.3, this.size * 0.8)
2456 pop()
2457 }
2458
2459 // Warning indicator if attacking
2460 if (this.attacking && frameCount % 20 < 10) {
2461 noFill()
2462 stroke(255, 100, 100, 150)
2463 strokeWeight(2)
2464 ellipse(0, 0, this.size * 2.5)
2465 }
2466
2467 pop()
2468 }
2469 }
2470
2471 class Particle {
2472 constructor (x, y) {
2473 this.pos = createVector(x, y)
2474 this.vel = createVector(random(-3, 3), random(-5, -2))
2475 this.lifetime = 255
2476 this.color = color(255, random(200, 255), random(100, 200))
2477 this.size = 6 // Default size
2478 }
2479
2480 update () {
2481 this.vel.y += 0.2
2482 this.pos.add(this.vel)
2483 this.lifetime -= 8
2484 }
2485
2486 display () {
2487 push()
2488 noStroke()
2489 fill(red(this.color), green(this.color), blue(this.color), this.lifetime)
2490 ellipse(this.pos.x, this.pos.y, this.size)
2491 pop()
2492 }
2493
2494 isDead () {
2495 return this.lifetime <= 0
2496 }
2497 }