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:
1 parent
6457a8904e
commit
82654d8f64
4 files changed
+433
-22
No files matched your search
+78
-7
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user