Skip to content

Commit 9e7bfbf

Browse files
committed
feat: Add mobile touch controls and opponent leave notification
- Add Up/Down button pairs on left/right sides of control panel - Buttons are subtle, positioned for both left and right-handed players - Notify remaining player when opponent leaves during GameOver - Both players return to start screen if either declines rematch
1 parent 401df5d commit 9e7bfbf

2 files changed

Lines changed: 111 additions & 138 deletions

File tree

lobby_worker/index.html

Lines changed: 104 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -756,35 +756,6 @@
756756
color: white;
757757
}
758758

759-
/* Touch Hint Overlay */
760-
.touch-hint {
761-
position: absolute;
762-
top: 50%;
763-
left: 50%;
764-
transform: translate(-50%, -50%);
765-
color: rgba(255, 255, 255, 0.5);
766-
font-family: 'Outfit', sans-serif;
767-
font-size: 24px;
768-
font-weight: 700;
769-
text-transform: uppercase;
770-
pointer-events: none;
771-
z-index: 40;
772-
text-align: center;
773-
width: 100%;
774-
display: none; /* Hidden by default, shown on mobile */
775-
animation: pulse-hint 2s infinite;
776-
}
777-
778-
@keyframes pulse-hint {
779-
0%, 100% { opacity: 0.3; transform: translate(-50%, -50%) scale(0.95); }
780-
50% { opacity: 0.6; transform: translate(-50%, -50%) scale(1.05); }
781-
}
782-
783-
.touch-hint.fade-out {
784-
opacity: 0;
785-
transition: opacity 0.5s;
786-
}
787-
788759
/* Game Title */
789760
.game-title {
790761
font-family: 'Outfit', sans-serif;
@@ -803,27 +774,57 @@
803774
text-transform: uppercase;
804775
}
805776

806-
/* Touch zones - visible on all touch devices including iPad */
807-
.touch-zone {
808-
position: absolute;
809-
top: 0;
810-
bottom: 0;
811-
width: 50%;
812-
z-index: 50;
813-
touch-action: none;
777+
/* Mobile touch buttons - subtle design for side panel */
778+
.touch-controls {
779+
display: none; /* Hidden by default, shown on mobile */
780+
flex-direction: column;
781+
gap: 8px;
782+
padding: 5px;
783+
align-items: center;
784+
justify-content: center;
785+
}
786+
787+
.touch-btn {
788+
width: 40px;
789+
height: 40px;
790+
border: 1px solid rgba(255, 255, 255, 0.3);
791+
border-radius: 8px;
792+
background: rgba(99, 102, 241, 0.2);
793+
color: rgba(255, 255, 255, 0.7);
794+
font-size: 16px;
795+
font-weight: bold;
796+
display: flex;
797+
align-items: center;
798+
justify-content: center;
799+
touch-action: manipulation;
814800
-webkit-touch-callout: none;
815801
-webkit-user-select: none;
816802
user-select: none;
803+
cursor: pointer;
804+
transition: background 0.1s, transform 0.1s;
817805
}
818806

819-
#touchZoneLeft { left: 0; }
820-
#touchZoneRight { right: 0; }
807+
.touch-btn:active, .touch-btn.pressed {
808+
background: rgba(99, 102, 241, 0.5);
809+
color: white;
810+
transform: scale(0.95);
811+
}
821812

822-
/* Show touch zones on any touch device (includes iPad) */
823-
@media (pointer: coarse) {
824-
.touch-hint {
825-
display: block;
813+
/* Show touch controls on mobile */
814+
@media (max-width: 850px) {
815+
#sidePanel {
816+
flex-direction: row !important;
817+
align-items: stretch;
818+
}
819+
820+
.touch-controls {
821+
display: flex !important;
826822
}
823+
824+
.panel-card {
825+
flex: 1;
826+
}
827+
827828
.controls-info {
828829
display: none;
829830
}
@@ -938,7 +939,7 @@
938939
border-radius: 12px;
939940
box-shadow: var(--shadow-lg);
940941
background: linear-gradient(145deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
941-
overflow: hidden;
942+
overflow: visible; /* Allow touch buttons to show */
942943
}
943944

944945
#canvas {
@@ -976,15 +977,6 @@ <h1 class="game-title">PONGO</h1>
976977
<!-- Game Area -->
977978
<div id="gameContainer">
978979
<canvas id="canvas" width="640" height="480"></canvas>
979-
<!-- Invisible Touch Zones -->
980-
<div id="touchZoneLeft" class="touch-zone"></div>
981-
<div id="touchZoneRight" class="touch-zone"></div>
982-
983-
<!-- Visual Hint for Controls -->
984-
<div id="touchHint" class="touch-hint">
985-
<span style="font-size: 32px; display: block; margin-bottom: 5px;"></span>
986-
Slide Vertical to Move
987-
</div>
988980

989981
<div id="countdown"></div>
990982
<div id="victoryOverlay">
@@ -996,8 +988,14 @@ <h1 class="game-title">PONGO</h1>
996988
</div>
997989
</div>
998990

999-
<!-- Side/Control Panel -->
991+
<!-- Side/Control Panel with Touch Controls -->
1000992
<div id="sidePanel">
993+
<!-- Mobile Touch Controls - Left Side -->
994+
<div class="touch-controls left">
995+
<button class="touch-btn" data-dir="up"></button>
996+
<button class="touch-btn" data-dir="down"></button>
997+
</div>
998+
1001999
<!-- Single Control Card -->
10021000
<div class="panel-card">
10031001
<div id="scoreRow">
@@ -1087,6 +1085,12 @@ <h1 class="game-title">PONGO</h1>
10871085
</a>
10881086
</div>
10891087
</div>
1088+
1089+
<!-- Mobile Touch Controls - Right Side -->
1090+
<div class="touch-controls right">
1091+
<button class="touch-btn" data-dir="up"></button>
1092+
<button class="touch-btn" data-dir="down"></button>
1093+
</div>
10901094
</div>
10911095
</div>
10921096

@@ -1442,7 +1446,11 @@ <h1 class="game-title">PONGO</h1>
14421446
FSM.transition('COUNTDOWN_DONE');
14431447
}
14441448
} else if (event === 'opponent_disconnected') {
1445-
if (FSM.state === GameState.WAITING || FSM.state === GameState.COUNTDOWN_MULTI || FSM.state === GameState.GAME_OVER_MULTI) {
1449+
// Handle opponent leaving at any multiplayer stage
1450+
if (FSM.state === GameState.WAITING ||
1451+
FSM.state === GameState.COUNTDOWN_MULTI ||
1452+
FSM.state === GameState.PLAYING_MULTI ||
1453+
FSM.state === GameState.GAME_OVER_MULTI) {
14461454
FSM.transition('DISCONNECTED');
14471455
}
14481456
}
@@ -1732,32 +1740,13 @@ <h1 class="game-title">PONGO</h1>
17321740
}
17331741
}, { capture: true, passive: false });
17341742

