[object Object]

← back to Neon Baguette Battle

Add mobile touch controls (drive Player 1)

6e85f4c7f44d453af64ed7a949dc0fcd62b140a9 · 2026-08-31 14:34:14 -0700 · Steve Abrams

- On-screen buttons overlay the arena during a match: left cluster JUMP/BLOCK/left/right, right cluster POKE/WHACK/throw-baguette/rocket/bomb; touchstart/end + mouse map to the same key state
- Auto-shows on touch devices (and on first touchscreen), only during a match; releases held keys on leave
- Mobile-friendly select: tap a card + tap START launches with current picks (no keyboard ready-up needed)
- viewport maximum-scale=1 to stop pinch/double-tap zoom; portrait rotate hint
- Verified via hasTouch emulation: overlay shows, buttons poke/move/rocket P1, tap-to-start works, 0 errors

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 6e85f4c7f44d453af64ed7a949dc0fcd62b140a9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 14:34:14 2026 -0700

    Add mobile touch controls (drive Player 1)
    
    - On-screen buttons overlay the arena during a match: left cluster JUMP/BLOCK/left/right, right cluster POKE/WHACK/throw-baguette/rocket/bomb; touchstart/end + mouse map to the same key state
    - Auto-shows on touch devices (and on first touchscreen), only during a match; releases held keys on leave
    - Mobile-friendly select: tap a card + tap START launches with current picks (no keyboard ready-up needed)
    - viewport maximum-scale=1 to stop pinch/double-tap zoom; portrait rotate hint
    - Verified via hasTouch emulation: overlay shows, buttons poke/move/rocket P1, tap-to-start works, 0 errors
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 README.md  |  7 ++++++
 index.html | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 84 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 0edc58d..3c426f0 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,13 @@ also click a card to choose P1's fighter. `Enter`/`Space` or **START FIGHT** beg
 otherwise it does a whack. (`J`/`K`/`L` also jab/whack/launch.) In 2-player the
 arrows belong to Player 2.
 
+## Mobile / touch
+On a touch device, on-screen controls appear during a match — a left cluster
+(JUMP · BLOCK · ◀ ▶) and a right cluster (POKE · WHACK · 🥖 throw · 🚀 rocket ·
+💣 bomb) — driving Player 1, so 1-player vs CPU plays great on a phone. Tap a
+fighter card then **START FIGHT** to begin (no keyboard needed). Best held in
+landscape (a rotate hint shows in portrait).
+
 ## Features
 - **Real combat + a weapon loadout**: baguette **poke** and **whack** (melee),
   plus three throwables — **throw baguette** (spinning missile, short cooldown),
