feat(mobile): Add responsive layout, mobile Drawer menu, and touch gestures

- Add viewport-fit=cover & mobile slide-out Drawer navigation menu
- Add @media screen rules (<=768px and landscape <=540px)
- Add Touch Drag & Drop planting and continuous swipe-to-collect
- Add Vibration API haptic feedback for planting, collecting, and explosions
- Add automated responsive test assertions in test_game.py (242/242 PASS)
This commit is contained in:
2026-09-17 10:07:17 +00:00
parent 6457a8904e
commit 82654d8f64
4 changed files with 433 additions and 22 deletions
+78 -7
View File
@@ -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) {