zeroed-some/cob / 503d6d2

Browse files

better mobile support

Authored by espadonne
SHA
503d6d234479ffb71058a5b51a6adb6875895e3d
Parents
60a9830
Tree
ac7c148

3 changed files

StatusFile+-
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 {
1818
   }
1919
 
2020
   jump(targetX, targetY) {
21
-    if (!this.canJump) return;
21
+    if (!this.canJump || this.isAirborne) return; // Don't jump if already airborne
2222
     
2323
     let direction = createVector(targetX - this.pos.x, targetY - this.pos.y);
2424
     let clickDistance = direction.mag();
@@ -33,6 +33,10 @@ class Spider {
3333
     this.isAirborne = true;
3434
     this.canJump = false;
3535
     this.lastAnchorPoint = this.pos.copy();
36
+    // Record jump time for touch debounce
37
+    if (typeof window !== 'undefined') {
38
+      window.lastJumpTime = millis();
39
+    }
3640
     
3741
     // Check if we're jumping off a web strand
3842
     for (let strand of webStrands) {
@@ -232,26 +236,33 @@ class Spider {
232236
   }
233237
 
234238
   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;
237242
   }
238243
 
239244
   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);
252260
   }
253261
 
254262
   landOnObstacle (obstacle) {
263
+    // Only land if we're actually airborne
264
+    if (!this.isAirborne) return;
265
+    
255266
     let angle = atan2(this.pos.y - obstacle.y, this.pos.x - obstacle.x)
256267
     this.pos.x = obstacle.x + cos(angle) * (obstacle.radius + this.radius)
257268
     this.pos.y = obstacle.y + sin(angle) * (obstacle.radius + this.radius)
@@ -260,20 +271,23 @@ class Spider {
260271
   }
261272
 
262273
   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();
277291
   }
278292
 
279293
   land () {
@@ -281,10 +295,15 @@ class Spider {
281295
     this.isAirborne = false
282296
     this.canJump = true
283297
 
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));
288307
     }
289308
 
290309
     currentStrand = null
