@@ -559,14 +559,46 @@ class Fly { |
| 559 | 559 | |
| 560 | 560 | class Obstacle { |
| 561 | 561 | constructor (x, y, radius, type) { |
| 562 | + // Store original position for drift tracking |
| 563 | + this.originalX = x |
| 564 | + this.originalY = y |
| 562 | 565 | this.x = x |
| 563 | 566 | this.y = y |
| 564 | 567 | this.radius = radius |
| 565 | | - this.type = type || (random() < 0.5 ? 'branch' : 'leaf') |
| 568 | + this.type = type || 'leaf' |
| 566 | 569 | this.rotation = random(TWO_PI) |
| 567 | 570 | this.leafPoints = [] |
| 568 | 571 | |
| 569 | | - if (this.type === 'leaf') { |
| 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 |
| 570 | 602 | let numPoints = 8 |
| 571 | 603 | for (let i = 0; i < numPoints; i++) { |
| 572 | 604 | let angle = (TWO_PI / numPoints) * i |
@@ -574,44 +606,292 @@ class Obstacle { |
| 574 | 606 | if (i === 0 || i === numPoints / 2) r = radius * 1.3 |
| 575 | 607 | this.leafPoints.push({ angle: angle, radius: r }) |
| 576 | 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 | + } |
| 577 | 741 | } |
| 578 | 742 | } |
| 579 | 743 | |
| 580 | 744 | display () { |
| 581 | 745 | push() |
| 582 | 746 | translate(this.x, this.y) |
| 583 | | - rotate(this.rotation) |
| 584 | 747 | |
| 585 | | - if (this.type === 'branch') { |
| 586 | | - if (gamePhase === 'NIGHT') { |
| 587 | | - stroke(40, 20, 0) |
| 588 | | - fill(50, 25, 5) |
| 589 | | - } else { |
| 590 | | - stroke(101, 67, 33) |
| 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) |
| 591 | 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) |
| 592 | 817 | } |
| 593 | | - strokeWeight(3) |
| 818 | + // Antennae |
| 819 | + line(-1, -1, -3, -3) |
| 820 | + line(1, -1, 3, -3) |
| 821 | + |
| 822 | + pop() |
| 594 | 823 | |
| 824 | + } else if (this.type === 'beetle') { |
| 825 | + // Big beetle! |
| 595 | 826 | push() |
| 596 | | - strokeWeight(this.radius / 3) |
| 597 | | - line(-this.radius, 0, this.radius, 0) |
| 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 | + } |
| 598 | 844 | |
| 845 | + // Main beetle body |
| 846 | + fill(red(this.beetleColor), green(this.beetleColor), blue(this.beetleColor)) |
| 847 | + stroke(0) |
| 599 | 848 | strokeWeight(2) |
| 600 | | - line(-this.radius / 2, 0, -this.radius / 2 - 10, -10) |
| 601 | | - line(this.radius / 3, 0, this.radius / 3 + 8, -8) |
| 602 | | - line(0, 0, 5, -15) |
| 849 | + ellipse(0, 0, this.radius * 1.6, this.radius * 2) |
| 603 | 850 | |
| 604 | | - stroke(80, 50, 20, 100) |
| 851 | + // Shell split line |
| 852 | + stroke(0) |
| 605 | 853 | strokeWeight(1) |
| 606 | | - for (let i = -this.radius; i < this.radius; i += 5) { |
| 607 | | - line(i, -2, i + 2, 2) |
| 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) |
| 608 | 876 | } |
| 609 | | - pop() |
| 610 | 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) |
| 611 | 885 | noStroke() |
| 612 | | - fill(255, 255, 255, 30) |
| 613 | | - ellipse(0, 0, this.radius * 2) |
| 886 | + ellipse(-5, -this.radius * 0.7, 4) |
| 887 | + ellipse(5, -this.radius * 0.7, 4) |
| 888 | + |
| 889 | + pop() |
| 890 | + |
| 614 | 891 | } else if (this.type === 'leaf') { |
| 892 | + // Original leaf code |
| 893 | + rotate(this.rotation) |
| 894 | + |
| 615 | 895 | if (gamePhase === 'NIGHT') { |
| 616 | 896 | fill(20, 40, 20) |
| 617 | 897 | stroke(10, 20, 10) |
@@ -646,6 +926,39 @@ class Obstacle { |
| 646 | 926 | line(0, 0, this.radius / 2, -this.radius / 2) |
| 647 | 927 | line(0, 0, -this.radius / 2, this.radius / 2) |
| 648 | 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) |
| 649 | 962 | } |
| 650 | 963 | |
| 651 | 964 | pop() |