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;
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 }
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 // ========================================
0 commit comments