← 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 <span class="key">P</span> 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 · 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)