Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 209 additions & 4 deletions game.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ function drawGrid(context) {
const DEFAULT_RAMP_ANGLE = normalizeAngle(-17 * Math.PI / 180); // -17 degrees, normalized to [0, 2π)
const ROTATION_INCREMENT = Math.PI / 12; // 15 degrees per key press

// Mobile detection
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth < 768;
};

// Game variables
let engine;
let render;
Expand All @@ -113,6 +119,15 @@ let placedConstraints = [];
let currentRampAngle = DEFAULT_RAMP_ANGLE;
let seesawIdCounter = 0; // Counter for unique seesaw IDs

// Mobile touch variables
let touchStartTime = 0;
let touchStartPos = { x: 0, y: 0 };
let longPressTimer = null;
const LONG_PRESS_DURATION = 500; // ms for long press to delete
let lastTouchDistance = 0; // For pinch-to-zoom detection
let initialPinchAngle = 0; // For two-finger rotation
let lastTouchAngle = 0;

// Undo/Redo system
let actionHistory = [];
let historyIndex = -1;
Expand Down Expand Up @@ -295,6 +310,50 @@ function loadSoundPreference() {
updateSoundButton();
}

/**
* Apply mobile-specific UI adjustments
*/
function applyMobileUI() {
// Update instructions text for mobile
const instructions = document.querySelector('.instructions');
if (instructions) {
const mobileInstructions = `
<h3>Mobile Instructions:</h3>
<ol>
<li>Tap on an object type to select it</li>
<li>Tap on the canvas to place objects</li>
<li>Long-press (0.5s) on objects to delete them</li>
<li>For ramps: use two fingers to rotate the angle</li>
<li>Use Undo and Redo buttons to reverse actions</li>
<li>Build a contraption that will hit the NPC (red figure on the right)</li>
<li>Tap "Run Machine" to start the physics simulation</li>
<li>Use Pause or Slow-Mo to control playback</li>
<li>Watch your machine doom the NPC! 💀</li>
</ol>
`;
instructions.innerHTML = mobileInstructions;
}

// Add mobile class to body for CSS targeting
document.body.classList.add('mobile-device');

// Setup collapsible control groups
const controlGroups = document.querySelectorAll('.control-group');
controlGroups.forEach((group, index) => {
const heading = group.querySelector('h3');
if (heading) {
// Start with Actions and Status expanded, others collapsed
if (index !== 1 && index !== 3) { // Not Actions or Status
group.classList.add('collapsed');
}

heading.addEventListener('click', () => {
group.classList.toggle('collapsed');
});
}
});
}

// Initialize the game
function init() {
canvas = document.getElementById('gameCanvas');
Expand All @@ -304,9 +363,15 @@ function init() {
world = engine.world;
world.gravity.y = 1;

// Increase iterations to prevent fast objects from tunneling through static bodies
engine.positionIterations = POSITION_ITERATIONS;
engine.velocityIterations = VELOCITY_ITERATIONS;
// Adjust physics iterations based on device capabilities
if (isMobile()) {
// Reduce iterations on mobile for better performance
engine.positionIterations = 8;
engine.velocityIterations = 4;
} else {
engine.positionIterations = POSITION_ITERATIONS;
engine.velocityIterations = VELOCITY_ITERATIONS;
}

// Create renderer
render = Render.create({
Expand All @@ -316,10 +381,17 @@ function init() {
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
wireframes: false,
background: '#87CEEB'
background: '#87CEEB',
// Optimize rendering for mobile
pixelRatio: isMobile() ? 1 : window.devicePixelRatio || 1
}
});

// Apply mobile-specific UI adjustments
if (isMobile()) {
applyMobileUI();
}

// Create ground
const ground = Bodies.rectangle(CANVAS_WIDTH / 2, CANVAS_HEIGHT - GROUND_HEIGHT / 2, CANVAS_WIDTH, GROUND_HEIGHT, {
isStatic: true,
Expand Down Expand Up @@ -539,6 +611,139 @@ function setupMouseControl() {

deleteObjectAtPosition(x, y);
});

// Touch controls for mobile
setupTouchControls();
}

function setupTouchControls() {
// Helper function to calculate distance between two touches
function getTouchDistance(touch1, touch2) {
const dx = touch2.clientX - touch1.clientX;
const dy = touch2.clientY - touch1.clientY;
return Math.sqrt(dx * dx + dy * dy);
}

// Helper function to calculate angle between two touches
function getTouchAngle(touch1, touch2) {
return Math.atan2(touch2.clientY - touch1.clientY, touch2.clientX - touch1.clientX);
}

// Prevent default touch behaviors to avoid zooming/scrolling
canvas.addEventListener('touchstart', (event) => {
event.preventDefault();

if (event.touches.length === 1) {
// Single touch - placement or long press to delete
const touch = event.touches[0];
touchStartTime = Date.now();

const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
touchStartPos.x = (touch.clientX - rect.left) * scaleX;
touchStartPos.y = (touch.clientY - rect.top) * scaleY;

// Start long press timer for delete
if (!isRunning) {
longPressTimer = setTimeout(() => {
// Long press detected - delete object
deleteObjectAtPosition(touchStartPos.x, touchStartPos.y);
longPressTimer = null;

// Visual feedback with vibration if supported
if (navigator.vibrate) {
navigator.vibrate(50);
}
}, LONG_PRESS_DURATION);
}
} else if (event.touches.length === 2) {
// Two-finger touch - rotation for ramp
if (!isRunning && selectedTool === 'ramp') {
lastTouchDistance = getTouchDistance(event.touches[0], event.touches[1]);
lastTouchAngle = getTouchAngle(event.touches[0], event.touches[1]);
initialPinchAngle = lastTouchAngle;

// Cancel long press timer if active
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
}
}
}, { passive: false });

canvas.addEventListener('touchmove', (event) => {
event.preventDefault();

// Cancel long press timer if finger moves
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}

// Two-finger rotation for ramp
if (event.touches.length === 2 && !isRunning && selectedTool === 'ramp') {
const currentAngle = getTouchAngle(event.touches[0], event.touches[1]);
const angleDelta = currentAngle - lastTouchAngle;

// Update ramp angle
rotateRamp(angleDelta);
lastTouchAngle = currentAngle;

// Provide haptic feedback for rotation
if (navigator.vibrate) {
navigator.vibrate(5);
}
}
}, { passive: false });

canvas.addEventListener('touchend', (event) => {
event.preventDefault();

// Clear long press timer if still active
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}

const touchDuration = Date.now() - touchStartTime;

// Short tap - place object (if not a long press and single touch)
if (event.touches.length === 0 && touchDuration < LONG_PRESS_DURATION && !isRunning && selectedTool) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
const scaleY = canvas.height / rect.height;
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

let x = touchStartPos.x;
let y = touchStartPos.y;

// Apply snap-to-grid if enabled
({ x, y } = snapToGrid(x, y));

placeObject(selectedTool, x, y);
}

// Reset touch tracking variables when all touches end
if (event.touches.length === 0) {
lastTouchDistance = 0;
lastTouchAngle = 0;
}
}, { passive: false });

canvas.addEventListener('touchcancel', (event) => {
event.preventDefault();

// Clear long press timer
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}

// Reset touch tracking variables
lastTouchDistance = 0;
lastTouchAngle = 0;
}, { passive: false });
}

function setupEventListeners() {
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Doomberg Machine - Rube Goldberg Death Machine</title>
<link rel="stylesheet" href="style.css">
</head>
Expand Down
Loading