const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const arenaSize = 500; const subSquareSize = arenaSize / 3; const clockRadius = subSquareSize / 9; const playerSize = subSquareSize / 10; const handLength = clockRadius; const triangleOpacity = 0.5; let rotationStep = 0; const maxRotationSteps = 6; let rotationStarted = false; let rotationComplete = false; let randomLayout = Math.floor(Math.random() * 5); let lastRotationTime = performance.now(); let gameStopped = false; let player = null; let clocks = null; let deathImage = new Image(); // Set the source of the image deathImage.src = "../images/vuln-stack.png"; function rotateClocks(elapsedTime) { if (rotationStep < maxRotationSteps && elapsedTime >= 1000) { clocks.forEach(clock => clock.rotateHand()); rotationStep++; lastRotationTime = performance.now(); } else if (rotationStep === maxRotationSteps) { rotationComplete = true; } } function resetRotation() { rotationStep = 0; rotationComplete = false; } class Clock { constructor(x, y, handDirection) { this.x = x; this.y = y; this.handDirection = handDirection; } drawArrow() { ctx.lineWidth = 1; ctx.strokeStyle = "black"; // Draw the line ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo( this.x + handLength * Math.cos(this.handDirection * Math.PI / 180), this.y + handLength * Math.sin(this.handDirection * Math.PI / 180) ); ctx.stroke(); // Draw the arrowhead var arrowSize = 7; // Change the value to adjust the size of the arrowhead var arrowAngle = Math.PI / 9; // Change the value to adjust the angle of the arrowhead var angle = Math.atan2(handLength * Math.sin(this.handDirection * Math.PI / 180), handLength * Math.cos(this.handDirection * Math.PI / 180)); ctx.beginPath(); ctx.moveTo( this.x + handLength * Math.cos(this.handDirection * Math.PI / 180), this.y + handLength * Math.sin(this.handDirection * Math.PI / 180) ); ctx.lineTo( this.x + (handLength - arrowSize) * Math.cos(angle - arrowAngle), this.y + (handLength - arrowSize) * Math.sin(angle - arrowAngle) ); ctx.lineTo( this.x + (handLength - arrowSize) * Math.cos(angle + arrowAngle), this.y + (handLength - arrowSize) * Math.sin(angle + arrowAngle) ); ctx.closePath(); ctx.fillStyle = "red"; // Set the fill color to red ctx.fill(); } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, clockRadius, 0, 2 * Math.PI); ctx.fillStyle = "gold"; ctx.fill(); ctx.strokeStyle = "orange"; ctx.lineWidth = 3; ctx.stroke(); this.drawArrow(); ctx.stroke(); } rotateHand() { this.handDirection = (this.handDirection + 90) % 360; } drawConal() { const angle = 45; const sideLength = 50; ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo( this.x + sideLength * handLength * Math.cos((this.handDirection - angle) * Math.PI / 180), this.y + sideLength * handLength * Math.sin((this.handDirection - angle) * Math.PI / 180) ); ctx.lineTo( this.x + sideLength * handLength * Math.cos((this.handDirection + angle) * Math.PI / 180), this.y + sideLength * handLength * Math.sin((this.handDirection + angle) * Math.PI / 180) ); ctx.closePath(); ctx.fillStyle = `rgba(255, 255, 0, ${triangleOpacity})`; ctx.fill(); // Draw red lines ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo( this.x + sideLength * handLength * Math.cos((this.handDirection - angle) * Math.PI / 180), this.y + sideLength * handLength * Math.sin((this.handDirection - angle) * Math.PI / 180) ); ctx.strokeStyle = 'red'; ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo( this.x + sideLength * handLength * Math.cos((this.handDirection + angle) * Math.PI / 180), this.y + sideLength * handLength * Math.sin((this.handDirection + angle) * Math.PI / 180) ); ctx.strokeStyle = 'red'; ctx.stroke(); ctx.strokeStyle = 'black'; } } const keysPressed = { ArrowUp: false, W: false, w: false, ArrowLeft: false, A: false, a: false, ArrowDown: false, S: false, s: false, ArrowRight: false, D: false, d: false }; class Player { constructor(x, y) { this.x = x; this.y = y; this.hasMoved = false; } draw() { ctx.fillStyle = 'blue'; ctx.fillRect(this.x - playerSize / 2, this.y - playerSize / 2, playerSize, playerSize); } move(dx, dy) { const newX = this.x + dx; const newY = this.y + dy; if (newX >= playerSize / 2 && newX <= arenaSize - playerSize / 2) { this.x = newX; this.hasMoved = true; } if (newY >= playerSize / 2 && newY <= arenaSize - playerSize / 2) { this.y = newY; this.hasMoved = true; } } } function getClockPositions(layout) { switch (layout) { case 0: return [ new Clock(subSquareSize, subSquareSize, 180), new Clock(arenaSize - subSquareSize, subSquareSize * 2, 0) ]; case 1: return [ new Clock(subSquareSize, subSquareSize, 180), new Clock(subSquareSize, subSquareSize * 2, 180), new Clock(arenaSize - subSquareSize, subSquareSize + (subSquareSize / 2), 0) ]; case 2: return [ new Clock(subSquareSize, subSquareSize, 270), new Clock(subSquareSize, subSquareSize * 2, 90), new Clock(arenaSize - subSquareSize, subSquareSize + (subSquareSize / 2), 180) ]; case 3: return [ new Clock(subSquareSize + (subSquareSize / 2), subSquareSize, 270), new Clock(subSquareSize, subSquareSize * 2, 90), new Clock(arenaSize - subSquareSize, subSquareSize + (subSquareSize / 2), 180) ]; case 4: return [ new Clock(subSquareSize + (subSquareSize / 2), subSquareSize, 0), new Clock(subSquareSize, subSquareSize * 2, 180), new Clock(arenaSize - subSquareSize, subSquareSize + (subSquareSize / 2), 90) ] } } function pointInTriangle(px, py, ax, ay, bx, by, cx, cy) { const v0x = cx - ax; const v0y = cy - ay; const v1x = bx - ax; const v1y = by - ay; const v2x = px - ax; const v2y = py - ay; const d00 = v0x * v0x + v0y * v0y; const d01 = v0x * v1x + v0y * v1y; const d11 = v1x * v1x + v1y * v1y; const d20 = v2x * v0x + v2y * v0y; const d21 = v2x * v1x + v2y * v1y; const denominator = d00 * d11 - d01 * d01; const a = (d11 * d20 - d01 * d21) / denominator; const b = (d00 * d21 - d01 * d20) / denominator; return a >= 0 && b >= 0 && a + b <= 1; } function checkCollisions() { const px = player.x; const py = player.y; let collision = false; for (const clock of clocks) { const ax = clock.x; const ay = clock.y; const angle = 45; const sideLength = 50; const bx = clock.x + sideLength * handLength * Math.cos((clock.handDirection - angle) * Math.PI / 180); const by = clock.y + sideLength * handLength * Math.sin((clock.handDirection - angle) * Math.PI / 180); const cx = clock.x + sideLength * handLength * Math.cos((clock.handDirection + angle) * Math.PI / 180); const cy = clock.y + sideLength * handLength * Math.sin((clock.handDirection + angle) * Math.PI / 180); if (pointInTriangle(px, py, ax, ay, bx, by, cx, cy)) { collision = true; break; } } return collision; } function displaySafeStatus(isSafe) { if (isSafe) succeeded(); else failed(); } function succeeded() { drawText("SUCCESS!", 'green'); } function failed() { ctx.drawImage(deathImage, player.x - playerSize, player.y - playerSize, playerSize * 2, playerSize * 2); drawText("FAILED!", 'red'); } function drawText(text, colour) { ctx.font = '35px Verdana'; // Set the font size and type ctx.fillStyle = colour; // Set the fill color ctx.textAlign = 'center'; // Set the horizontal alignment ctx.textBaseline = 'middle'; // Set the vertical alignment const x = canvas.width / 2; // Set the x-coordinate of the text const y = 80; // Set the y-coordinate of the text ctx.fillText(text, x, y); // Draw the text using fillText() } function gameLoop(currentTime) { if (gameStopped) { requestAnimationFrame(gameLoop); return; } ctx.fillStyle = "gray"; ctx.fillRect(0, 0, canvas.width, canvas.height); if (rotationStarted === false && player.hasMoved) { rotationStarted = true; } const elapsedTime = currentTime - lastRotationTime; if (rotationStarted && !rotationComplete) { rotateClocks(elapsedTime); } const moveAmount = 4; if (movingUp()) player.move(0, -moveAmount); if (movingDown()) player.move(0, moveAmount); if (movingLeft()) player.move(-moveAmount, 0); if (movingRight()) player.move(moveAmount, 0); player.draw(); clocks.forEach(clock => { clock.draw(); if (rotationComplete) { clock.drawConal(); } }); if (rotationComplete) { const isSafe = !checkCollisions(); displaySafeStatus(isSafe); stopGame(); } requestAnimationFrame(gameLoop); } function movingUp() { return keysPressed.ArrowUp || keysPressed.W || keysPressed.w; } function movingDown() { return keysPressed.ArrowDown || keysPressed.S || keysPressed.s; } function movingLeft() { return keysPressed.ArrowLeft || keysPressed.A || keysPressed.a; } function movingRight() { return keysPressed.ArrowRight || keysPressed.D || keysPressed.d; } function startGame() { gameStopped = false; rotationStarted = false; rotationComplete = false; player = new Player(arenaSize / 2, arenaSize / 2); clocks = getClockPositions(randomLayout); } function stopGame() { gameStopped = true; } function generateNewLayout() { let newLayout; do { newLayout = Math.floor(Math.random() * 5); } while (newLayout === randomLayout); randomLayout = newLayout; } function keydownHandler(event) { if (keysPressed.hasOwnProperty(event.key)) { keysPressed[event.key] = true; } } function keyupHandler(event) { if (keysPressed.hasOwnProperty(event.key)) { keysPressed[event.key] = false; } } document.addEventListener('keydown', keydownHandler); document.addEventListener('keyup', keyupHandler); document.getElementById("resetButton").addEventListener("click", function() { stopGame(); resetRotation(); generateNewLayout(); startGame(); }); document.getElementById("retryButton").addEventListener("click", function() { stopGame(); resetRotation(); startGame(); }); startGame(); gameLoop();