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")