zeroed-some/cob / 054467b

Browse files

touch support for store modals

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
054467b8b1902307aa951669087b489bbcca7a03
Parents
16abfe2
Tree
438b375

2 changed files

StatusFile+-
M css/styles.css 19 0
M js/game.js 252 174
css/styles.cssmodified
@@ -84,3 +84,22 @@ canvas {
8484
   text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
8585
 }
8686
 
87
+  /* Make buttons touch-friendly */
88
+  #upgrade-shop button,
89
+  #stats-panel button,
90
+  #game-over-screen button {
91
+    min-height: 44px;
92
+    min-width: 44px;
93
+    touch-action: manipulation;
94
+    -webkit-tap-highlight-color: transparent;
95
+    user-select: none;
96
+    cursor: pointer;
97
+  }
98
+  
99
+  /* Prevent scroll/zoom issues on mobile */
100
+  #upgrade-shop,
101
+  #stats-panel {
102
+    touch-action: none;
103
+    -webkit-user-select: none;
104
+    user-select: none;
105
+  }
js/game.jsmodified
@@ -70,7 +70,7 @@ let isExhausted = false
7070
 let fliesMunchedLastNight = 0
7171
 let birds = []
7272
 let staminaRegenCooldown = 0
73
-let staminaBonus = 0;
73
+let staminaBonus = 0
7474
 
7575
 // PHASE 4B: Wind System
7676
 let windActive = false