1735-
// --- Touch Zone Logic (Direct Paddle Tracking) ---
1736-
const canvas = document.getElementById('canvas');
1737-
const gameContainer = document.getElementById('gameContainer');
1743+
// --- Touch Button Controls ---
17381744
let currentTouchDir = 0;
1739-
let lastTouchY = null;
17401745

1741-
// Calculate paddle target based on touch Y position
1742-
const handleTouchForPaddle = (touchY) => {
1743-
if (!canvas || !client) return;
1744-
1745-
const canvasRect = canvas.getBoundingClientRect();
1746-
const relativeY = touchY - canvasRect.top;
1747-
const normalizedY = relativeY / canvasRect.height;
1748-
1749-
// Canvas logical height is 480, paddle moves in roughly middle 80%
1750-
// Map touch position to up/down movement
1751-
const centerY = 0.5;
1752-
const deadZone = 0.1; // 10% dead zone in center
1753-
1754-
let newDir = 0;
1755-
if (normalizedY < centerY - deadZone) {
1756-
newDir = -1; // Up
1757-
} else if (normalizedY > centerY + deadZone) {
1758-
newDir = 1; // Down
1759-
}
1746+
const handleTouchBtnPress = (dir) => {
1747+
if (!client) return;
17601748

1749+
const newDir = dir === 'up' ? -1 : 1;
17611750
if (newDir !== currentTouchDir) {
17621751
// Release old direction
17631752
if (currentTouchDir === -1) client.handle_key_string('ArrowUp', false);
@@ -1770,81 +1759,59 @@ <h1 class="game-title">PONGO</h1>
17701759
currentTouchDir = newDir;
17711760
sendInput();
17721761
}
1773-
1774-
lastTouchY = touchY;
17751762
};
17761763

1777-
const handleTouchEnd = () => {
1778-
if (currentTouchDir === -1) client && client.handle_key_string('ArrowUp', false);
1779-
if (currentTouchDir === 1) client && client.handle_key_string('ArrowDown', false);
1764+
const handleTouchBtnRelease = () => {
1765+
if (!client) return;
1766+
1767+
if (currentTouchDir === -1) client.handle_key_string('ArrowUp', false);
1768+
if (currentTouchDir === 1) client.handle_key_string('ArrowDown', false);
17801769
currentTouchDir = 0;
1781-
lastTouchY = null;
17821770
sendInput();
17831771
};
17841772

1785-
const setupTouchZone = (elementId) => {
1786-
const zone = document.getElementById(elementId);
1787-
if (!zone) return;
1788-
1789-
zone.addEventListener('touchstart', (e) => {
1790-
e.preventDefault();
1791-
const touchY = e.changedTouches[0].clientY;
1792-
handleTouchForPaddle(touchY);
1793-
1794-
// Hide hint on first interaction
1795-
const hint = document.getElementById('touchHint');
1796-
if (hint) {
1797-
hint.classList.add('fade-out');
1798-
setTimeout(() => hint.remove(), 1000);
1799-
}
1800-
}, { passive: false });
1801-
1802-
zone.addEventListener('touchmove', (e) => {
1803-
e.preventDefault();
1804-
handleTouchForPaddle(e.changedTouches[0].clientY);
1805-
}, { passive: false });
1806-
1807-
zone.addEventListener('touchend', (e) => {
1808-
e.preventDefault();
1809-
handleTouchEnd();
1810-
}, { passive: false });
1811-
1812-
zone.addEventListener('touchcancel', (e) => {
1813-
e.preventDefault();
1814-
handleTouchEnd();
1815-
}, { passive: false });
1816-
};
1817-
1818-
setupTouchZone('touchZoneLeft');
1819-
setupTouchZone('touchZoneRight');
1820-
1821-
// Also handle touch directly on canvas for iPad compatibility
1822-
if (canvas) {
1823-
canvas.addEventListener('touchstart', (e) => {
1773+
// Set up touch button event listeners
1774+
document.querySelectorAll('.touch-btn').forEach(btn => {
1775+
const dir = btn.dataset.dir;
1776+
1777+
btn.addEventListener('touchstart', (e) => {
18241778
e.preventDefault();
1825-
handleTouchForPaddle(e.changedTouches[0].clientY);
1826-
const hint = document.getElementById('touchHint');
1827-
if (hint) {
1828-
hint.classList.add('fade-out');
1829-
setTimeout(() => hint.remove(), 1000);
1830-
}
1779+
btn.classList.add('pressed');
1780+
handleTouchBtnPress(dir);
18311781
}, { passive: false });
18321782

1833-
canvas.addEventListener('touchmove', (e) => {
1783+
btn.addEventListener('touchend', (e) => {
18341784
e.preventDefault();
1835-
handleTouchForPaddle(e.changedTouches[0].clientY);
1785+
btn.classList.remove('pressed');
1786+
handleTouchBtnRelease();
18361787
}, { passive: false });
18371788

1838-
canvas.addEventListener('touchend', (e) => {
1789+
btn.addEventListener('touchcancel', (e) => {
18391790
e.preventDefault();
1840-
handleTouchEnd();
1791+
btn.classList.remove('pressed');
1792+
handleTouchBtnRelease();
18411793
}, { passive: false });
18421794

1843-
canvas.addEventListener('touchcancel', (e) => {
1795+
// Also support mouse for testing on desktop
1796+
btn.addEventListener('mousedown', (e) => {
18441797
e.preventDefault();
1845-
handleTouchEnd();
1846-
}, { passive: false });
1847-
}
1798+
btn.classList.add('pressed');
1799+
handleTouchBtnPress(dir);
1800+
});
1801+
1802+
btn.addEventListener('mouseup', (e) => {
1803+
e.preventDefault();
1804+
btn.classList.remove('pressed');
1805+
handleTouchBtnRelease();
1806+
});
1807+
1808+
btn.addEventListener('mouseleave', () => {
1809+
if (btn.classList.contains('pressed')) {
1810+
btn.classList.remove('pressed');
1811+
handleTouchBtnRelease();
1812+
}
1813+
});
1814+
});
18481815
}
18491816

18501817
// ========================================

server_do/src/game_state.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,13 @@ impl GameState {
200200
self.match_state = MatchState::Waiting;
201201
self.countdown_remaining = 3;
202202
}
203-
MatchState::Waiting | MatchState::GameOver => {
203+
MatchState::GameOver => {
204+
// Notify remaining player that opponent left (won't rematch)
205+
self.broadcast_to_all(&S2C::OpponentDisconnected);
206+
// Reset to waiting state
207+
self.match_state = MatchState::Waiting;
208+
}
209+
MatchState::Waiting => {
204210
// Just update state
205211
if self.clients.is_empty() {
206212
self.match_state = MatchState::Waiting;

0 commit comments

Comments
 (0)