diff --git a/game/css/style.css b/game/css/style.css index bf59dca..416ec97 100644 --- a/game/css/style.css +++ b/game/css/style.css @@ -28,7 +28,7 @@ body { max-width: 98vw; } -/* Clean Flexbox Top Navigation Bar (Adaptive & No Overflow) */ +/* Clean Flexbox Top Navigation Bar (Adaptive & Responsive) */ .top-nav-bar { width: 100%; background: linear-gradient(to bottom, #3e2723, #2a160c); @@ -37,7 +37,7 @@ body { border-radius: 10px 10px 0 0; padding: 4px 8px; display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; justify-content: space-between; align-items: center; gap: 4px; @@ -49,10 +49,26 @@ body { .nav-left, .nav-right { display: flex; align-items: center; - flex-wrap: wrap; + flex-wrap: nowrap; gap: 4px; } +.desktop-nav-links { + display: flex; + align-items: center; + gap: 4px; +} + +.btn-drawer-toggle { + display: none; + background: #ff8f00; + color: #fff; + border-color: #ffd54f; +} +.btn-drawer-toggle:hover { + background: #ffa000; +} + .nav-btn { background: #2a160c; color: #ffeb3b; @@ -1892,3 +1908,294 @@ body.theme-winter .banner-card { color: #ffcdd2; border: 1px solid #e53935; } + +/* ========================================================================== + 📱 Mobile Slide-out Drawer Menu & Backdrop + ========================================================================== */ +.drawer-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + height: 100dvh; + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(4px); + z-index: 1000; + transition: opacity 0.25s ease; + opacity: 1; +} + +.drawer-backdrop.hidden { + display: none; + opacity: 0; +} + +.mobile-drawer { + position: fixed; + top: 0; + right: 0; + width: 290px; + max-width: 85vw; + height: 100vh; + height: 100dvh; + background: linear-gradient(180deg, #3e2723 0%, #1a0d05 100%); + border-left: 3px solid #e09f3e; + box-shadow: -8px 0 30px rgba(0, 0, 0, 0.85); + z-index: 1001; + display: flex; + flex-direction: column; + transform: translateX(0); + transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1); + padding-top: env(safe-area-inset-top, 0px); + padding-bottom: env(safe-area-inset-bottom, 0px); + box-sizing: border-box; +} + +.mobile-drawer.hidden { + transform: translateX(100%); + pointer-events: none; +} + +.drawer-header { + padding: 14px 16px; + background: rgba(0, 0, 0, 0.35); + border-bottom: 2px solid #5d4037; + display: flex; + justify-content: space-between; + align-items: center; +} + +.drawer-header h2 { + font-size: 16px; + color: #ffeb3b; + margin: 0; +} + +.btn-drawer-close { + background: rgba(255, 255, 255, 0.1); + border: 1px solid #8d6e63; + color: #fff; + font-size: 16px; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; +} + +.btn-drawer-close:hover { + background: #d32f2f; + border-color: #f44336; +} + +.drawer-body { + flex: 1; + overflow-y: auto; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.drawer-item { + width: 100%; + background: #2a160c; + color: #ffeb3b; + border: 1.5px solid #8d6e63; + border-radius: 8px; + padding: 10px 12px; + font-size: 13px; + font-weight: bold; + text-align: left; + cursor: pointer; + transition: all 0.15s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + gap: 8px; +} + +.drawer-item:hover, .drawer-item:active { + background: #422817; + border-color: #ffd54f; + transform: translateX(-2px); +} + +.btn-drawer-bug { + border-color: #0288d1; + color: #e1f5fe; + margin-top: 6px; +} +.btn-drawer-bug:hover { + background: #0277bd; +} + +/* ========================================================================== + 📱 Comprehensive Mobile & Tablet Responsive Rules (@media) + ========================================================================== */ + +/* Tablets & Mobile Screens (<= 768px) */ +@media screen and (max-width: 768px) { + body { + padding: 0; + justify-content: flex-start; + } + + #game-wrapper { + width: 100%; + max-width: 100vw; + } + + .top-nav-bar { + border-radius: 0; + border-left: none; + border-right: none; + padding: 4px 6px; + padding-top: max(4px, env(safe-area-inset-top)); + } + + /* Hide long list of top links; use drawer button instead */ + .desktop-nav-links { + display: none !important; + } + + .btn-drawer-toggle { + display: inline-flex !important; + } + + .nav-btn { + font-size: 10px; + padding: 4px 6px; + } + + .profile-badge { + padding: 2px 4px; + font-size: 10px; + } + .profile-badge span { + max-width: 70px; + } + + #game-container { + border-radius: 0; + border-left: none; + border-right: none; + border-bottom: none; + max-height: calc(100dvh - 42px); + width: 100vw; + } + + /* Fullscreen & Bottom-sheet style modals for mobile */ + .overlay { + padding: 4px; + } + + .banner-card { + max-width: 96vw; + width: 96vw; + padding: 14px 12px; + max-height: 94dvh; + } + + .start-card h1 { + font-size: 22px; + } + .start-card p { + font-size: 11px; + margin-bottom: 8px; + } + + .mode-selector { + gap: 4px; + margin-bottom: 8px; + } + .mode-btn { + font-size: 11px; + padding: 6px 8px; + } + + .start-hero-row { + flex-direction: column; + gap: 6px; + } + .btn-hero { + font-size: 14px; + padding: 10px 16px; + width: 100%; + } + + .start-activities-grid { + grid-template-columns: repeat(2, 1fr); + gap: 6px; + } + .btn-act { + font-size: 11px; + padding: 8px 6px; + } + + /* Modal Grids responsive */ + .shop-items { + grid-template-columns: 1fr; + gap: 8px; + } + + .almanac-layout { + flex-direction: column; + max-height: 80vh; + } + .almanac-grid { + grid-template-columns: repeat(4, 1fr) !important; + } + .almanac-details { + width: 100%; + } + + .workshop-tabs { + flex-wrap: wrap; + gap: 4px; + } + .workshop-tab { + font-size: 11px; + padding: 5px 8px; + flex: 1; + text-align: center; + } + + .campaign-levels-grid { + grid-template-columns: repeat(5, 1fr) !important; + gap: 4px !important; + } +} + +/* Landscape Mode for Smartphones (< 540px height) */ +@media screen and (orientation: landscape) and (max-height: 540px) { + body { + padding: 0; + } + + .top-nav-bar { + padding: 2px 6px; + padding-left: max(6px, env(safe-area-inset-left)); + padding-right: max(6px, env(safe-area-inset-right)); + } + + .nav-btn { + padding: 2px 5px; + font-size: 9px; + } + + #game-container { + height: calc(100dvh - 30px); + max-height: calc(100dvh - 30px); + } + + .banner-card { + max-height: 96dvh; + padding: 10px 14px; + } +} + diff --git a/game/index.html b/game/index.html index 80a3d1c..19cf15a 100644 --- a/game/index.html +++ b/game/index.html @@ -2,7 +2,7 @@ - + Plants vs. Zombies — Игра онлайн на ai.qpid.pro @@ -16,32 +16,54 @@
- +
+ + + +
diff --git a/game/js/game.js b/game/js/game.js index d92e0f8..590feab 100644 --- a/game/js/game.js +++ b/game/js/game.js @@ -579,6 +579,12 @@ window.GameEngine = class GameEngine { document.getElementById('victory-modal').classList.remove('hidden'); } + triggerHaptic(ms = 15) { + if (typeof navigator !== 'undefined' && navigator.vibrate) { + try { navigator.vibrate(ms); } catch (_) {} + } + } + getEventPos(e) { const rect = this.canvas.getBoundingClientRect(); let clientX, clientY; @@ -603,15 +609,17 @@ window.GameEngine = class GameEngine { checkTouchCollect(pos) { for (let i = this.economy.suns.length - 1; i >= 0; i--) { const s = this.economy.suns[i]; - if (!s.collecting && Math.hypot(pos.x - s.x, pos.y - s.y) <= s.radius + 24) { + if (!s.collecting && Math.hypot(pos.x - s.x, pos.y - s.y) <= s.radius + 28) { s.collecting = true; + this.triggerHaptic(12); if (window.PvZAudio) window.PvZAudio.playSun(); } } for (let i = this.economy.coins.length - 1; i >= 0; i--) { const c = this.economy.coins[i]; - if (!c.collecting && Math.hypot(pos.x - c.x, pos.y - c.y) <= c.radius + 24) { + if (!c.collecting && Math.hypot(pos.x - c.x, pos.y - c.y) <= c.radius + 28) { c.collecting = true; + this.triggerHaptic(15); if (window.PvZAudio) window.PvZAudio.playCoin(); } } @@ -632,28 +640,37 @@ window.GameEngine = class GameEngine { const pos = this.getEventPos(e); const econResult = this.economy.handleClick(pos.x, pos.y); - if (econResult.handled) return; + if (econResult.handled) { + this.triggerHaptic(15); + return; + } const cell = this.grid.getCellFromCoords(pos.x, pos.y); if (cell) { this.handleCellClick(cell.col, cell.row); + this.triggerHaptic(25); } }); - // Mobile touch + // Mobile touch with Drag & Drop and continuous swipe-collect this.canvas.addEventListener('touchstart', (e) => { if (!this.isRunning) return; - e.preventDefault(); const pos = this.getEventPos(e); this.checkTouchCollect(pos); const econResult = this.economy.handleClick(pos.x, pos.y); - if (econResult.handled) return; + if (econResult.handled) { + this.triggerHaptic(15); + this.dragCard = this.economy.selectedCard; + return; + } const cell = this.grid.getCellFromCoords(pos.x, pos.y); if (cell) { + this.hoverCell = cell; this.handleCellClick(cell.col, cell.row); + this.triggerHaptic(25); } }, { passive: false }); @@ -665,12 +682,66 @@ window.GameEngine = class GameEngine { this.hoverCell = this.grid.getCellFromCoords(pos.x, pos.y); }, { passive: false }); - this.canvas.addEventListener('touchend', () => { + this.canvas.addEventListener('touchend', (e) => { + if (!this.isRunning) return; + const pos = this.getEventPos(e); + if (this.dragCard) { + const cell = this.grid.getCellFromCoords(pos.x, pos.y); + if (cell && this.economy.selectedCard === this.dragCard) { + this.handleCellClick(cell.col, cell.row); + this.triggerHaptic(30); + } + this.dragCard = null; + } this.hoverCell = null; }); } bindUI() { + // Mobile Drawer Handlers + const mobileDrawer = document.getElementById('mobile-drawer'); + const drawerBackdrop = document.getElementById('drawer-backdrop'); + const btnDrawerOpen = document.getElementById('btn-mobile-drawer'); + const btnDrawerClose = document.getElementById('btn-drawer-close'); + + const openDrawer = () => { + if (mobileDrawer) mobileDrawer.classList.remove('hidden'); + if (drawerBackdrop) drawerBackdrop.classList.remove('hidden'); + this.triggerHaptic(10); + }; + + const closeDrawer = () => { + if (mobileDrawer) mobileDrawer.classList.add('hidden'); + if (drawerBackdrop) drawerBackdrop.classList.add('hidden'); + }; + + if (btnDrawerOpen) btnDrawerOpen.addEventListener('click', openDrawer); + if (btnDrawerClose) btnDrawerClose.addEventListener('click', closeDrawer); + if (drawerBackdrop) drawerBackdrop.addEventListener('click', closeDrawer); + + // Mobile Drawer Items delegation to corresponding modal open triggers + const drawerActions = [ + { id: 'btn-drawer-campaign', target: 'btn-campaign-open' }, + { id: 'btn-drawer-workshop', target: 'btn-workshop' }, + { id: 'btn-drawer-zengarden', target: 'btn-zengarden' }, + { id: 'btn-drawer-almanac', target: 'btn-almanac' }, + { id: 'btn-drawer-achievements', target: 'btn-achievements' }, + { id: 'btn-drawer-tree', target: 'btn-tree' }, + { id: 'btn-drawer-leaderboard', target: 'btn-leaderboard' }, + { id: 'btn-drawer-bugreport', target: 'btn-bugreport' } + ]; + + drawerActions.forEach(action => { + const btn = document.getElementById(action.id); + if (btn) { + btn.addEventListener('click', () => { + closeDrawer(); + const targetBtn = document.getElementById(action.target); + if (targetBtn) targetBtn.click(); + }); + } + }); + // Menu Button (Top Nav) const btnMenu = document.getElementById('btn-menu'); if (btnMenu) { diff --git a/scripts/test_game.py b/scripts/test_game.py index 86b5c08..6ef5420 100644 --- a/scripts/test_game.py +++ b/scripts/test_game.py @@ -611,13 +611,24 @@ def test_feedback_and_layout_system(): bug_reports_md_path = os.path.join(BASE_DIR, "BUG_REPORTS.md") feedback_srv_path = os.path.join(BASE_DIR, "scripts/feedback_server.py") - # 1. UI Layout rules + # 1. UI Layout & Mobile Responsive rules + html_path = os.path.join(GAME_DIR, "index.html") + with open(html_path, "r", encoding="utf-8") as f: + html_code = f.read() + layout_checks = [ - ("Top nav bar flex-wrap enabled for zero overflow", "flex-wrap: wrap" in css_code), + ("Top nav bar flex structure present", "top-nav-bar" in css_code), ("Responsive start-card class implemented", ".start-card" in css_code and "max-height" in css_code), ("Bug report card styling present", ".bugreport-card" in css_code and ".bugreport-feed" in css_code), ("Bug status badges and cards styling present", ".bug-card" in css_code and ".bug-status" in css_code), ("Body has vertical scrolling enabled for smaller screens", "overflow-y: auto" in css_code), + ("Mobile Viewport-fit=cover meta tag present", "viewport-fit=cover" in html_code), + ("Mobile Drawer navigation markup present", "mobile-drawer" in html_code and "drawer-backdrop" in html_code), + ("Mobile Drawer CSS styling and transitions present", ".mobile-drawer" in css_code and ".drawer-backdrop" in css_code), + ("Responsive Media Queries @media screen and (max-width: 768px)", "@media screen and (max-width: 768px)" in css_code), + ("Mobile Landscape optimization media query present", "orientation: landscape" in css_code), + ("GameEngine triggerHaptic Vibration API implemented", "triggerHaptic" in game_code), + ("Mobile Touch Drag & Drop card planting supported", "dragCard" in game_code), ] for name, cond in layout_checks: record(name, "PASS" if cond else "FAIL")