@@ -477,99 +477,99 @@ function setup () {
477477
   let numObstacles = Math.floor((width * height) / 60000) // More obstacles
478478
   numObstacles = constrain(numObstacles, 15, 25)
479479
 
480
-// Create ant balloons
481
-let numBalloons = Math.floor(random(15, 21))
482
-for (let i = 0; i < numBalloons; i++) {
483
-  let attempts = 0
484
-  let placed = false
485
-
486
-  while (!placed && attempts < 30) {
487
-    // FIX: True random distribution with better spread
488
-    let x, y
489
-    
490
-    // Use different strategies for better distribution
491
-    let strategy = random()
492
-    
493
-    if (strategy < 0.3) {
494
-      // 30% - Truly random across upper area
495
-      x = random(80, width - 80)
496
-      y = random(60, height * 0.5)
497
-    } else if (strategy < 0.6) {
498
-      // 30% - Radial distribution from center
499
-      let angle = random(TWO_PI)
500
-      let radius = random(100, min(width, height) * 0.35)
501
-      x = width / 2 + cos(angle) * radius
502
-      y = height * 0.35 + sin(angle) * radius * 0.7  // Elliptical, flatter
503
-      x = constrain(x, 80, width - 80)
504
-      y = constrain(y, 60, height * 0.6)
505
-    } else if (strategy < 0.8) {
506
-      // 20% - Edge preference for variety
507
-      if (random() < 0.5) {
508
-        x = random() < 0.5 ? random(80, 150) : random(width - 150, width - 80)
480
+  // Create ant balloons
481
+  let numBalloons = Math.floor(random(15, 21))
482
+  for (let i = 0; i < numBalloons; i++) {
483
+    let attempts = 0
484
+    let placed = false
485
+
486
+    while (!placed && attempts < 30) {
487
+      // FIX: True random distribution with better spread
488
+      let x, y
489
+
490
+      // Use different strategies for better distribution
491
+      let strategy = random()
492
+
493
+      if (strategy < 0.3) {
494
+        // 30% - Truly random across upper area
495
+        x = random(80, width - 80)
509496
         y = random(60, height * 0.5)
497
+      } else if (strategy < 0.6) {
498
+        // 30% - Radial distribution from center
499
+        let angle = random(TWO_PI)
500
+        let radius = random(100, min(width, height) * 0.35)
501
+        x = width / 2 + cos(angle) * radius
502
+        y = height * 0.35 + sin(angle) * radius * 0.7 // Elliptical, flatter
503
+        x = constrain(x, 80, width - 80)
504
+        y = constrain(y, 60, height * 0.6)
505
+      } else if (strategy < 0.8) {
506
+        // 20% - Edge preference for variety
507
+        if (random() < 0.5) {
508
+          x = random() < 0.5 ? random(80, 150) : random(width - 150, width - 80)
509
+          y = random(60, height * 0.5)
510
+        } else {
511
+          x = random(80, width - 80)
512
+          y = random(60, 120)
513
+        }
510514
       } else {
511
-        x = random(80, width - 80)
512
-        y = random(60, 120)
513
-      }
514
-    } else {
515
-      // 20% - Poisson disk sampling attempt (avoid clusters)
516
-      let bestX = random(80, width - 80)
517
-      let bestY = random(60, height * 0.6)
518
-      let bestMinDist = 0
519
-      
520
-      // Try a few positions and pick the one furthest from existing balloons
521
-      for (let j = 0; j < 5; j++) {
522
-        let testX = random(80, width - 80)
523
-        let testY = random(60, height * 0.6)
524
-        let minDist = Infinity
525
-        
526
-        for (let obstacle of obstacles) {
527
-          if (obstacle.type === 'balloon') {
528
-            let d = dist(testX, testY, obstacle.x, obstacle.y)
529
-            minDist = min(minDist, d)
515
+        // 20% - Poisson disk sampling attempt (avoid clusters)
516
+        let bestX = random(80, width - 80)
517
+        let bestY = random(60, height * 0.6)
518
+        let bestMinDist = 0
519
+
520
+        // Try a few positions and pick the one furthest from existing balloons
521
+        for (let j = 0; j < 5; j++) {
522
+          let testX = random(80, width - 80)
523
+          let testY = random(60, height * 0.6)
524
+          let minDist = Infinity
525
+
526
+          for (let obstacle of obstacles) {
527
+            if (obstacle.type === 'balloon') {
528
+              let d = dist(testX, testY, obstacle.x, obstacle.y)
529
+              minDist = min(minDist, d)
530
+            }
531
+          }
532
+
533
+          if (minDist > bestMinDist) {
534
+            bestMinDist = minDist
535
+            bestX = testX
536
+            bestY = testY
530537
           }
531538
         }
532
-        
533
-        if (minDist > bestMinDist) {
534
-          bestMinDist = minDist
535
-          bestX = testX
536
-          bestY = testY
537
-        }
539
+
540
+        x = bestX
541
+        y = bestY
538542
       }
539
-      
540
-      x = bestX
541
-      y = bestY
542
-    }
543543
 
544
-    let radius = random(35, 50) // Varied sizes for visual interest
544
+      let radius = random(35, 50) // Varied sizes for visual interest
545545
 
546
-    let valid = true
547
-    // Check distance from other obstacles
548
-    for (let obstacle of obstacles) {
549
-      if (
550
-        dist(x, y, obstacle.x, obstacle.y) <
551
-        radius + obstacle.radius + 40
552
-      ) {
553
-        valid = false
554
-        break
546
+      let valid = true
547
+      // Check distance from other obstacles
548
+      for (let obstacle of obstacles) {
549
+        if (
550
+          dist(x, y, obstacle.x, obstacle.y) <
551
+          radius + obstacle.radius + 40
552
+        ) {
553
+          valid = false
554
+          break
555
+        }
555556
       }
556
-    }
557557
 
558
-    // Check distance from home branch
559
-    if (valid && window.homeBranch) {
560
-      let branchY = window.homeBranch.y
561
-      if (Math.abs(y - branchY) < radius + 40) {
562
-        valid = false
558
+      // Check distance from home branch
559
+      if (valid && window.homeBranch) {
560
+        let branchY = window.homeBranch.y
561
+        if (Math.abs(y - branchY) < radius + 40) {
562
+          valid = false
563
+        }
563564
       }
564
-    }
565565
 
566
-    if (valid) {
567
-      obstacles.push(new Obstacle(x, y, radius, 'balloon'))
568
-      placed = true
566
+      if (valid) {
567
+        obstacles.push(new Obstacle(x, y, radius, 'balloon'))
568
+        placed = true
569
+      }
570
+      attempts++
569571
     }
570
-    attempts++
571572
   }
572
-}
573573
 
574574
   // Create beetles
575575
   let numBeetles = Math.floor(random(9, 15))
@@ -1411,20 +1411,39 @@ function openStatsPanel () {
14111411
   // Show panel
14121412
   document.getElementById('stats-panel').style.display = 'block'
14131413
 
1414
-  // Add close button listener
1415
-  document.getElementById('close-stats-btn').onclick = () => {
1414
+  // FIX: Add both click AND touch listeners
1415
+  let closeBtn = document.getElementById('close-stats-btn')
1416
+
1417
+  // Remove any existing listeners
1418
+  closeBtn.replaceWith(closeBtn.cloneNode(true))
1419
+  closeBtn = document.getElementById('close-stats-btn')
1420
+
1421
+  closeBtn.addEventListener('click', function () {
14161422
     document.getElementById('stats-panel').style.display = 'none'
1423
+    if (gamePhase === 'DAY') {
1424
+      gamePhase = 'DAY_TO_DUSK'
1425
+      phaseTimer = 0
1426
+    }
1427
+  })
14171428
 
1418
-    // IMMEDIATELY transition to dusk after closing stats
1429
+  closeBtn.addEventListener('touchend', function (e) {
1430
+    e.preventDefault()
1431
+    document.getElementById('stats-panel').style.display = 'none'
14191432
     if (gamePhase === 'DAY') {
14201433
       gamePhase = 'DAY_TO_DUSK'
14211434
       phaseTimer = 0
14221435
     }
1423
-  }
1436
+  })
14241437
 }
14251438
 
14261439
 // Make selectSkin global
1427
-window.selectSkin = function (skinId) {
1440
+window.selectSkin = function (skinId, event) {
1441
+  // Prevent touch issues
1442
+  if (event) {
1443
+    event.preventDefault()
1444
+    event.stopPropagation()
1445
+  }
1446
+
14281447
   if (unlockedSkins[skinId]) {
14291448
     currentSkin = skinId
14301449
     saveGame()
@@ -1653,7 +1672,7 @@ function saveGame () {
16531672
     nightsSurvived: nightsSurvived,
16541673
     currentNight: currentNight,
16551674
     playerPoints: playerPoints,
1656
-    spentPoints: spentPoints,
1675
+    spentPoints: spentPoints
16571676
   }
16581677
 
16591678
   localStorage.setItem('cobGameSave', JSON.stringify(saveData))
@@ -1792,23 +1811,32 @@ function spawnDawnBirds () {
17921811
 // ============================================
17931812
 
17941813
 function openUpgradeShop () {
1795
-  if (currentNight <= 1) return // No shop on first night
1814
+  if (currentNight <= 1) return
17961815
 
17971816
   shopOpen = true
17981817
   noLoop() // Pause the game
17991818
 
1800
-  // Calculate points from flies caught this session
1801
-  // playerPoints = totalFliesCaught
1802
-
18031819
   // Update shop UI
18041820
   document.getElementById('upgrade-shop').style.display = 'block'
1805
-  document.getElementById('available-points').textContent = playerPoints - spentPoints
1821
+  document.getElementById('available-points').textContent =
1822
+    playerPoints - spentPoints
18061823
 
18071824
   // Populate upgrade lists
18081825
   updateShopDisplay()
18091826
 
1810
-  // Add continue button listener
1811
-  document.getElementById('continue-btn').onclick = closeUpgradeShop
1827
+  // FIX: Add both click AND touch listeners for mobile
1828
+  let continueBtn = document.getElementById('continue-btn')
1829
+
1830
+  // Remove any existing listeners to prevent duplicates
1831
+  continueBtn.replaceWith(continueBtn.cloneNode(true))
1832
+  continueBtn = document.getElementById('continue-btn')
1833
+
1834
+  // Add both click and touch support
1835
+  continueBtn.addEventListener('click', closeUpgradeShop)
1836
+  continueBtn.addEventListener('touchend', function (e) {
1837
+    e.preventDefault() // Prevent ghost clicks
1838
+    closeUpgradeShop()
1839
+  })
18121840
 }
18131841
 
18141842
 function closeUpgradeShop () {
@@ -1862,7 +1890,7 @@ function updateShopDisplay () {
18621890
       }/${upgrade.maxLevel})
18631891
                             <br><small>${upgrade.description}</small>
18641892
                         </div>
1865
-                        <button onclick="buyUpgrade('${key}')" ${buttonDisabled}
1893
+                        <button ontouchend="buyUpgrade('${key}')" onclick="buyUpgrade('${key}')" ${buttonDisabled}
18661894
                                 style="padding: 5px 15px; background: ${
18671895
                                   canAfford && !maxed ? '#4CAF50' : '#666'
18681896
                                 }; 
@@ -1907,7 +1935,7 @@ function updateShopDisplay () {
19071935
       }/${upgrade.maxLevel})
19081936
                             <br><small>${upgrade.description}</small>
19091937
                         </div>
1910
-                        <button onclick="buyUpgrade('${key}')" ${buttonDisabled}
1938
+                        <button ontouchend="buyUpgrade('${key}')" onclick="buyUpgrade('${key}')" ${buttonDisabled}
19111939
                                 style="padding: 5px 15px; background: ${
19121940
                                   canAfford && !maxed ? '#FF69B4' : '#666'
19131941
                                 }; 
@@ -1934,6 +1962,12 @@ function updateShopDisplay () {
19341962
 
19351963
 // Make buyUpgrade global so onclick can access it
19361964
 window.buyUpgrade = function (upgradeKey) {
1965
+  // Prevent any touch/click propagation issues
1966
+  if (event) {
1967
+    event.preventDefault()
1968
+    event.stopPropagation()
1969
+  }
1970
+
19371971
   let upgrade = upgrades[upgradeKey]
19381972
   if (!upgrade) return
19391973
 
@@ -1949,16 +1983,17 @@ window.buyUpgrade = function (upgradeKey) {
19491983
   }
19501984
 
19511985
   // Check if can afford and not maxed
1952
-  let availablePoints = playerPoints - spentPoints  // Calculate available points
1986
+  let availablePoints = playerPoints - spentPoints // Calculate available points
19531987
   if (availablePoints >= upgrade.cost && upgrade.level < upgrade.maxLevel) {
1954
-    spentPoints += upgrade.cost  // Track spent points
1988
+    spentPoints += upgrade.cost // Track spent points
19551989
     upgrade.level++
19561990
 
19571991
     // Apply upgrade effects immediately
19581992
     applyUpgradeEffects()
19591993
 
19601994
     // Update display with available points
1961
-    document.getElementById('available-points').textContent = playerPoints - spentPoints
1995
+    document.getElementById('available-points').textContent =
1996
+      playerPoints - spentPoints
19621997
     updateShopDisplay()
19631998
 
19641999
     // Show notification
@@ -2709,39 +2744,48 @@ function updateUI () {
27092744
     `<br><small ${staminaColor}>Dawn Stamina: ${potentialStamina}</small>`
27102745
 
27112746
   if (gamePhase === 'NIGHT') {
2712
-  let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60);
2713
-  
2714
-  // Calculate current munch percentage
2715
-  let totalFliesInNight = fliesSpawnedThisNight + flies.length;
2716
-  let currentMunchPercent = totalFliesInNight > 0 ? 
2717
-    Math.floor((fliesMunched / totalFliesInNight) * 100) : 0;
2718
-  
2719
-  // Calculate predicted dawn stamina
2720
-  let predictedStamina;
2721
-  if (currentMunchPercent >= 50) {
2722
-    predictedStamina = 100;
2723
-  } else {
2724
-    predictedStamina = Math.floor(20 + (currentMunchPercent * 2) * 0.8);
2725
-  }
2726
-  
2727
-  timerText = `${timeLeft}s • ${flies.length} flies`;
2728
-  
2729
-  // Show special fly counts if any
2730
-  let goldenCount = flies.filter(f => f.type === 'golden').length;
2731
-  let mothCount = flies.filter(f => f.type === 'moth').length;
2732
-  let queenCount = flies.filter(f => f.type === 'queen').length;
2733
-  
2734
-  if (goldenCount > 0 || mothCount > 0 || queenCount > 0) {
2735
-    let specialCounts = [];
2736
-    if (queenCount > 0) specialCounts.push(`${queenCount}👑`);
2737
-    if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`);
2738
-    if (mothCount > 0) specialCounts.push(`${mothCount}🦋`);
2739
-    timerText += ` (${specialCounts.join(' ')})`;
2740
-  }
2741
-   // Show munch progress
2742
-  document.getElementById('timer').innerHTML = timerText + 
2743
-    `<br><small style="color: ${predictedStamina < 40 ? '#ff4444' : predictedStamina < 70 ? '#ffaa44' : '#44ff44'}">` +
2744
-    `Munched: ${currentMunchPercent}% → ${predictedStamina} dawn stamina</small>`;
2747
+    let timeLeft = Math.ceil((NIGHT_DURATION - phaseTimer) / 60)
2748
+
2749
+    // Calculate current munch percentage
2750
+    let totalFliesInNight = fliesSpawnedThisNight + flies.length
2751
+    let currentMunchPercent =
2752
+      totalFliesInNight > 0
2753
+        ? Math.floor((fliesMunched / totalFliesInNight) * 100)
2754
+        : 0
2755
+
2756
+    // Calculate predicted dawn stamina
2757
+    let predictedStamina
2758
+    if (currentMunchPercent >= 50) {
2759
+      predictedStamina = 100
2760
+    } else {
2761
+      predictedStamina = Math.floor(20 + currentMunchPercent * 2 * 0.8)
2762
+    }
2763
+
2764
+    timerText = `${timeLeft}s • ${flies.length} flies`
2765
+
2766
+    // Show special fly counts if any
2767
+    let goldenCount = flies.filter(f => f.type === 'golden').length
2768
+    let mothCount = flies.filter(f => f.type === 'moth').length
2769
+    let queenCount = flies.filter(f => f.type === 'queen').length
2770
+
2771
+    if (goldenCount > 0 || mothCount > 0 || queenCount > 0) {
2772
+      let specialCounts = []
2773
+      if (queenCount > 0) specialCounts.push(`${queenCount}👑`)
2774
+      if (goldenCount > 0) specialCounts.push(`${goldenCount}✨`)
2775
+      if (mothCount > 0) specialCounts.push(`${mothCount}🦋`)
2776
+      timerText += ` (${specialCounts.join(' ')})`
2777
+    }
2778
+    // Show munch progress
2779
+    document.getElementById('timer').innerHTML =
2780
+      timerText +
2781
+      `<br><small style="color: ${
2782
+        predictedStamina < 40
2783
+          ? '#ff4444'
2784
+          : predictedStamina < 70
2785
+          ? '#ffaa44'
2786
+          : '#44ff44'
2787
+      }">` +
2788
+      `Munched: ${currentMunchPercent}% → ${predictedStamina} dawn stamina</small>`
27452789
   } else if (gamePhase === 'DAWN') {
27462790
     let timeLeft = Math.ceil((DAWN_DURATION - phaseTimer) / 60)
27472791
     // PHASE 4: Show birds and exhaustion status
@@ -2786,49 +2830,56 @@ function updateUI () {
27862830
   }
27872831
 
27882832
   // PHASE 4: Update meter based on phase
2789
-if (gamePhase === 'DAWN') {
2790
-  // Show stamina instead of silk during dawn
2791
-  document.getElementById('web-meter-label').textContent = 'STAMINA';
2792
-  
2793
-  // FIX: Always show percentage out of 100, not out of variable max
2794
-  let staminaPercent = (jumpStamina / 100) * 100; // Always out of 100
2795
-  document.getElementById('web-meter-fill').style.width = staminaPercent + '%';
2796
-
2797
-  // Color based on stamina level
2798
-  if (jumpStamina < 20) {
2799
-    // Exhausted - red flash
2800
-    let flash = sin(frameCount * 0.3) * 0.5 + 0.5;
2801
-    document.getElementById('web-meter-fill').style.background = 
2802
-      `linear-gradient(90deg, rgb(255, ${50 + flash * 50}, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`;
2803
-  } else if (jumpStamina < 40) {
2804
-    // Very tired - orange-red
2805
-    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #FF6B35, #FF4444)';
2806
-  } else if (jumpStamina < 60) {
2807
-    // Tired - orange
2808
-    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #FFA500, #FF8C00)';
2809
-  } else if (jumpStamina < 80) {
2810
-    // OK - yellow-orange
2811
-    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #FFD700, #FFA500)';
2833
+  if (gamePhase === 'DAWN') {
2834
+    // Show stamina instead of silk during dawn
2835
+    document.getElementById('web-meter-label').textContent = 'STAMINA'
2836
+
2837
+    // FIX: Always show percentage out of 100, not out of variable max
2838
+    let staminaPercent = (jumpStamina / 100) * 100 // Always out of 100
2839
+    document.getElementById('web-meter-fill').style.width = staminaPercent + '%'
2840
+
2841
+    // Color based on stamina level
2842
+    if (jumpStamina < 20) {
2843
+      // Exhausted - red flash
2844
+      let flash = sin(frameCount * 0.3) * 0.5 + 0.5
2845
+      document.getElementById(
2846
+        'web-meter-fill'
2847
+      ).style.background = `linear-gradient(90deg, rgb(255, ${
2848
+        50 + flash * 50
2849
+      }, ${50 + flash * 50}), rgb(200, ${30 + flash * 30}, ${30 + flash * 30}))`
2850
+    } else if (jumpStamina < 40) {
2851
+      // Very tired - orange-red
2852
+      document.getElementById('web-meter-fill').style.background =
2853
+        'linear-gradient(90deg, #FF6B35, #FF4444)'
2854
+    } else if (jumpStamina < 60) {
2855
+      // Tired - orange
2856
+      document.getElementById('web-meter-fill').style.background =
2857
+        'linear-gradient(90deg, #FFA500, #FF8C00)'
2858
+    } else if (jumpStamina < 80) {
2859
+      // OK - yellow-orange
2860
+      document.getElementById('web-meter-fill').style.background =
2861
+        'linear-gradient(90deg, #FFD700, #FFA500)'
2862
+    } else {
2863
+      // Good stamina - green-yellow
2864
+      document.getElementById('web-meter-fill').style.background =
2865
+        'linear-gradient(90deg, #90EE90, #FFD700)'
2866
+    }
2867
+
2868
+    // Show critical warning overlay
2869
+    if (jumpStamina <= 0 && !gameOver) {
2870
+      push()
2871
+      fill(255, 0, 0, 50 + sin(frameCount * 0.3) * 50)
2872
+      rect(0, 0, width, height)
2873
+
2874
+      textAlign(CENTER)
2875
+      textSize(32)
2876
+      fill(255, 50, 50)
2877
+      stroke(0)
2878
+      strokeWeight(3)
2879
+      text('NO STAMINA - AVOID BIRDS!', width / 2, height / 2)
2880
+      pop()
2881
+    }
28122882
   } else {
2813
-    // Good stamina - green-yellow
2814
-    document.getElementById('web-meter-fill').style.background = 'linear-gradient(90deg, #90EE90, #FFD700)';
2815
-  }
2816
-  
2817
-  // Show critical warning overlay
2818
-  if (jumpStamina <= 0 && !gameOver) {
2819
-    push();
2820
-    fill(255, 0, 0, 50 + sin(frameCount * 0.3) * 50);
2821
-    rect(0, 0, width, height);
2822
-
2823
-    textAlign(CENTER);
2824
-    textSize(32);
2825
-    fill(255, 50, 50);
2826
-    stroke(0);
2827
-    strokeWeight(3);
2828
-    text('NO STAMINA - AVOID BIRDS!', width / 2, height / 2);
2829
-    pop();
2830
-  }
2831
-} else {
28322883
     // Normal silk meter
28332884
     document.getElementById('web-meter-label').textContent = 'SILK'
28342885
     let meterPercent = (webSilk / maxWebSilk) * 100
@@ -2918,6 +2969,13 @@ function showGameOverScreen () {
29182969
     `
29192970
 
29202971
   document.body.insertAdjacentHTML('beforeend', gameOverHTML)
2972
+  // FIX: Add touch support to restart button
2973
+  let restartBtn = document.getElementById('restart-btn')
2974
+  restartBtn.addEventListener('click', restartGame)
2975
+  restartBtn.addEventListener('touchend', function (e) {
2976
+    e.preventDefault()
2977
+    restartGame()
2978
+  })
29212979
 }
29222980
 
29232981
 // Add restart game function:
@@ -3125,6 +3183,14 @@ function recycleNearbyWeb () {
31253183
 }
31263184
 
31273185
 function touchStarted () {
3186
+  // FIX: Don't process game touches when modals are open
3187
+  if (
3188
+    shopOpen ||
3189
+    document.getElementById('stats-panel').style.display === 'block'
3190
+  ) {
3191
+    return false
3192
+  }
3193
+
31283194
   if (touches.length > 0) {
31293195
     touchStartTime = millis()
31303196
     touchStartX = touches[0].x
@@ -3188,6 +3254,12 @@ function touchStarted () {
31883254
 }
31893255
 
31903256
 function touchMoved () {
3257
+  if (
3258
+    shopOpen ||
3259
+    document.getElementById('stats-panel').style.display === 'block'
3260
+  ) {
3261
+    return false
3262
+  }
31913263
   // Update web deployment target while holding
31923264
   if (
31933265
     touchHolding &&
@@ -3206,6 +3278,12 @@ function touchMoved () {
32063278
 }
32073279
 
32083280
 function touchEnded () {
3281
+  if (
3282
+    shopOpen ||
3283
+    document.getElementById('stats-panel').style.display === 'block'
3284
+  ) {
3285
+    return false
3286
+  }
32093287
   touchHolding = false
32103288
   touchProcessing = false
32113289