JavaScript · 30405 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 }
18
19 jump(targetX, targetY) {
20 if (!this.canJump) return;
21
22 let direction = createVector(targetX - this.pos.x, targetY - this.pos.y);
23 let clickDistance = direction.mag();
24 direction.normalize();
25
26 // Scale jump power based on click distance (closer clicks = smaller jumps)
27 let actualJumpPower = map(clickDistance, 0, 200, 3, this.jumpPower);
28 actualJumpPower = constrain(actualJumpPower, 3, this.jumpPower);
29 direction.mult(actualJumpPower);
30
31 this.vel = direction;
32 this.isAirborne = true;
33 this.canJump = false;
34 this.lastAnchorPoint = this.pos.copy();
35
36 // Check if we're jumping off a web strand
37 for (let strand of webStrands) {
38 if (strand === currentStrand) continue;
39
40 if (this.checkStrandCollision(strand)) {
41 // Much simpler shimmy detection based on actual jump power used
42 let isShimmy = actualJumpPower < 6; // If we used less than half power, it's a shimmy
43
44 // Apply appropriate recoil based on movement type
45 if (isShimmy) {
46 // Trigger shimmy visual effect
47 this.shimmyEffect = 20;
48
49 // NO recoil at all for shimmying - just tiny vibration
50 strand.vibrate(0.3);
51
52 // Tiny yellow particles
53 let p = new Particle(this.pos.x, this.pos.y);
54 p.color = color(255, 255, 100, 80);
55 p.vel = createVector(random(-0.3, 0.3), random(-0.3, 0.3));
56 p.size = 2;
57 particles.push(p);
58 } else {
59 // Scale recoil based on actual jump power
60 let recoilForce = -(actualJumpPower / this.jumpPower) * 0.08; // Scale by power ratio
61 strand.applyRecoil(recoilForce);
62
63 // Create particles only for real jumps
64 for (let i = 0; i < 2; i++) {
65 let p = new Particle(this.pos.x, this.pos.y);
66 p.color = color(255, 255, 255, 120);
67 p.vel = createVector(random(-0.8, 0.8), random(1, 2));
68 p.size = 3;
69 particles.push(p);
70 }
71 }
72
73 break;
74 }
75 }
76 }
77
78 munch () {
79 if (this.munchCooldown > 0) return
80
81 isMunching = true
82 this.munchCooldown = 30
83
84 for (let i = flies.length - 1; i >= 0; i--) {
85 let fly = flies[i]
86 let d = dist(this.pos.x, this.pos.y, fly.pos.x, fly.pos.y)
87 if (d < this.munchRadius) {
88 fliesMunched++
89 webSilk = min(webSilk + 15, maxWebSilk)
90
91 for (let j = 0; j < 12; j++) {
92 let p = new Particle(fly.pos.x, fly.pos.y)
93 p.color = color(255, random(100, 255), 0)
94 particles.push(p)
95 }
96
97 flies.splice(i, 1)
98 break
99 }
100 }
101 }
102
103 update () {
104 if (this.isAirborne) {
105 this.acc.add(this.gravity)
106 }
107
108 this.vel.add(this.acc)
109 this.vel.limit(this.maxSpeed)
110 this.pos.add(this.vel)
111 this.acc.mult(0)
112
113 if (this.munchCooldown > 0) {
114 this.munchCooldown--
115 if (this.munchCooldown === 0) {
116 isMunching = false
117 }
118 }
119
120 // Check ground collision
121 if (this.pos.y >= height - this.radius) {
122 this.pos.y = height - this.radius
123 this.land()
124 }
125
126 // Check wall collisions
127 if (this.pos.x <= this.radius || this.pos.x >= width - this.radius) {
128 this.pos.x = constrain(this.pos.x, this.radius, width - this.radius)
129 this.vel.x *= -0.5
130 }
131
132 // Check ceiling
133 if (this.pos.y <= this.radius) {
134 this.pos.y = this.radius
135 this.vel.y *= -0.5 // Bounce off ceiling, don't land
136 }
137
138 // Check home branch collision (one-way platform)
139 if (window.homeBranch && this.isAirborne && this.vel.y > 0.1) {
140 // Only when actually falling
141 let branch = window.homeBranch
142
143 // Check if spider is within branch X range
144 let branchStart = Math.min(branch.startX, branch.endX)
145 let branchEnd = Math.max(branch.startX, branch.endX)
146
147 // Since the branch angle is very small (0.05 radians ≈ 3 degrees),
148 // we can use a simpler approximation
149 if (this.pos.x >= branchStart - 10 && this.pos.x <= branchEnd + 10) {
150 // Calculate position along branch (0 to 1)
151 let t = (this.pos.x - branchStart) / (branchEnd - branchStart)
152 t = constrain(t, 0, 1)
153
154 // Branch visual thickness tapers from full at start to 35% at end
155 // This matches exactly how it's drawn in the bezier curves
156 let branchTopThickness = lerp(
157 branch.thickness * 0.9,
158 branch.thickness * 0.35,
159 t
160 )
161
162 // The branch is drawn centered at branch.y
163 // With small angle approximation: the top of the branch is at
164 let branchSurfaceY = branch.y - branchTopThickness
165
166 // Add slight angle correction (for small angles, tan ≈ sin ≈ angle in radians)
167 let angleCorrection = (this.pos.x - branchStart) * branch.angle
168 branchSurfaceY += angleCorrection
169
170 // Check if spider is crossing the branch from above
171 let prevY = this.pos.y - this.vel.y
172
173 if (
174 prevY <= branchSurfaceY && // Was above
175 this.pos.y + this.radius >= branchSurfaceY && // Now at or below
176 this.pos.y < branch.y + branch.thickness
177 ) {
178 // Not too far below
179
180 // Place spider on the branch surface
181 this.pos.y = branchSurfaceY - this.radius
182 this.land()
183 }
184 }
185 }
186
187 // Check obstacle collisions
188 for (let obstacle of obstacles) {
189 if (this.checkObstacleCollision(obstacle)) {
190 this.landOnObstacle(obstacle)
191 }
192 }
193
194 // Check web strand collisions
195 for (let strand of webStrands) {
196 if (strand === currentStrand) continue
197
198 if (this.isAirborne && this.checkStrandCollision(strand)) {
199 this.landOnStrand(strand)
200 }
201 }
202
203 // Check food box collisions
204 for (let i = foodBoxes.length - 1; i >= 0; i--) {
205 let box = foodBoxes[i]
206 if (
207 dist(this.pos.x, this.pos.y, box.pos.x, box.pos.y) <
208 this.radius + box.radius
209 ) {
210 box.collect()
211 foodBoxes.splice(i, 1)
212 }
213 }
214 }
215
216 checkObstacleCollision (obstacle) {
217 let d = dist(this.pos.x, this.pos.y, obstacle.x, obstacle.y)
218 return d < this.radius + obstacle.radius
219 }
220
221 checkStrandCollision (strand) {
222 let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
223 return d < this.radius + 2
224 }
225
226 pointToLineDistance (point, lineStart, lineEnd) {
227 let line = p5.Vector.sub(lineEnd, lineStart)
228 let lineLength = line.mag()
229 line.normalize()
230
231 let pointToStart = p5.Vector.sub(point, lineStart)
232 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
233
234 let closestPoint = p5.Vector.add(
235 lineStart,
236 p5.Vector.mult(line, projLength)
237 )
238 return p5.Vector.dist(point, closestPoint)
239 }
240
241 landOnObstacle (obstacle) {
242 let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
243 this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius)
244 this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius)
245 this.land()
246 }
247
248 landOnStrand (strand) {
249 let line = p5.Vector.sub(strand.end, strand.start)
250 let lineLength = line.mag()
251 line.normalize()
252
253 let pointToStart = p5.Vector.sub(this.pos, strand.start)
254 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
255
256 let closestPoint = p5.Vector.add(
257 strand.start,
258 p5.Vector.mult(line, projLength)
259 )
260 this.pos = closestPoint
261 this.land()
262 }
263
264 land () {
265 this.vel.mult(0)
266 this.isAirborne = false
267 this.canJump = true
268
269 if (currentStrand && isDeployingWeb && spacePressed) {
270 currentStrand.end = this.pos.copy()
271 currentStrand.path.push(this.pos.copy())
272 webNodes.push(new WebNode(this.pos.x, this.pos.y))
273 }
274
275 currentStrand = null
276 isDeployingWeb = false
277 }
278
279 display () {
280 push()
281 translate(this.pos.x, this.pos.y)
282
283 if (isMunching && this.munchCooldown > 15) {
284 push()
285 fill(255, 100, 100, 150)
286 noStroke()
287 let munchSize = 15 + sin(frameCount * 0.5) * 5
288 arc(0, 0, munchSize, munchSize, 0, PI + HALF_PI, PIE)
289 pop()
290 }
291
292 fill(20)
293 stroke(0)
294 strokeWeight(1)
295 ellipse(0, 0, this.radius * 2)
296
297 fill(40)
298 noStroke()
299 ellipse(0, -2, this.radius * 1.2, this.radius * 1.5)
300
301 if (gamePhase === 'NIGHT') {
302 fill(255, 100, 100)
303 } else {
304 fill(255, 0, 0)
305 }
306 ellipse(-3, -3, 3)
307 ellipse(3, -3, 3)
308
309 stroke(0)
310 strokeWeight(1.5)
311 for (let i = 0; i < 4; i++) {
312 let angle = PI / 6 + (i * PI) / 8
313 line(0, 0, cos(angle) * 12, sin(angle) * 8)
314 line(0, 0, -cos(angle) * 12, sin(angle) * 8)
315 }
316
317 if (webSilk < 20) {
318 fill(255, 100, 100, 150 + sin(frameCount * 0.2) * 50)
319 noStroke()
320 ellipse(0, -15, 8)
321 }
322
323 pop()
324 }
325 }
326
327 class Fly {
328 constructor () {
329 if (random() < 0.5) {
330 this.pos = createVector(
331 random() < 0.5 ? -20 : width + 20,
332 random(50, height - 100)
333 )
334 } else {
335 this.pos = createVector(random(width), random() < 0.5 ? -20 : height + 20)
336 }
337
338 this.vel = createVector(random(-2, 2), random(-1, 1))
339 this.acc = createVector(0, 0)
340 this.radius = 4
341 this.caught = false
342 this.stuck = false
343 this.wingPhase = random(TWO_PI)
344 this.wanderAngle = random(TWO_PI)
345 this.glowIntensity = random(150, 255)
346 this.touchedStrands = new Set()
347 this.slowedBy = new Set() // Track which strands are slowing us
348 this.baseSpeed = 3
349 this.currentSpeed = this.baseSpeed
350 }
351
352 update () {
353 if (this.stuck) return
354
355 if (this.caught) {
356 this.vel.mult(0.95)
357 if (this.vel.mag() < 0.1) {
358 this.stuck = true
359 fliesCaught++
360 webSilk = min(webSilk + 5, maxWebSilk)
361 }
362 return
363 }
364
365 this.wanderAngle += random(-0.3, 0.3)
366 let wanderForce = createVector(cos(this.wanderAngle), sin(this.wanderAngle))
367 wanderForce.mult(0.1)
368 this.acc.add(wanderForce)
369
370 // Apply current speed (which may be slowed)
371 this.vel.add(this.acc)
372 this.vel.limit(this.currentSpeed)
373 this.pos.add(this.vel)
374 this.acc.mult(0)
375
376 if (this.pos.x < -30) this.pos.x = width + 30
377 if (this.pos.x > width + 30) this.pos.x = -30
378 if (this.pos.y < -30) this.pos.y = height + 30
379 if (this.pos.y > height + 30) this.pos.y = -30
380
381 // Check web collisions
382 this.checkWebCollisions()
383 }
384
385 checkWebCollisions () {
386 let currentlyTouching = new Set()
387
388 for (let strand of webStrands) {
389 let touching = false
390
391 // Check collision with strand path
392 if (strand.path && strand.path.length > 1) {
393 for (let i = 0; i < strand.path.length - 1; i++) {
394 let p1 = strand.path[i]
395 let p2 = strand.path[i + 1]
396 let d = this.pointToLineDistance(this.pos, p1, p2)
397 if (d < this.radius + 3) {
398 touching = true
399 break
400 }
401 }
402 } else if (strand.start && strand.end) {
403 // Fallback for strands without path
404 let d = this.pointToLineDistance(this.pos, strand.start, strand.end)
405 if (d < this.radius + 3) {
406 touching = true
407 }
408 }
409
410 if (touching) {
411 currentlyTouching.add(strand)
412
413 // If this is a new strand we're touching
414 if (!this.touchedStrands.has(strand)) {
415 this.touchedStrands.add(strand)
416
417 // Vibrate the web when first touching
418 strand.vibrate(3)
419
420 // First strand slows us down
421 if (this.touchedStrands.size === 1) {
422 this.currentSpeed = this.baseSpeed * 0.4 // Slow to 40% speed
423 this.slowedBy.add(strand)
424
425 // Visual feedback - yellow particles for slowing
426 for (let j = 0; j < 3; j++) {
427 let p = new Particle(this.pos.x, this.pos.y)
428 p.color = color(255, 255, 0, 150)
429 p.vel = createVector(random(-1, 1), random(-1, 1))
430 p.size = 3
431 particles.push(p)
432 }
433 }
434 // Second strand catches us
435 else if (this.touchedStrands.size >= 2 && !this.caught) {
436 this.caught = true
437 this.currentSpeed = 0
438
439 // Stronger vibration when caught
440 strand.vibrate(8)
441
442 // Also vibrate nearby strands
443 for (let otherStrand of webStrands) {
444 if (otherStrand !== strand) {
445 for (let touchedStrand of this.touchedStrands) {
446 let d1 = dist(
447 otherStrand.start.x,
448 otherStrand.start.y,
449 touchedStrand.start.x,
450 touchedStrand.start.y
451 )
452 let d2 = dist(
453 otherStrand.start.x,
454 otherStrand.start.y,
455 touchedStrand.end.x,
456 touchedStrand.end.y
457 )
458 let d3 = dist(
459 otherStrand.end.x,
460 otherStrand.end.y,
461 touchedStrand.start.x,
462 touchedStrand.start.y
463 )
464 let d4 = dist(
465 otherStrand.end.x,
466 otherStrand.end.y,
467 touchedStrand.end.x,
468 touchedStrand.end.y
469 )
470 if (min(d1, d2, d3, d4) < 50) {
471 otherStrand.vibrate(2)
472 break
473 }
474 }
475 }
476 }
477
478 // Create caught particles
479 for (let j = 0; j < 6; j++) {
480 let p = new Particle(this.pos.x, this.pos.y)
481 p.color = color(255, 200, 0, 200)
482 p.vel = createVector(random(-2, 2), random(-2, 2))
483 particles.push(p)
484 }
485 }
486 }
487 }
488 }
489
490 // If we're no longer touching strands we were slowed by, speed back up
491 if (this.slowedBy.size > 0 && currentlyTouching.size === 0) {
492 this.currentSpeed = this.baseSpeed
493 this.slowedBy.clear()
494 }
495 }
496
497 pointToLineDistance (point, lineStart, lineEnd) {
498 let line = p5.Vector.sub(lineEnd, lineStart)
499 let lineLength = line.mag()
500 line.normalize()
501
502 let pointToStart = p5.Vector.sub(point, lineStart)
503 let projLength = constrain(pointToStart.dot(line), 0, lineLength)
504
505 let closestPoint = p5.Vector.add(
506 lineStart,
507 p5.Vector.mult(line, projLength)
508 )
509 return p5.Vector.dist(point, closestPoint)
510 }
511
512 display () {
513 push()
514 translate(this.pos.x, this.pos.y)
515
516 // Show slowdown effect
517 if (this.slowedBy.size > 0 && !this.caught) {
518 stroke(255, 255, 0, 100)
519 strokeWeight(1)
520 noFill()
521 ellipse(0, 0, 20)
522 }
523
524 if (gamePhase === 'NIGHT') {
525 noStroke()
526 fill(255, 255, 150, this.glowIntensity * 0.3)
527 ellipse(0, 0, 30)
528 fill(255, 255, 100, this.glowIntensity * 0.5)
529 ellipse(0, 0, 20)
530 }
531
532 fill(30)
533 stroke(0)
534 strokeWeight(0.5)
535 ellipse(0, 0, this.radius * 2)
536
537 if (!this.stuck) {
538 this.wingPhase += 0.5
539 // Wing animation slows down when slowed
540 let wingSpeed = this.slowedBy.size > 0 ? 0.25 : 0.5
541 this.wingPhase += wingSpeed
542 let wingSpread = sin(this.wingPhase) * 5
543
544 fill(255, 255, 255, 150)
545 noStroke()
546 ellipse(-wingSpread, 0, 6, 4)
547 ellipse(wingSpread, 0, 6, 4)
548 }
549
550 if (gamePhase === 'NIGHT') {
551 fill(255, 255, 100, this.glowIntensity)
552 noStroke()
553 ellipse(0, 2, 3)
554 }
555
556 pop()
557 }
558 }
559
560 class Obstacle {
561 constructor (x, y, radius, type) {
562 // Store original position for drift tracking
563 this.originalX = x
564 this.originalY = y
565 this.x = x
566 this.y = y
567 this.radius = radius
568 this.type = type || 'leaf'
569 this.rotation = random(TWO_PI)
570 this.leafPoints = []
571
572 // Movement properties for all types
573 this.bobOffset = random(TWO_PI)
574 this.bobSpeed = random(0.02, 0.04)
575 this.bobAmount = 0
576
577 // Type-specific initialization
578 if (this.type === 'balloon') {
579 this.bobAmount = 8 // Balloons bob more
580 this.balloonColors = [
581 color(255, 100, 100), // Red
582 color(100, 200, 255), // Blue
583 color(255, 200, 100) // Yellow
584 ]
585 this.balloonColor = random(this.balloonColors)
586 this.stringWave = 0
587 this.antLegPhase = random(TWO_PI)
588
589 } else if (this.type === 'beetle') {
590 this.bobAmount = 4
591 this.driftSpeed = random(0.15, 0.35)
592 this.driftAngle = random(TWO_PI)
593 this.driftChangeRate = random(0.005, 0.015)
594 this.wingPhase = random(TWO_PI)
595 this.beetleColor = random() < 0.5 ?
596 color(20, 60, 20) : // Dark green
597 color(40, 20, 60) // Purple
598 this.driftDistance = 0 // Track total drift
599
600 } else if (this.type === 'leaf') {
601 this.bobAmount = 2 // Leaves bob slightly
602 let numPoints = 8
603 for (let i = 0; i < numPoints; i++) {
604 let angle = (TWO_PI / numPoints) * i
605 let r = radius * random(0.7, 1.2)
606 if (i === 0 || i === numPoints / 2) r = radius * 1.3
607 this.leafPoints.push({ angle: angle, radius: r })
608 }
609 } else if (this.type === 'branch') {
610 // Keep for backwards compatibility
611 this.bobAmount = 0
612 }
613 }
614
615 update() {
616 // Bobbing motion for all types
617 let bob = sin(frameCount * this.bobSpeed + this.bobOffset) * this.bobAmount
618 this.y = this.originalY + bob
619
620 // Beetle-specific drift
621 if (this.type === 'beetle') {
622 // Store initial position if not set
623 if (!this.initialX) {
624 this.initialX = this.x
625 this.initialY = this.y
626 }
627
628 // Slowly change drift direction using Perlin noise
629 this.driftAngle += (noise(frameCount * this.driftChangeRate, this.originalX * 0.01) - 0.5) * 0.1
630
631 // Apply drift to original position
632 this.originalX += cos(this.driftAngle) * this.driftSpeed
633 this.originalY += sin(this.driftAngle) * this.driftSpeed * 0.5
634
635 // Calculate total drift distance from initial position
636 this.driftDistance = dist(this.originalX, this.originalY, this.initialX, this.initialY)
637
638 // Keep beetles on screen with soft boundaries
639 if (this.originalX < 80) {
640 this.driftAngle = random(-PI/4, PI/4)
641 this.originalX = 80
642 }
643 if (this.originalX > width - 80) {
644 this.driftAngle = random(3*PI/4, 5*PI/4)
645 this.originalX = width - 80
646 }
647 if (this.originalY < 80) {
648 this.driftAngle = random(-3*PI/4, -PI/4)
649 this.originalY = 80
650 }
651 if (this.originalY > height - 150) {
652 this.driftAngle = random(PI/4, 3*PI/4)
653 this.originalY = height - 150
654 }
655
656 // Update actual position (with bob already applied to y)
657 this.x = this.originalX
658
659 // Check if beetle has drifted too far and break attached strands
660 if (this.driftDistance > 100) {
661 this.breakAttachedStrands()
662 }
663 }
664
665 // Update animation phases
666 if (this.type === 'balloon') {
667 this.stringWave = sin(frameCount * 0.05 + this.bobOffset) * 0.1
668 this.antLegPhase += 0.1
669 } else if (this.type === 'beetle') {
670 this.wingPhase += 0.15
671 }
672
673 // For all moving obstacles, update any attached web strands
674 if (this.bobAmount > 0 || this.type === 'beetle') {
675 this.updateAttachedStrands()
676 }
677 }
678
679 updateAttachedStrands() {
680 // Update web strands that are connected to this obstacle
681 for (let strand of webStrands) {
682 // Check if strand starts at this obstacle
683 if (dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10) {
684 strand.start.x = this.x
685 strand.start.y = this.y
686 if (strand.path && strand.path.length > 0) {
687 strand.path[0].x = this.x
688 strand.path[0].y = this.y
689 }
690 }
691
692 // Check if strand ends at this obstacle
693 if (strand.end && dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10) {
694 strand.end.x = this.x
695 strand.end.y = this.y
696 if (strand.path && strand.path.length > 0) {
697 strand.path[strand.path.length - 1].x = this.x
698 strand.path[strand.path.length - 1].y = this.y
699 }
700 }
701 }
702 }
703
704 breakAttachedStrands() {
705 // Break any strands attached to this beetle that has drifted too far
706 for (let strand of webStrands) {
707 let attachedToStart = dist(strand.start.x, strand.start.y, this.x, this.y) < this.radius + 10
708 let attachedToEnd = strand.end && dist(strand.end.x, strand.end.y, this.x, this.y) < this.radius + 10
709
710 if (attachedToStart || attachedToEnd) {
711 // Mark strand as broken
712 strand.broken = true
713
714 // Create dramatic snap particles
715 let snapX = attachedToStart ? strand.start.x : strand.end.x
716 let snapY = attachedToStart ? strand.start.y : strand.end.y
717
718 // Red/pink particles for the snap
719 for (let i = 0; i < 8; i++) {
720 let p = new Particle(snapX, snapY)
721 p.color = color(255, random(100, 200), random(100, 150))
722 p.vel = createVector(random(-5, 5), random(-5, 2))
723 p.size = random(4, 8)
724 particles.push(p)
725 }
726
727 // White strand particles
728 for (let i = 0; i < 4; i++) {
729 let p = new Particle(snapX, snapY)
730 p.color = color(255, 255, 255)
731 p.vel = createVector(random(-3, 3), random(-3, 0))
732 p.size = 3
733 particles.push(p)
734 }
735
736 // Reset beetle drift after breaking strands
737 this.initialX = this.x
738 this.initialY = this.y
739 this.driftDistance = 0
740 }
741 }
742 }
743
744 display () {
745 push()
746 translate(this.x, this.y)
747
748 if (this.type === 'balloon') {
749 // Balloon with ant in basket!
750 push()
751
752 // String first (behind balloon)
753 stroke(80, 60, 40)
754 strokeWeight(1)
755 noFill()
756 beginShape()
757 for (let i = 0; i <= 10; i++) {
758 let t = i / 10
759 let stringX = sin(t * PI * 2 + this.stringWave) * 3
760 let stringY = t * 40 + this.radius
761 curveVertex(stringX, stringY)
762 }
763 endShape()
764
765 // Balloon shadow
766 noStroke()
767 fill(0, 0, 0, 30)
768 ellipse(5, 5, this.radius * 2.2, this.radius * 2.5)
769
770 // Main balloon
771 noStroke()
772 fill(red(this.balloonColor), green(this.balloonColor), blue(this.balloonColor), 150)
773 ellipse(0, 0, this.radius * 2.2, this.radius * 2.5)
774 fill(red(this.balloonColor) + 30, green(this.balloonColor) + 30, blue(this.balloonColor) + 30, 200)
775 ellipse(-this.radius * 0.3, -this.radius * 0.3, this.radius * 1.2, this.radius * 1.4)
776 // Highlight
777 fill(255, 255, 255, 120)
778 ellipse(-this.radius * 0.4, -this.radius * 0.5, this.radius * 0.5, this.radius * 0.6)
779
780 // Basket
781 translate(0, this.radius + 10)
782 fill(139, 90, 43)
783 stroke(100, 60, 20)
784 strokeWeight(1)
785 // Trapezoid basket
786 beginShape()
787 vertex(-8, 0)
788 vertex(8, 0)
789 vertex(6, 10)
790 vertex(-6, 10)
791 endShape(CLOSE)
792 // Basket weave pattern
793 stroke(100, 60, 20, 100)
794 for (let i = -6; i < 6; i += 3) {
795 line(i, 2, i, 8)
796 }
797 for (let i = 2; i < 8; i += 3) {
798 line(-6, i, 6, i)
799 }
800
801 // Ant in basket
802 translate(0, 5)
803 fill(20)
804 noStroke()
805 // Ant body
806 ellipse(0, 0, 6, 4) // Head
807 ellipse(0, 3, 5, 6) // Thorax
808 ellipse(0, 7, 7, 9) // Abdomen
809 // Ant legs (animated)
810 stroke(20)
811 strokeWeight(0.5)
812 for (let i = 0; i < 3; i++) {
813 let legAngle = this.antLegPhase + i * 0.5
814 let legSpread = 4 + sin(legAngle) * 2
815 line(-2, 3 + i * 2, -legSpread, 3 + i * 2)
816 line(2, 3 + i * 2, legSpread, 3 + i * 2)
817 }
818 // Antennae
819 line(-1, -1, -3, -3)
820 line(1, -1, 3, -3)
821
822 pop()
823
824 } else if (this.type === 'beetle') {
825 // Big beetle!
826 push()
827 rotate(this.rotation)
828
829 // Shadow
830 noStroke()
831 fill(0, 0, 0, 40)
832 ellipse(3, 3, this.radius * 1.8, this.radius * 2.2)
833
834 // Wings (if flying at night)
835 if (gamePhase === 'NIGHT') {
836 push()
837 fill(255, 255, 255, 100 + sin(this.wingPhase) * 50)
838 noStroke()
839 let wingSpread = sin(this.wingPhase) * 15
840 ellipse(-wingSpread, 0, 20, 12)
841 ellipse(wingSpread, 0, 20, 12)
842 pop()
843 }
844
845 // Main beetle body
846 fill(red(this.beetleColor), green(this.beetleColor), blue(this.beetleColor))
847 stroke(0)
848 strokeWeight(2)
849 ellipse(0, 0, this.radius * 1.6, this.radius * 2)
850
851 // Shell split line
852 stroke(0)
853 strokeWeight(1)
854 line(0, -this.radius, 0, this.radius)
855
856 // Head
857 fill(10)
858 ellipse(0, -this.radius * 0.8, this.radius * 0.8, this.radius * 0.6)
859
860 // Spots/pattern
861 noStroke()
862 fill(0, 0, 0, 80)
863 ellipse(-this.radius * 0.3, 0, this.radius * 0.4)
864 ellipse(this.radius * 0.3, -this.radius * 0.2, this.radius * 0.3)
865 ellipse(this.radius * 0.2, this.radius * 0.4, this.radius * 0.35)
866 ellipse(-this.radius * 0.25, this.radius * 0.3, this.radius * 0.25)
867
868 // Legs
869 stroke(0)
870 strokeWeight(2)
871 for (let i = 0; i < 3; i++) {
872 let legY = -this.radius * 0.3 + i * this.radius * 0.3
873 let legMove = sin(this.wingPhase * 2 + i) * 2
874 line(-this.radius * 0.8, legY, -this.radius * 1.2 + legMove, legY + 5)
875 line(this.radius * 0.8, legY, this.radius * 1.2 - legMove, legY + 5)
876 }
877
878 // Antennae
879 strokeWeight(1)
880 line(-3, -this.radius * 1.1, -8, -this.radius * 1.4)
881 line(3, -this.radius * 1.1, 8, -this.radius * 1.4)
882
883 // Eyes
884 fill(255, 0, 0)
885 noStroke()
886 ellipse(-5, -this.radius * 0.7, 4)
887 ellipse(5, -this.radius * 0.7, 4)
888
889 pop()
890
891 } else if (this.type === 'leaf') {
892 // Original leaf code
893 rotate(this.rotation)
894
895 if (gamePhase === 'NIGHT') {
896 fill(20, 40, 20)
897 stroke(10, 20, 10)
898 } else {
899 fill(34, 139, 34)
900 stroke(25, 100, 25)
901 }
902 strokeWeight(2)
903
904 beginShape()
905 for (let point of this.leafPoints) {
906 let x = cos(point.angle) * point.radius
907 let y = sin(point.angle) * point.radius
908 curveVertex(x, y)
909 }
910 let firstPoint = this.leafPoints[0]
911 curveVertex(
912 cos(firstPoint.angle) * firstPoint.radius,
913 sin(firstPoint.angle) * firstPoint.radius
914 )
915 let secondPoint = this.leafPoints[1]
916 curveVertex(
917 cos(secondPoint.angle) * secondPoint.radius,
918 sin(secondPoint.angle) * secondPoint.radius
919 )
920 endShape()
921
922 stroke(25, 100, 25, 100)
923 strokeWeight(1)
924 line(0, -this.radius, 0, this.radius)
925 line(0, 0, -this.radius / 2, -this.radius / 2)
926 line(0, 0, this.radius / 2, -this.radius / 2)
927 line(0, 0, -this.radius / 2, this.radius / 2)
928 line(0, 0, this.radius / 2, this.radius / 2)
929
930 } else if (this.type === 'branch') {
931 // Keep old branch code for backwards compatibility
932 rotate(this.rotation)
933
934 if (gamePhase === 'NIGHT') {
935 stroke(40, 20, 0)
936 fill(50, 25, 5)
937 } else {
938 stroke(101, 67, 33)
939 fill(139, 90, 43)
940 }
941 strokeWeight(3)
942
943 push()
944 strokeWeight(this.radius / 3)
945 line(-this.radius, 0, this.radius, 0)
946
947 strokeWeight(2)
948 line(-this.radius / 2, 0, -this.radius / 2 - 10, -10)
949 line(this.radius / 3, 0, this.radius / 3 + 8, -8)
950 line(0, 0, 5, -15)
951
952 stroke(80, 50, 20, 100)
953 strokeWeight(1)
954 for (let i = -this.radius; i < this.radius; i += 5) {
955 line(i, -2, i + 2, 2)
956 }
957 pop()
958
959 noStroke()
960 fill(255, 255, 255, 30)
961 ellipse(0, 0, this.radius * 2)
962 }
963
964 pop()
965 }
966 }
967
968 class FoodBox {
969 constructor (x, y) {
970 this.pos = createVector(x, y)
971 this.radius = 10
972 this.collected = false
973 this.floatOffset = random(TWO_PI)
974 this.silkValue = random(20, 35)
975 this.glowPhase = random(TWO_PI)
976 }
977
978 collect () {
979 webSilk = min(webSilk + this.silkValue, maxWebSilk)
980
981 for (let i = 0; i < 8; i++) {
982 particles.push(new Particle(this.pos.x, this.pos.y))
983 }
984 }
985
986 display () {
987 push()
988 let floatY = sin(frameCount * 0.05 + this.floatOffset) * 3
989 translate(this.pos.x, this.pos.y + floatY)
990
991 let glowIntensity = 100 + sin(frameCount * 0.1 + this.glowPhase) * 50
992 noStroke()
993 fill(255, 200, 100, glowIntensity * 0.3)
994 ellipse(0, 0, 40)
995 fill(255, 220, 150, glowIntensity * 0.5)
996 ellipse(0, 0, 25)
997
998 rectMode(CENTER)
999
1000 fill(0, 0, 0, 50)
1001 rect(2, 2, this.radius * 2, this.radius * 1.8, 3)
1002
1003 fill(139, 69, 19)
1004 stroke(100, 50, 0)
1005 strokeWeight(1)
1006 rect(0, 0, this.radius * 2, this.radius * 1.8, 3)
1007
1008 stroke(100, 50, 0)
1009 strokeWeight(1)
1010 line(-this.radius, 0, this.radius, 0)
1011 line(0, -this.radius * 0.9, 0, this.radius * 0.9)
1012
1013 noStroke()
1014 fill(255, 200, 100)
1015 ellipse(-5, -4, 4)
1016 ellipse(5, -4, 3)
1017 ellipse(-4, 5, 3)
1018 ellipse(4, 4, 4)
1019
1020 pop()
1021 }
1022 }
1023
1024 class Particle {
1025 constructor (x, y) {
1026 this.pos = createVector(x, y)
1027 this.vel = createVector(random(-3, 3), random(-5, -2))
1028 this.lifetime = 255
1029 this.color = color(255, random(200, 255), random(100, 200))
1030 this.size = 6 // Default size
1031 }
1032
1033 update () {
1034 this.vel.y += 0.2
1035 this.pos.add(this.vel)
1036 this.lifetime -= 8
1037 }
1038
1039 display () {
1040 push()
1041 noStroke()
1042 fill(red(this.color), green(this.color), blue(this.color), this.lifetime)
1043 ellipse(this.pos.x, this.pos.y, this.size)
1044 pop()
1045 }
1046
1047 isDead () {
1048 return this.lifetime <= 0
1049 }
1050 }