@@ -854,12 +873,12 @@ class Obstacle {
854873
     translate(this.x, this.y)
855874
     
856875
     if (this.type === 'balloon') {
857
-      // Balloon with ant in basket!
876
+      // Hot air balloon with canvas texture!
858877
       push()
859878
       
860
-      // String first (behind balloon)
879
+      // String/rope first (behind balloon)
861880
       stroke(80, 60, 40)
862
-      strokeWeight(1)
881
+      strokeWeight(1.5)
863882
       noFill()
864883
       beginShape()
865884
       for (let i = 0; i <= 10; i++) {
@@ -875,22 +894,115 @@ class Obstacle {
875894
       fill(0, 0, 0, 30)
876895
       ellipse(5, 5, this.radius * 2.2, this.radius * 2.5)
877896
       
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
879970
       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()
887998
       
888999
       // Basket
1000
+      push()
8891001
       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)
8921004
       strokeWeight(1)
893
-      // Trapezoid basket
1005
+      // Woven basket shape
8941006
       beginShape()
8951007
       vertex(-8, 0)
8961008
       vertex(8, 0)
@@ -898,34 +1010,36 @@ class Obstacle {
8981010
       vertex(-6, 10)
8991011
       endShape(CLOSE)
9001012
       // 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)
9041016
       }
905
-      for (let i = 2; i < 8; i += 3) {
1017
+      for (let i = 2; i < 9; i += 2) {
9061018
         line(-6, i, 6, i)
9071019
       }
1020
+      // Basket rim
1021
+      stroke(60, 40, 20)
1022
+      strokeWeight(1.5)
1023
+      line(-8, 0, 8, 0)
1024
+      pop()
9081025
       
909
-      // Ant in basket
910
-      translate(0, 5)
1026
+      // Ant in basket (peeking over edge)
1027
+      push()
1028
+      translate(0, this.radius + 12)
9111029
       fill(20)
9121030
       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
9181034
       stroke(20)
9191035
       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()
9291043
       
9301044
       pop()
9311045
       
js/game.jsmodified
@@ -680,11 +680,13 @@ function drawMoon()
680680
 function updateResources() {
681681
     webSilk = min(webSilk + silkRechargeRate, maxWebSilk);
682682
     
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) {
684685
         webSilk = max(0, webSilk - silkDrainRate);
685686
         if (webSilk <= 0) {
686687
             isDeployingWeb = false;
687688
             spacePressed = false;
689
+            touchHolding = false;
688690
             if (currentStrand) {
689691
                 webStrands.pop();
690692
                 currentStrand = null;
@@ -692,12 +694,13 @@ function updateResources() {
692694
         }
693695
     }
694696
     
695
-    if (!spacePressed && isDeployingWeb) {
697
+    if (!spacePressed && !touchHolding && isDeployingWeb) {
696698
         isDeployingWeb = false;
697699
     }
698700
 }
699701
 
700702
 function handleWebDeployment() {
703
+    // Handle keyboard-based web deployment
701704
     if (spacePressed && spider.isAirborne && !isDeployingWeb && webSilk > 10) {
702705
         isDeployingWeb = true;
703706
         currentStrand = new WebStrand(spider.lastAnchorPoint.copy(), null);
@@ -706,15 +709,33 @@ function handleWebDeployment() {
706709
         webNodes.push(new WebNode(spider.lastAnchorPoint.x, spider.lastAnchorPoint.y));
707710
     }
708711
     
709
-    if (currentStrand && isDeployingWeb && spider.isAirborne) {
712
+    // Update web for keyboard controls
713
+    if (currentStrand && isDeployingWeb && spider.isAirborne && spacePressed) {
710714
         currentStrand.end = spider.pos.copy();
711715
         if (frameCount % 2 === 0) {
712716
             currentStrand.path.push(spider.pos.copy());
713717
         }
714718
     }
719
+    
720
+    // Touch-based web deployment is handled in touchMoved()
715721
 }
716722
 
717723
 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
+    
718739
     document.getElementById('strand-count').textContent = webStrands.length;
719740
     document.getElementById('flies-caught').textContent = fliesCaught;
720741
     document.getElementById('flies-munched').textContent = fliesMunched;
@@ -743,6 +764,12 @@ function updateUI() {
743764
 }
744765
 
745766
 // Input handlers
767
+let touchStartTime = 0;
768
+let lastTapTime = 0;
769
+let touchHolding = false;
770
+let touchStartX = 0;
771
+let touchStartY = 0;
772
+
746773
 function keyPressed() {
747774
     if (key === ' ') {
748775
         spacePressed = true;
@@ -763,24 +790,72 @@ function keyReleased() {
763790
 }
764791
 
765792
 function mousePressed() {
793
+    // Only handle mouse on desktop (not touch devices)
794
+    if (touches.length === 0) {
766795
         if (!spider.isAirborne) {
767796
             spider.jump(mouseX, mouseY);
768797
         }
769798
     }
799
+}
770800
 
771801
 function mouseReleased() {
772802
     // No longer needed for web deployment
773803
 }
774804
 
775805
 function touchStarted() {
776
-    if (!spider.isAirborne) {
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
777820
             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;
778833
         }
779
-    return false;
834
+    }
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
780848
 }
781849
 
782850
 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
784859
 }
785860
 
786861
 function windowResized() {
js/physics.jsmodified
@@ -97,8 +97,9 @@ class WebStrand {
9797
         }
9898
         
9999
         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) {
102103
                 node.applyForce(0, 0.1);
103104
             }
104105
         }
@@ -144,6 +145,13 @@ class WebStrand {
144145
 
145146
         push();
146147
 
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
+
147155
         // Change color based on tension
148156
         if (this.tension > 0.8) {
149157
             stroke(255, 200, 200, 200); // Reddish when strained
@@ -227,11 +235,11 @@ class WebStrand {
227235
         
228236
         // Add some energy dissipation through the web network (more subtle)
229237
         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);
233241
             if (minDist < 100) {
234
-                let forceFalloff = map(minDist, 0, 100, 0.3, 0);
242
+                const forceFalloff = map(minDist, 0, 100, 0.3, 0);
235243
                 node.applyForce(0, force * forceFalloff * 0.15);
236244
             }
237245
         }