diff --git a/index.html b/index.html
index c7b1a52..2867d17 100644
--- a/index.html
+++ b/index.html
@@ -2,7 +2,7 @@
 <html lang="en">
 <head>
 <meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
 <title>Baguette Battle — Neon Paris</title>
 <style>
   :root{ --gold:#c9a961; --cream:#f5f2ea; --pink:#ff3d9a; --cyan:#22e0ff; }
@@ -94,6 +94,27 @@
   .card .rdy{ position:absolute; top:5px; font-size:11px; font-weight:700; padding:2px 8px; border-radius:5px; z-index:2; }
   .card .rdy.r1{ left:5px; background:#c0392b; color:#fff; }
   .card .rdy.r2{ right:5px; background:#2c6fb0; color:#fff; }
+
+  /* touch controls (mobile) */
+  #touch{ position:absolute; inset:0; z-index:6; display:none; pointer-events:none; }
+  body.touch #touch.on{ display:block; }
+  .tc{ position:absolute; bottom:12px; display:flex; flex-wrap:wrap; gap:8px; pointer-events:none; }
+  .tc-left{ left:12px; width:118px; }
+  .tc-right{ right:12px; width:190px; justify-content:flex-end; }
+  .tbtn{ pointer-events:auto; -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;
+         touch-action:none; width:52px; height:52px; border-radius:50%;
+         border:1px solid rgba(255,255,255,.32); background:rgba(18,14,28,.5);
+         color:#f5f2ea; font:600 11px/1 Georgia,serif; -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
+         display:flex; align-items:center; justify-content:center; box-shadow:0 2px 10px rgba(0,0,0,.4); }
+  .tbtn.wide{ width:55px; height:44px; border-radius:12px; font-size:11px; letter-spacing:.04em; }
+  .tbtn.atk{ width:56px; height:56px; font-size:12px; }
+  .tbtn[data-k="a"], .tbtn[data-k="d"]{ font-size:20px; }
+  .tbtn:active{ background:rgba(201,169,97,.55); color:#1a140a; transform:scale(.93); }
+  #rotate{ display:none; }
+  @media (orientation:portrait) and (max-width:820px){
+    body.touch #rotate{ position:fixed; inset:0; z-index:30; display:flex; align-items:center; justify-content:center;
+      background:rgba(5,5,12,.92); color:#f5f2ea; font-size:20px; letter-spacing:.06em; text-align:center; padding:24px; }
+  }
 </style>
 </head>
 <body>
@@ -174,6 +195,24 @@
     </div>
     <div class="tiny">Press&nbsp;<span class="key">P</span>&nbsp;to resume</div>
   </div>
+
+  <!-- TOUCH CONTROLS (mobile) — drive Player 1 -->
+  <div id="touch" aria-hidden="true">
+    <div class="tc tc-left">
+      <button class="tbtn wide" data-k="w">JUMP</button>
+      <button class="tbtn wide" data-k="s">BLOCK</button>
+      <button class="tbtn" data-k="a">◀</button>
+      <button class="tbtn" data-k="d">▶</button>
+    </div>
+    <div class="tc tc-right">
+      <button class="tbtn atk" data-k="f">POKE</button>
+      <button class="tbtn atk" data-k="g">WHACK</button>
+      <button class="tbtn atk" data-k="h">🥖</button>
+      <button class="tbtn atk" data-k="r">🚀</button>
+      <button class="tbtn atk" data-k="b">💣</button>
+    </div>
+  </div>
+  <div id="rotate">↺ Rotate your device to landscape</div>
 </div>
 
 <script>
@@ -1189,9 +1228,8 @@
     });
     document.getElementById('p1tag').textContent = 'P1: ' + ROSTER[p1Idx].name + (p1Ready?' ✔':'');
     document.getElementById('p2tag').textContent = (mode==='2p'?'P2: ':'CPU: ') + ROSTER[p2Idx].name + (p2Ready?' ✔':'');
-    const both = p1Ready && p2Ready;
     const b = document.getElementById('btnStart');
-    b.disabled = !both; b.textContent = both ? 'START FIGHT ▶' : 'READY UP';
+    b.disabled = false; b.textContent = 'START FIGHT ▶';   // always launchable with current picks (mobile-friendly)
     document.getElementById('selHint').innerHTML = mode==='2p'
       ? 'P1 <span class="key">A</span><span class="key">D</span> move · <span class="key">F</span> ready &nbsp;·&nbsp; P2 <span class="key">←</span><span class="key">→</span> move · <span class="key">,</span> ready'
       : 'P1 <span class="key">A</span><span class="key">D</span> move · <span class="key">F</span> ready · CPU auto-picks · click a card to choose';
@@ -1219,7 +1257,7 @@
     Audio.bell(); renderSelect();
   }
   function beginSelectedMatch(){
-    if (!(p1Ready && p2Ready)) return;
+    p1Ready = true; p2Ready = true;                // launch with whatever is currently selected
     p1map = (mode==='2p') ? P1MAP_2P : P1MAP_1P;   // 1P: P1 also gets arrows + space/⌘
     f1 = new Fighter(W*0.32,'p1', ROSTER[p1Idx], true);
     f2 = new Fighter(W*0.68,'p2', ROSTER[p2Idx], mode==='2p');
@@ -1383,7 +1421,42 @@
     const vg = ctx.createRadialGradient(W/2,H/2,H*0.4,W/2,H/2,H*0.9);
     vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,.45)');
     ctx.fillStyle=vg; ctx.fillRect(0,0,W,H);
+
+    updateTouchVisibility();
+  }
+
+  // ---------- Mobile touch controls (drive Player 1) ----------
+  const touchEl = document.getElementById('touch');
+  let touchEnabled = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
+  const TOUCH_KEYS = ['a','d','w','s','f','g','h','r','b'];
+  let touchWasActive = false;
+  function updateTouchVisibility(){
+    const active = touchEnabled && (state==='fight' || state==='intro' || state==='koPause');
+    if (active !== touchWasActive){
+      touchEl.classList.toggle('on', active);
+      if (!active) TOUCH_KEYS.forEach(k => { keys[k]=false; });   // release any held buttons on leave
+      touchWasActive = active;
+    }
+  }
+  function setupTouch(){
+    if (touchEnabled) document.body.classList.add('touch');
+    // some devices only reveal touch after the first touch
+    window.addEventListener('touchstart', () => { touchEnabled = true; document.body.classList.add('touch'); }, { once:true, passive:true });
+    const press   = k => { if(!keys[k]) keys['_pressed_'+k]=true; keys[k]=true; };
+    const release = k => { keys[k]=false; };
+    touchEl.querySelectorAll('.tbtn').forEach(btn => {
+      const k = btn.dataset.k;
+      const on  = e => { e.preventDefault(); press(k); };
+      const off = e => { e.preventDefault(); release(k); };
+      btn.addEventListener('touchstart', on,  { passive:false });
+      btn.addEventListener('touchend',   off, { passive:false });
+      btn.addEventListener('touchcancel',off, { passive:false });
+      btn.addEventListener('mousedown',  on);   // desktop testing
+      btn.addEventListener('mouseup',    off);
+      btn.addEventListener('mouseleave', off);
+    });
   }
+  setupTouch();
 
   buildRoster();
   // tiny test hook (only when the URL ends in #debug) — no effect on normal play

← beedee1 Polish: distinct per-stat select bars + stronger fighter/flo  ·  back to Neon Baguette Battle  ·  (newest)