better mobile support
- SHA
503d6d234479ffb71058a5b51a6adb6875895e3d- Parents
-
60a9830 - Tree
ac7c148
503d6d2
503d6d234479ffb71058a5b51a6adb6875895e3d60a9830
ac7c148| Status | File | + | - |
|---|---|---|---|
| M |
js/entities.js
|
181 | 67 |
| M |
js/game.js
|
84 | 9 |
| M |
js/physics.js
|
26 | 18 |
js/entities.jsmodified@@ -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 | |
js/game.jsmodified@@ -680,11 +680,13 @@ function drawMoon() | ||
| 680 | 680 | function updateResources() { |
| 681 | 681 | webSilk = min(webSilk + silkRechargeRate, maxWebSilk); |
| 682 | 682 | |
| 683 | - if (isDeployingWeb && spider.isAirborne && spacePressed && webSilk > 0) { | |
| 683 | + // Handle silk drain for both keyboard and touch | |
| 684 | + if (isDeployingWeb && spider.isAirborne && (spacePressed || touchHolding) && webSilk > 0) { | |
| 684 | 685 | webSilk = max(0, webSilk - silkDrainRate); |
| 685 | 686 | if (webSilk <= 0) { |
| 686 | 687 | isDeployingWeb = false; |
| 687 | 688 | spacePressed = false; |
| 689 | + touchHolding = false; | |
| 688 | 690 | if (currentStrand) { |
| 689 | 691 | webStrands.pop(); |
| 690 | 692 | currentStrand = null; |
@@ -692,12 +694,13 @@ function updateResources() { | ||
| 692 | 694 | } |
| 693 | 695 | } |
| 694 | 696 | |
| 695 | - if (!spacePressed && isDeployingWeb) { | |
| 697 | + if (!spacePressed && !touchHolding && isDeployingWeb) { | |
| 696 | 698 | isDeployingWeb = false; |
| 697 | 699 | } |
| 698 | 700 | } |
| 699 | 701 | |
| 700 | 702 | function handleWebDeployment() { |
| 703 | + // Handle keyboard-based web deployment | |
| 701 | 704 | if (spacePressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) { |
| 702 | 705 | isDeployingWeb = true; |
| 703 | 706 | currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null); |
@@ -706,15 +709,33 @@ function handleWebDeployment() { | ||
| 706 | 709 | webNodes.push(new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y)); |
| 707 | 710 | } |
| 708 | 711 | |
| 709 | - if (currentStrand && isDeployingWeb && spider.isAirborne) { | |
| 712 | + // Update web for keyboard controls | |
| 713 | + if (currentStrand && isDeployingWeb && spider.isAirborne && spacePressed) { | |
| 710 | 714 | currentStrand.end = spider.pos.copy(); |
| 711 | 715 | if (frameCount % 2 === 0) { |
| 712 | 716 | currentStrand.path.push(spider.pos.copy()); |
| 713 | 717 | } |
| 714 | 718 | } |
| 719 | + | |
| 720 | + // Touch-based web deployment is handled in touchMoved() | |
| 715 | 721 | } |
| 716 | 722 | |
| 717 | 723 | function updateUI() { |
| 724 | + // Update control instructions based on device | |
| 725 | + let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); | |
| 726 | + | |
| 727 | + if (isMobile) { | |
| 728 | + document.getElementById('info').innerHTML = | |
| 729 | + 'Tap to jump • Hold mid-air for web • Double-tap spider to munch!<br>' + | |
| 730 | + 'Web Strands: <span id="strand-count">0</span><br>' + | |
| 731 | + 'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span>'; | |
| 732 | + } else { | |
| 733 | + document.getElementById('info').innerHTML = | |
| 734 | + 'Click to jump • Space to spin web • Shift to munch!<br>' + | |
| 735 | + 'Web Strands: <span id="strand-count">0</span><br>' + | |
| 736 | + 'Flies Caught: <span id="flies-caught">0</span> | Munched: <span id="flies-munched">0</span>'; | |
| 737 | + } | |
| 738 | + | |
| 718 | 739 | document.getElementById('strand-count').textContent = webStrands.length; |
| 719 | 740 | document.getElementById('flies-caught').textContent = fliesCaught; |
| 720 | 741 | document.getElementById('flies-munched').textContent = fliesMunched; |
@@ -743,6 +764,12 @@ function updateUI() { | ||
| 743 | 764 | } |
| 744 | 765 | |
| 745 | 766 | // Input handlers |
| 767 | +let touchStartTime = 0; | |
| 768 | +let lastTapTime = 0; | |
| 769 | +let touchHolding = false; | |
| 770 | +let touchStartX = 0; | |
| 771 | +let touchStartY = 0; | |
| 772 | + | |
| 746 | 773 | function keyPressed() { |
| 747 | 774 | if (key === ' ') { |
| 748 | 775 | spacePressed = true; |
@@ -763,8 +790,11 @@ function keyReleased() { | ||
| 763 | 790 | } |
| 764 | 791 | |
| 765 | 792 | function mousePressed() { |
| 766 | - if (!spider.isAirborne) { | |
| 767 | - spider.jump(mouseX, mouseY); | |
| 793 | + // Only handle mouse on desktop (not touch devices) | |
| 794 | + if (touches.length === 0) { | |
| 795 | + if (!spider.isAirborne) { | |
| 796 | + spider.jump(mouseX, mouseY); | |
| 797 | + } | |
| 768 | 798 | } |
| 769 | 799 | } |
| 770 | 800 | |
@@ -773,14 +803,59 @@ function mouseReleased() { | ||
| 773 | 803 | } |
| 774 | 804 | |
| 775 | 805 | function touchStarted() { |
| 776 | - if (!spider.isAirborne) { | |
| 777 | - spider.jump(touches[0].x, touches[0].y); | |
| 806 | + if (touches.length > 0) { | |
| 807 | + touchStartTime = millis(); | |
| 808 | + touchStartX = touches[0].x; | |
| 809 | + touchStartY = touches[0].y; | |
| 810 | + | |
| 811 | + // Check for double tap on spider to munch | |
| 812 | + let touchOnSpider = dist(touches[0].x, touches[0].y, spider.pos.x, spider.pos.y) < 30; | |
| 813 | + | |
| 814 | + if (touchOnSpider && millis() - lastTapTime < 300) { | |
| 815 | + // Double tap detected on spider - MUNCH! | |
| 816 | + spider.munch(); | |
| 817 | + lastTapTime = 0; // Reset to prevent triple tap | |
| 818 | + } else if (!spider.isAirborne) { | |
| 819 | + // Single tap while on ground - jump | |
| 820 | + spider.jump(touches[0].x, touches[0].y); | |
| 821 | + lastTapTime = millis(); | |
| 822 | + } else if (spider.isAirborne && webSilk > 10 && !isDeployingWeb) { | |
| 823 | + // Start web deployment if airborne (only if not already deploying) | |
| 824 | + touchHolding = true; | |
| 825 | + isDeployingWeb = true; | |
| 826 | + currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null); | |
| 827 | + currentStrand.path = [spider.lastAnchorPoint.copy()]; | |
| 828 | + webStrands.push(currentStrand); | |
| 829 | + webNodes.push(new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y)); | |
| 830 | + } else if (spider.isAirborne && isDeployingWeb) { | |
| 831 | + // If already deploying and user taps again, just continue (don't create new strand) | |
| 832 | + touchHolding = true; | |
| 833 | + } | |
| 778 | 834 | } |
| 779 | - return false; | |
| 835 | + return false; // Prevent default | |
| 836 | +} | |
| 837 | + | |
| 838 | +function touchMoved() { | |
| 839 | + // Update web deployment target while holding | |
| 840 | + if (touchHolding && spider.isAirborne && isDeployingWeb && currentStrand && webSilk > 0) { | |
| 841 | + // Web follows spider while deploying (not finger position) | |
| 842 | + currentStrand.end = spider.pos.copy(); | |
| 843 | + if (frameCount % 2 === 0) { | |
| 844 | + currentStrand.path.push(spider.pos.copy()); | |
| 845 | + } | |
| 846 | + } | |
| 847 | + return false; // Prevent default | |
| 780 | 848 | } |
| 781 | 849 | |
| 782 | 850 | function touchEnded() { |
| 783 | - return false; | |
| 851 | + touchHolding = false; | |
| 852 | + | |
| 853 | + // Stop web deployment when releasing touch | |
| 854 | + if (isDeployingWeb && spider.isAirborne) { | |
| 855 | + isDeployingWeb = false; | |
| 856 | + } | |
| 857 | + | |
| 858 | + return false; // Prevent default | |
| 784 | 859 | } |
| 785 | 860 | |
| 786 | 861 | function windowResized() { |
js/physics.jsmodified@@ -97,8 +97,9 @@ class WebStrand { | ||
| 97 | 97 | } |
| 98 | 98 | |
| 99 | 99 | for (let node of webNodes) { |
| 100 | - if (dist(node.x, node.y, this.start.x, this.start.y) < 5 || | |
| 101 | - dist(node.x, node.y, this.end.x, this.end.y) < 5) { | |
| 100 | + const nearStart = dist(node.x, node.y, this.start.x, this.start.y) < 5; | |
| 101 | + const nearEnd = this.end ? (dist(node.x, node.y, this.end.x, this.end.y) < 5) : false; | |
| 102 | + if (nearStart || nearEnd) { | |
| 102 | 103 | node.applyForce(0, 0.1); |
| 103 | 104 | } |
| 104 | 105 | } |
@@ -141,9 +142,16 @@ class WebStrand { | ||
| 141 | 142 | |
| 142 | 143 | display() { |
| 143 | 144 | if (this.broken) return; // Don't display broken strands |
| 144 | - | |
| 145 | + | |
| 145 | 146 | push(); |
| 146 | - | |
| 147 | + | |
| 148 | + // If the strand's end hasn't been established yet (e.g., just started deploying on touch), | |
| 149 | + // skip physics rendering here. The in-progress strand is drawn from game.js. | |
| 150 | + if (!this.end) { | |
| 151 | + pop(); | |
| 152 | + return; | |
| 153 | + } | |
| 154 | + | |
| 147 | 155 | // Change color based on tension |
| 148 | 156 | if (this.tension > 0.8) { |
| 149 | 157 | stroke(255, 200, 200, 200); // Reddish when strained |
@@ -152,24 +160,24 @@ class WebStrand { | ||
| 152 | 160 | } else { |
| 153 | 161 | stroke(255, 255, 255, 200); |
| 154 | 162 | } |
| 155 | - | |
| 163 | + | |
| 156 | 164 | strokeWeight(gamePhase === 'NIGHT' ? 2 : 1.5); |
| 157 | 165 | noFill(); |
| 158 | - | |
| 166 | + | |
| 159 | 167 | if (this.path && this.path.length > 2) { |
| 160 | 168 | beginShape(); |
| 161 | 169 | curveVertex(this.path[0].x, this.path[0].y + this.vibration * sin(frameCount * 0.3)); |
| 162 | - | |
| 170 | + | |
| 163 | 171 | for (let i = 0; i < this.path.length; i++) { |
| 164 | 172 | let point = this.path[i]; |
| 165 | 173 | let vibOffset = this.vibration * sin(frameCount * 0.3 + i * 0.1) * (i / this.path.length); |
| 166 | 174 | curveVertex(point.x, point.y + vibOffset); |
| 167 | 175 | } |
| 168 | - | |
| 176 | + | |
| 169 | 177 | let lastPoint = this.path[this.path.length - 1]; |
| 170 | 178 | curveVertex(lastPoint.x, lastPoint.y + this.vibration * sin(frameCount * 0.3)); |
| 171 | 179 | endShape(); |
| 172 | - | |
| 180 | + | |
| 173 | 181 | stroke(255, 255, 255, 50); |
| 174 | 182 | strokeWeight(4); |
| 175 | 183 | beginShape(); |
@@ -182,15 +190,15 @@ class WebStrand { | ||
| 182 | 190 | } else { |
| 183 | 191 | let midX = (this.start.x + this.end.x) / 2; |
| 184 | 192 | let midY = (this.start.y + this.end.y) / 2 + this.vibration * sin(frameCount * 0.3); |
| 185 | - | |
| 193 | + | |
| 186 | 194 | // Add sag based on horizontal distance |
| 187 | 195 | let horizontalDist = abs(this.end.x - this.start.x); |
| 188 | 196 | let sag = horizontalDist * 0.12; |
| 189 | 197 | midY += sag * (1 - cos(PI * 0.5)); |
| 190 | - | |
| 198 | + | |
| 191 | 199 | // Apply recoil deformation to the web (very subtle) |
| 192 | 200 | midY += this.recoil * 2; // Further reduced from 3 |
| 193 | - | |
| 201 | + | |
| 194 | 202 | beginShape(); |
| 195 | 203 | curveVertex(this.start.x, this.start.y); |
| 196 | 204 | curveVertex(this.start.x, this.start.y); |
@@ -198,7 +206,7 @@ class WebStrand { | ||
| 198 | 206 | curveVertex(this.end.x, this.end.y); |
| 199 | 207 | curveVertex(this.end.x, this.end.y); |
| 200 | 208 | endShape(); |
| 201 | - | |
| 209 | + | |
| 202 | 210 | stroke(255, 255, 255, 50); |
| 203 | 211 | strokeWeight(4); |
| 204 | 212 | beginShape(); |
@@ -209,7 +217,7 @@ class WebStrand { | ||
| 209 | 217 | curveVertex(this.end.x, this.end.y); |
| 210 | 218 | endShape(); |
| 211 | 219 | } |
| 212 | - | |
| 220 | + | |
| 213 | 221 | pop(); |
| 214 | 222 | } |
| 215 | 223 | |
@@ -227,11 +235,11 @@ class WebStrand { | ||
| 227 | 235 | |
| 228 | 236 | // Add some energy dissipation through the web network (more subtle) |
| 229 | 237 | for (let node of webNodes) { |
| 230 | - let d1 = dist(node.x, node.y, this.start.x, this.start.y); | |
| 231 | - let d2 = dist(node.x, node.y, this.end.x, this.end.y); | |
| 232 | - let minDist = min(d1, d2); | |
| 238 | + const d1 = dist(node.x, node.y, this.start.x, this.start.y); | |
| 239 | + const d2 = this.end ? dist(node.x, node.y, this.end.x, this.end.y) : Infinity; | |
| 240 | + const minDist = Math.min(d1, d2); | |
| 233 | 241 | if (minDist < 100) { |
| 234 | - let forceFalloff = map(minDist, 0, 100, 0.3, 0); | |
| 242 | + const forceFalloff = map(minDist, 0, 100, 0.3, 0); | |
| 235 | 243 | node.applyForce(0, force * forceFalloff * 0.15); |
| 236 | 244 | } |
| 237 | 245 | } |