@@ -18,7 +18,7 @@ class Spider { |
| 18 | 18 | } |
| 19 | 19 | |
| 20 | 20 | jump(targetX, targetY) { |
| 21 | | - if (!this.canJump) return; |
| 21 | + if (!this.canJump || this.isAirborne) return; // Don't jump if already airborne |
| 22 | 22 | |
| 23 | 23 | let direction = createVector(targetX - this.pos.x, targetY - this.pos.y); |
| 24 | 24 | let clickDistance = direction.mag(); |
@@ -33,6 +33,10 @@ class Spider { |
| 33 | 33 | this.isAirborne = true; |
| 34 | 34 | this.canJump = false; |
| 35 | 35 | this.lastAnchorPoint = this.pos.copy(); |
| 36 | + // Record jump time for touch debounce |
| 37 | + if (typeof window !== 'undefined') { |
| 38 | + window.lastJumpTime = millis(); |
| 39 | + } |
| 36 | 40 | |
| 37 | 41 | // Check if we're jumping off a web strand |
| 38 | 42 | for (let strand of webStrands) { |
@@ -232,26 +236,33 @@ class Spider { |
| 232 | 236 | } |
| 233 | 237 | |
| 234 | 238 | checkStrandCollision (strand) { |
| 235 | | - let d = this.pointToLineDistance(this.pos, strand.start, strand.end) |
| 236 | | - return d < this.radius + 2 |
| 239 | + if (!strand || !strand.start || !strand.end) return false; |
| 240 | + let d = this.pointToLineDistance(this.pos, strand.start, strand.end); |
| 241 | + return d < this.radius + 2; |
| 237 | 242 | } |
| 238 | 243 | |
| 239 | 244 | pointToLineDistance (point, lineStart, lineEnd) { |
| 240 | | - let line = p5.Vector.sub(lineEnd, lineStart) |
| 241 | | - let lineLength = line.mag() |
| 242 | | - line.normalize() |
| 243 | | - |
| 244 | | - let pointToStart = p5.Vector.sub(point, lineStart) |
| 245 | | - let projLength = constrain(pointToStart.dot(line), 0, lineLength) |
| 246 | | - |
| 247 | | - let closestPoint = p5.Vector.add( |
| 248 | | - lineStart, |
| 249 | | - p5.Vector.mult(line, projLength) |
| 250 | | - ) |
| 251 | | - return p5.Vector.dist(point, closestPoint) |
| 245 | + // Guard nulls |
| 246 | + if (!lineStart || !lineEnd) { |
| 247 | + return Infinity; |
| 248 | + } |
| 249 | + let line = p5.Vector.sub(lineEnd, lineStart); |
| 250 | + let lineLength = line.mag(); |
| 251 | + // If start and end coincide, distance is to the single point |
| 252 | + if (lineLength === 0) { |
| 253 | + return p5.Vector.dist(point, lineStart); |
| 254 | + } |
| 255 | + line.normalize(); |
| 256 | + let pointToStart = p5.Vector.sub(point, lineStart); |
| 257 | + let projLength = constrain(pointToStart.dot(line), 0, lineLength); |
| 258 | + let closestPoint = p5.Vector.add(lineStart, p5.Vector.mult(line, projLength)); |
| 259 | + return p5.Vector.dist(point, closestPoint); |
| 252 | 260 | } |
| 253 | 261 | |
| 254 | 262 | landOnObstacle (obstacle) { |
| 263 | + // Only land if we're actually airborne |
| 264 | + if (!this.isAirborne) return; |
| 265 | + |
| 255 | 266 | let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x) |
| 256 | 267 | this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius) |
| 257 | 268 | this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius) |
@@ -260,20 +271,23 @@ class Spider { |
| 260 | 271 | } |
| 261 | 272 | |
| 262 | 273 | landOnStrand (strand) { |
| 263 | | - let line = p5.Vector.sub(strand.end, strand.start) |
| 264 | | - let lineLength = line.mag() |
| 265 | | - line.normalize() |
| 266 | | - |
| 267 | | - let pointToStart = p5.Vector.sub(this.pos, strand.start) |
| 268 | | - let projLength = constrain(pointToStart.dot(line), 0, lineLength) |
| 269 | | - |
| 270 | | - let closestPoint = p5.Vector.add( |
| 271 | | - strand.start, |
| 272 | | - p5.Vector.mult(line, projLength) |
| 273 | | - ) |
| 274 | | - this.pos = closestPoint |
| 275 | | - this.attachedObstacle = null // Not on an obstacle |
| 276 | | - this.land() |
| 274 | + // Only land if we're actually airborne |
| 275 | + if (!this.isAirborne) return; |
| 276 | + if (!strand || !strand.start || !strand.end) return; |
| 277 | + let line = p5.Vector.sub(strand.end, strand.start); |
| 278 | + let lineLength = line.mag(); |
| 279 | + if (lineLength === 0) { |
| 280 | + // Degenerate strand; snap to start |
| 281 | + this.pos = strand.start.copy ? strand.start.copy() : createVector(strand.start.x, strand.start.y); |
| 282 | + } else { |
| 283 | + line.normalize(); |
| 284 | + let pointToStart = p5.Vector.sub(this.pos, strand.start); |
| 285 | + let projLength = constrain(pointToStart.dot(line), 0, lineLength); |
| 286 | + let closestPoint = p5.Vector.add(strand.start, p5.Vector.mult(line, projLength)); |
| 287 | + this.pos = closestPoint; |
| 288 | + } |
| 289 | + this.attachedObstacle = null; // Not on an obstacle |
| 290 | + this.land(); |
| 277 | 291 | } |
| 278 | 292 | |
| 279 | 293 | land () { |
@@ -281,10 +295,15 @@ class Spider { |
| 281 | 295 | this.isAirborne = false |
| 282 | 296 | this.canJump = true |
| 283 | 297 | |
| 284 | | - if (currentStrand && isDeployingWeb && spacePressed) { |
| 285 | | - currentStrand.end = this.pos.copy() |
| 286 | | - currentStrand.path.push(this.pos.copy()) |
| 287 | | - webNodes.push(new WebNode(this.pos.x, this.pos.y)) |
| 298 | + if (currentStrand && isDeployingWeb && (spacePressed || touchHolding)) { |
| 299 | + // Ensure the strand has a valid end and a final node on landing |
| 300 | + currentStrand.end = this.pos.copy(); |
| 301 | + if (!currentStrand.path || currentStrand.path.length === 0) { |
| 302 | + currentStrand.path = [this.pos.copy()]; |
| 303 | + } else { |
| 304 | + currentStrand.path.push(this.pos.copy()); |
| 305 | + } |
| 306 | + webNodes.push(new WebNode(this.pos.x, this.pos.y)); |
| 288 | 307 | } |
| 289 | 308 | |
| 290 | 309 | currentStrand = null |
@@ -854,12 +873,12 @@ class Obstacle { |
| 854 | 873 | translate(this.x, this.y) |
| 855 | 874 | |
| 856 | 875 | if (this.type === 'balloon') { |
| 857 | | - // Balloon with ant in basket! |
| 876 | + // Hot air balloon with canvas texture! |
| 858 | 877 | push() |
| 859 | 878 | |
| 860 | | - // String first (behind balloon) |
| 879 | + // String/rope first (behind balloon) |
| 861 | 880 | stroke(80, 60, 40) |
| 862 | | - strokeWeight(1) |
| 881 | + strokeWeight(1.5) |
| 863 | 882 | noFill() |
| 864 | 883 | beginShape() |
| 865 | 884 | for (let i = 0; i <= 10; i++) { |
@@ -875,22 +894,115 @@ class Obstacle { |
| 875 | 894 | fill(0, 0, 0, 30) |
| 876 | 895 | ellipse(5, 5, this.radius * 2.2, this.radius * 2.5) |
| 877 | 896 | |
| 878 | | - // Main balloon |
| 897 | + // Main balloon with canvas panels |
| 898 | + push() |
| 899 | + // Draw vertical panels for that classic hot air balloon look |
| 900 | + let numPanels = 8 |
| 901 | + for (let i = 0; i < numPanels; i++) { |
| 902 | + let angle1 = (TWO_PI / numPanels) * i |
| 903 | + let angle2 = (TWO_PI / numPanels) * (i + 1) |
| 904 | + |
| 905 | + // Alternate panel colors for striped effect |
| 906 | + if (i % 2 === 0) { |
| 907 | + fill(red(this.balloonColor), green(this.balloonColor), blue(this.balloonColor), 200) |
| 908 | + } else { |
| 909 | + fill( |
| 910 | + red(this.balloonColor) - 30, |
| 911 | + green(this.balloonColor) - 30, |
| 912 | + blue(this.balloonColor) - 30, |
| 913 | + 200 |
| 914 | + ) |
| 915 | + } |
| 916 | + |
| 917 | + // Draw tapered panel (wider at middle, narrow at top/bottom) |
| 918 | + beginShape() |
| 919 | + // Top point |
| 920 | + vertex(0, -this.radius * 1.2) |
| 921 | + // Upper curve |
| 922 | + bezierVertex( |
| 923 | + cos(angle1) * this.radius * 0.3, -this.radius * 0.9, |
| 924 | + cos(angle1) * this.radius * 0.8, -this.radius * 0.3, |
| 925 | + cos(angle1) * this.radius * 1.1, 0 |
| 926 | + ) |
| 927 | + // Lower curve to bottom |
| 928 | + bezierVertex( |
| 929 | + cos(angle1) * this.radius * 0.9, this.radius * 0.5, |
| 930 | + cos(angle1) * this.radius * 0.4, this.radius * 0.9, |
| 931 | + 0, this.radius * 1.1 |
| 932 | + ) |
| 933 | + // Back up the other side |
| 934 | + bezierVertex( |
| 935 | + cos(angle2) * this.radius * 0.4, this.radius * 0.9, |
| 936 | + cos(angle2) * this.radius * 0.9, this.radius * 0.5, |
| 937 | + cos(angle2) * this.radius * 1.1, 0 |
| 938 | + ) |
| 939 | + bezierVertex( |
| 940 | + cos(angle2) * this.radius * 0.8, -this.radius * 0.3, |
| 941 | + cos(angle2) * this.radius * 0.3, -this.radius * 0.9, |
| 942 | + 0, -this.radius * 1.2 |
| 943 | + ) |
| 944 | + endShape(CLOSE) |
| 945 | + } |
| 946 | + |
| 947 | + // Panel seams/ropes |
| 948 | + stroke(60, 40, 20, 100) |
| 949 | + strokeWeight(0.5) |
| 950 | + for (let i = 0; i < numPanels; i++) { |
| 951 | + let angle = (TWO_PI / numPanels) * i |
| 952 | + // Vertical seam lines |
| 953 | + beginShape() |
| 954 | + noFill() |
| 955 | + vertex(0, -this.radius * 1.2) |
| 956 | + bezierVertex( |
| 957 | + cos(angle) * this.radius * 0.3, -this.radius * 0.9, |
| 958 | + cos(angle) * this.radius * 0.8, -this.radius * 0.3, |
| 959 | + cos(angle) * this.radius * 1.1, 0 |
| 960 | + ) |
| 961 | + bezierVertex( |
| 962 | + cos(angle) * this.radius * 0.9, this.radius * 0.5, |
| 963 | + cos(angle) * this.radius * 0.4, this.radius * 0.9, |
| 964 | + 0, this.radius * 1.1 |
| 965 | + ) |
| 966 | + endShape() |
| 967 | + } |
| 968 | + |
| 969 | + // Highlight on balloon |
| 879 | 970 | noStroke() |
| 880 | | - fill(red(this.balloonColor), green(this.balloonColor), blue(this.balloonColor), 150) |
| 881 | | - ellipse(0, 0, this.radius * 2.2, this.radius * 2.5) |
| 882 | | - fill(red(this.balloonColor) + 30, green(this.balloonColor) + 30, blue(this.balloonColor) + 30, 200) |
| 883 | | - ellipse(-this.radius * 0.3, -this.radius * 0.3, this.radius * 1.2, this.radius * 1.4) |
| 884 | | - // Highlight |
| 885 | | - fill(255, 255, 255, 120) |
| 886 | | - ellipse(-this.radius * 0.4, -this.radius * 0.5, this.radius * 0.5, this.radius * 0.6) |
| 971 | + fill(255, 255, 255, 80) |
| 972 | + ellipse(-this.radius * 0.3, -this.radius * 0.5, this.radius * 0.6, this.radius * 0.7) |
| 973 | + pop() |
| 974 | + |
| 975 | + // FLAME EFFECT! |
| 976 | + push() |
| 977 | + translate(0, this.radius - 5) |
| 978 | + // Flame glow |
| 979 | + noStroke() |
| 980 | + fill(255, 200, 0, 40 + sin(frameCount * 0.3) * 20) |
| 981 | + ellipse(0, 0, 25, 25) |
| 982 | + fill(255, 150, 0, 60 + sin(frameCount * 0.4) * 30) |
| 983 | + ellipse(0, 0, 15, 18) |
| 984 | + // Flame itself |
| 985 | + fill(255, 200, 0) |
| 986 | + push() |
| 987 | + let flameHeight = 8 + sin(frameCount * 0.5) * 3 |
| 988 | + translate(0, -2) |
| 989 | + beginShape() |
| 990 | + vertex(-3, 0) |
| 991 | + bezierVertex(-3, -flameHeight * 0.7, -1, -flameHeight, 0, -flameHeight * 1.2) |
| 992 | + bezierVertex(1, -flameHeight, 3, -flameHeight * 0.7, 3, 0) |
| 993 | + endShape(CLOSE) |
| 994 | + fill(255, 255, 200) |
| 995 | + ellipse(0, -flameHeight * 0.5, 3, 4) |
| 996 | + pop() |
| 997 | + pop() |
| 887 | 998 | |
| 888 | 999 | // Basket |
| 1000 | + push() |
| 889 | 1001 | translate(0, this.radius + 10) |
| 890 | | - fill(139, 90, 43) |
| 891 | | - stroke(100, 60, 20) |
| 1002 | + fill(101, 67, 33) |
| 1003 | + stroke(80, 50, 20) |
| 892 | 1004 | strokeWeight(1) |
| 893 | | - // Trapezoid basket |
| 1005 | + // Woven basket shape |
| 894 | 1006 | beginShape() |
| 895 | 1007 | vertex(-8, 0) |
| 896 | 1008 | vertex(8, 0) |
@@ -898,34 +1010,36 @@ class Obstacle { |
| 898 | 1010 | vertex(-6, 10) |
| 899 | 1011 | endShape(CLOSE) |
| 900 | 1012 | // Basket weave pattern |
| 901 | | - stroke(100, 60, 20, 100) |
| 902 | | - for (let i = -6; i < 6; i += 3) { |
| 903 | | - line(i, 2, i, 8) |
| 1013 | + stroke(80, 50, 20, 150) |
| 1014 | + for (let i = -6; i < 6; i += 2) { |
| 1015 | + line(i, 1, i, 9) |
| 904 | 1016 | } |
| 905 | | - for (let i = 2; i < 8; i += 3) { |
| 1017 | + for (let i = 2; i < 9; i += 2) { |
| 906 | 1018 | line(-6, i, 6, i) |
| 907 | 1019 | } |
| 1020 | + // Basket rim |
| 1021 | + stroke(60, 40, 20) |
| 1022 | + strokeWeight(1.5) |
| 1023 | + line(-8, 0, 8, 0) |
| 1024 | + pop() |
| 908 | 1025 | |
| 909 | | - // Ant in basket |
| 910 | | - translate(0, 5) |
| 1026 | + // Ant in basket (peeking over edge) |
| 1027 | + push() |
| 1028 | + translate(0, this.radius + 12) |
| 911 | 1029 | fill(20) |
| 912 | 1030 | noStroke() |
| 913 | | - // Ant body |
| 914 | | - ellipse(0, 0, 6, 4) // Head |
| 915 | | - ellipse(0, 3, 5, 6) // Thorax |
| 916 | | - ellipse(0, 7, 7, 9) // Abdomen |
| 917 | | - // Ant legs (animated) |
| 1031 | + // Just ant head and antennae visible |
| 1032 | + ellipse(0, -2, 6, 4) // Head peeking up |
| 1033 | + // Antennae |
| 918 | 1034 | stroke(20) |
| 919 | 1035 | strokeWeight(0.5) |
| 920 | | - for (let i = 0; i < 3; i++) { |
| 921 | | - let legAngle = this.antLegPhase + i * 0.5 |
| 922 | | - let legSpread = 4 + sin(legAngle) * 2 |
| 923 | | - line(-2, 3 + i * 2, -legSpread, 3 + i * 2) |
| 924 | | - line(2, 3 + i * 2, legSpread, 3 + i * 2) |
| 925 | | - } |
| 926 | | - // Antennae |
| 927 | | - line(-1, -1, -3, -3) |
| 928 | | - line(1, -1, 3, -3) |
| 1036 | + line(-1, -3, -3, -6) |
| 1037 | + line(1, -3, 3, -6) |
| 1038 | + // Tiny ant arms gripping basket edge |
| 1039 | + strokeWeight(1) |
| 1040 | + line(-3, 0, -4, 2) |
| 1041 | + line(3, 0, 4, 2) |
| 1042 | + pop() |
| 929 | 1043 | |
| 930 | 1044 | pop() |
| 931 | 1045 | |