xxxxxxxxxx
494
let player; //player variable
let obstacles = []; //list of obstacles
const OBSTACLE_THICKNESS = 18; //thickness of each rectangle
let rectImg, startImg; //maze pattern and start screen
let obstaclesG; // pre rendered obstacle course pattern for performance
let gameStarted = false; //game started flag
let gameEnded = false; //game ended flag
let startTime = 0; //start time
let elapsedTime = 0; //time passed since start of level
let lowestTime = Infinity; //infinity so the first level completion leads to the new lowest time
let lives = 8; // player starts with 3 lives
let collisionCooldown = false; // Tracks if cooldown is active
let cooldownDuration = 1000; // Cooldown duration in milliseconds
let lastCollisionTime = 0; // Timestamp of the last collision
let heartImg;//live hearts img
let bgMusic;
let lifeLostSound;
let winSound;
let serial; //for arduino connection
let joystickX = 500; // default joystick X position
let joystickY = 500; // default joystick Y position
let powerUps = []; // Array to store power-ups
let powerUpSpawnInterval = 10000; // interval to spawn a new
let lastPowerUpTime = 0; // time when the last power-up was spawned
let speedUpImg, slowDownImg, loseLifeImg;
let buttonPressed = false;
function preload() {
rectImg = loadImage('pattern.png'); // Load obstacle pattern
startImg = loadImage('start.png'); // Load start screen image
heartImg = loadImage('heart.png');// load heart image
bgMusic = loadSound('background_music.mp3'); // background music
lifeLostSound = loadSound('life_lost.wav'); // Sound for losing a life
winSound = loadSound('win_sound.wav'); //sound for winning
speedUpImg = loadImage('speed_up.png'); //icons for powerups
slowDownImg = loadImage('slow_down.png');
loseLifeImg = loadImage('lose_life.png');
}
function setup() {
createCanvas(1450, 900);
serial = new p5.SerialPort(); // Initialize SerialPort
serial.open('/dev/tty.usbmodem1101'); //the code for the arduino device being opened
serial.on('data', handleSerialData);
player = new Player(30, 220, 15, 5); //maze starting coordinate for player
//maze background
obstaclesG = createGraphics(1450, 900);
obstaclesG.background(220);
// Add obstacles
addObstacles(); //adds all the obstacles during setup
// loops through the list and displays each one
for (let obs of obstacles) {
obs.showOnGraphics(obstaclesG);
}
bgMusic.loop() //background music starts
}
function spawnPowerUp() {
let x, y;
let validPosition = false;
while (!validPosition) {
x = random(50, width - 50);
y = random(50, height - 50);
//a valid position for a powerup is such that it does not collide with any obstacles
validPosition = !obstacles.some(obs =>
collideRectCircle(obs.x, obs.y, obs.w, obs.h, x, y, 30)
) && !powerUps.some(pu => dist(pu.x, pu.y, x, y) < 60);
}
const types = ["speedUp", "slowDown", "loseLife"];
const type = random(types); //one random type of powerup
powerUps.push(new PowerUp(x, y, type)); //adds to powerup array
}
function handlePowerUps() {
// Spawn a new power-up if the interval has passed
if (millis() - lastPowerUpTime > powerUpSpawnInterval) {
spawnPowerUp();
lastPowerUpTime = millis(); // reset the spawn timer
}
// Display and check for player interaction with power-ups
for (let i = powerUps.length - 1; i >= 0; i--) {
const powerUp = powerUps[i];
powerUp.display();
if (powerUp.collidesWith(player)) {
powerUp.applyEffect(); // Apply the effect of the power-up
powerUps.splice(i, 1); // Remove the collected power-up
}
}
}
function draw() {
if (!gameStarted) {
background(220);
image(startImg, 0, 0, width, height);
noFill();
stroke(0);
// Start the game with joystick button or mouse click
if (buttonPressed || (mouseIsPressed && mouseX > 525 && mouseX < 915 && mouseY > 250 && mouseY < 480)) {
gameStarted = true;
startTime = millis();
}
} else if (!gameEnded) {
background(220);
image(obstaclesG, 0, 0);
player.update(obstacles); // Update player position
handlePowerUps(); // Manage power-ups
player.show(); // Display the player
// Update and display elapsed time, hearts, etc.
elapsedTime = millis() - startTime;
serial.write(`L${lives}\n`);
displayHearts();
fill(0);
textSize(22);
textAlign(LEFT);
text(`Time: ${(elapsedTime / 1000).toFixed(2)} seconds`, 350, 50);
textAlign(RIGHT);
text(
`Lowest Time: ${lowestTime < Infinity ? (lowestTime / 1000).toFixed(2) : "N/A"}`,
width - 205,
50
);
if (dist(player.x, player.y, 1440, 674) < player.r) {
endGame(); // Check if the player reaches the goal
}
} else if (gameEnded) {
// Restart the game with joystick button or mouse click
if (buttonPressed || mouseIsPressed) {
restartGame();
}
}
}
function handleSerialData() {
let data = serial.readLine().trim(); // Read and trim incoming data
if (data.length > 0) {
let values = data.split(","); // Split data by comma
if (values.length === 3) {
joystickX = Number(values[0]); // Update joystick X
joystickY = Number(values[1]); // Update joystick Y
buttonPressed = Number(values[2]) === 0; // Update button state (0 = pressed)
}
}
}
function displayHearts() { //display lives
const heartSize = 40; // size of each heart
const startX = 550; // x position for hearts
const startY = 40; // y position for hearts
for (let i = 0; i < lives; i++) { //only displays as many hearts as there are lives left
image(heartImg, startX + i * (heartSize + 10), startY, heartSize, heartSize);
}
}
function endGame() {
gameEnded = true;
noLoop(); // stop the draw loop
winSound.play(); //if game ends
serial.write("END\n");
// check if the current elapsed time is a new record
const isNewRecord = elapsedTime < lowestTime;
if (isNewRecord) {
lowestTime = elapsedTime; // update lowest time
}
// Display end screen
background(220);
fill(0);
textSize(36);
textAlign(CENTER, CENTER);
text("Congratulations! You reached the goal!", width / 2, height / 2 - 100);
textSize(24);
text(`Time: ${(elapsedTime / 1000).toFixed(2)} seconds`, width / 2, height / 2 - 50);
// Display "New Record!" message if applicable
if (isNewRecord) {
textSize(28);
fill(255, 0, 0); // Red color for emphasis
text("New Record!", width / 2, height / 2 - 150);
}
textSize(24);
fill(0); // Reset text color
text("Click anywhere to restart", width / 2, height / 2 + 50);
}
function mouseClicked() {
if (!gameStarted) {
// start the game if clicked in start button area
if (mouseX > 525 && mouseX < 915 && mouseY > 250 && mouseY < 480) {
gameStarted = true;
startTime = millis();
}
} else if (gameEnded) {
// Restart game
restartGame();
}
}
function checkJoystickClick() {
if (buttonPressed) {
if (!gameStarted) {
gameStarted = true;
startTime = millis();
} else if (gameEnded) {
restartGame();
}
}
}
function restartGame() {
gameStarted = true;
gameEnded = false;
lives = 8;
powerUps = []; // Clear all power-ups
player = new Player(30, 220, 15, 5); // Reset player position
startTime = millis(); // Reset start time
loop();
bgMusic.loop(); // Restart background music
}
function loseGame() {
gameEnded = true; // End the game
noLoop(); // Stop the draw loop
bgMusic.stop();
serial.write("END\n");
// Display level lost message
background(220);
fill(0);
textSize(36);
textAlign(CENTER, CENTER);
text("Level Lost!", width / 2, height / 2 - 100);
textSize(24);
text("You ran out of lives!", width / 2, height / 2 - 50);
text("Click anywhere to restart", width / 2, height / 2 + 50);
}
function keyPressed() { //key controls
let k = key.toLowerCase();
if (k === 'w') player.moveUp(true);
if (k === 'a') player.moveLeft(true);
if (k === 's') player.moveDown(true);
if (k === 'd') player.moveRight(true);
if (k === 'f') fullscreen(!fullscreen());
}
function keyReleased() { //to stop movement once key is released
let k = key.toLowerCase();
if (k === 'w') player.moveUp(false);
if (k === 'a') player.moveLeft(false);
if (k === 's') player.moveDown(false);
if (k === 'd') player.moveRight(false);
}
class Player { //player class
constructor(x, y, r, speed) {
this.x = x;
this.y = y;
this.r = r;
this.speed = speed;
this.movingUp = false;
this.movingDown = false;
this.movingLeft = false;
this.movingRight = false;
}
update(obsArray) {
let oldX = this.x;
let oldY = this.y;
// Calculate joystick displacement from the center
let xDisplacement = joystickX - 500;
let yDisplacement = joystickY - 500;
// Calculate the magnitude of the displacement (distance from center)
let displacementMagnitude = sqrt(xDisplacement ** 2 + yDisplacement ** 2);
// Map the displacement magnitude to a speed range 1 being lowest and 10 being max speed
let dynamicSpeed = map(displacementMagnitude, 0, 500, 1, 7); // Adjust max values as needed
dynamicSpeed = constrain(dynamicSpeed, 1, 7); // Ensure speed stays within bounds
// Normalize joystick direction (to maintain proportional movement)
let angle = atan2(yDisplacement, xDisplacement);
let moveX = cos(angle) * dynamicSpeed;
let moveY = sin(angle) * dynamicSpeed;
// Apply movement based on joystick displacement
if (abs(xDisplacement) > 50) this.x += moveX; // Threshold to prevent jitter near the center
if (abs(yDisplacement) > 50) this.y += moveY;
// Constrain to canvas
this.x = constrain(this.x, this.r, width - this.r);
this.y = constrain(this.y, this.r, height - this.r);
// Restrict movement if colliding with obstacles
if (this.collidesWithObstacles(obsArray)) {
this.x = oldX;
this.y = oldY;
// Handle life deduction only if not in cooldown to prevent all lives being lost in quick succession
if (!collisionCooldown) {
lives--;
lastCollisionTime = millis(); // Record the time of this collision
collisionCooldown = true; // Activate cooldown
lifeLostSound.play(); // Play life lost sound
if (lives <= 0) {
loseGame(); // Call loseGame function if lives reach 0
}
}
}
// Check if cooldown period has elapsed
if (collisionCooldown && millis() - lastCollisionTime > cooldownDuration) {
collisionCooldown = false; // Reset cooldown
}
}
show() { //display function
fill(0);
ellipse(this.x, this.y, this.r * 2);
}
collidesWithObstacles(obsArray) { //checks collisions in a loop
for (let obs of obsArray) {
if (this.collidesWithRect(obs.x, obs.y, obs.w, obs.h)) return true;
}
return false;
}
collidesWithRect(rx, ry, rw, rh) { //collision detection function checks if distance between player and wall is less than player radius which means a collision occurred
let closestX = constrain(this.x, rx, rx + rw);
let closestY = constrain(this.y, ry, ry + rh);
let distX = this.x - closestX;
let distY = this.y - closestY;
return sqrt(distX ** 2 + distY ** 2) < this.r;
}
moveUp(state) {
this.movingUp = state;
}
moveDown(state) {
this.movingDown = state;
}
moveLeft(state) {
this.movingLeft = state;
}
moveRight(state) {
this.movingRight = state;
}
}
class Obstacle { //obstacle class
constructor(x, y, length, horizontal) {
this.x = x;
this.y = y;
this.w = horizontal ? length : OBSTACLE_THICKNESS;
this.h = horizontal ? OBSTACLE_THICKNESS : length;
}
showOnGraphics(pg) { //to show the obstacle pattern image repeatedly
for (let xPos = this.x; xPos < this.x + this.w; xPos += rectImg.width) {
for (let yPos = this.y; yPos < this.y + this.h; yPos += rectImg.height) {
pg.image(
rectImg,
xPos,
yPos,
min(rectImg.width, this.x + this.w - xPos),
min(rectImg.height, this.y + this.h - yPos)
);
}
}
}
}
class PowerUp {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type; // Type of power-up: 'speedUp', 'slowDown', 'loseLife'
this.size = 30; // Size of the power-up image
}
display() {
let imgToDisplay;
if (this.type === "speedUp") imgToDisplay = speedUpImg;
else if (this.type === "slowDown") imgToDisplay = slowDownImg;
else if (this.type === "loseLife") imgToDisplay = loseLifeImg;
image(imgToDisplay, this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
}
collidesWith(player) {
return dist(this.x, this.y, player.x, player.y) < player.r + this.size / 2;
}
applyEffect() {
if (this.type === "speedUp") player.speed += 2;
else if (this.type === "slowDown") player.speed = max(player.speed - 1, 2);
else if (this.type === "loseLife") {
lives--;
lifeLostSound.play();
if (lives <= 0) loseGame();
}
}
}
function addObstacles() {
// adding all obstacles so the collision can check all in an array
obstacles.push(new Obstacle(0, 0, 1500, true));
obstacles.push(new Obstacle(0, 0, 200, false));
obstacles.push(new Obstacle(0, 250, 600, false));
obstacles.push(new Obstacle(1432, 0, 660, false));
obstacles.push(new Obstacle(1432, 700, 200, false));
obstacles.push(new Obstacle(0, 882, 1500, true));
obstacles.push(new Obstacle(100, 0, 280, false));
obstacles.push(new Obstacle(0, 400, 200, true));
obstacles.push(new Obstacle(200, 90, 328, false));
obstacles.push(new Obstacle(300, 0, 500, false));
obstacles.push(new Obstacle(120, 500, 198, true));
obstacles.push(new Obstacle(0, 590, 220, true));
obstacles.push(new Obstacle(300, 595, 350, false));
obstacles.push(new Obstacle(100, 680, 200, true));
obstacles.push(new Obstacle(0, 770, 220, true));
obstacles.push(new Obstacle(318, 400, 250, true));
obstacles.push(new Obstacle(300, 592, 250, true));
obstacles.push(new Obstacle(420, 510, 85, false));
obstacles.push(new Obstacle(567, 400, 100, false));
obstacles.push(new Obstacle(420, 680, 100, false));
obstacles.push(new Obstacle(567, 750, 150, false));
obstacles.push(new Obstacle(420, 680, 400, true));
obstacles.push(new Obstacle(410, 90, 200, false));
obstacles.push(new Obstacle(410, 90, 110, true));
obstacles.push(new Obstacle(520, 90, 120, false));
obstacles.push(new Obstacle(410, 290, 350, true));
obstacles.push(new Obstacle(660, 90, 710, false));
obstacles.push(new Obstacle(660, 90, 100, true));
obstacles.push(new Obstacle(420, 680, 500, true));
obstacles.push(new Obstacle(410, 290, 315, true));
obstacles.push(new Obstacle(830, 0, 290, false));
obstacles.push(new Obstacle(760, 200, 70, true));
obstacles.push(new Obstacle(742, 200, 90, false));
obstacles.push(new Obstacle(950, 120, 480, false));
obstacles.push(new Obstacle(1050, 0, 200, false));
obstacles.push(new Obstacle(1150, 120, 200, false));
obstacles.push(new Obstacle(1250, 0, 200, false));
obstacles.push(new Obstacle(1350, 120, 200, false));
obstacles.push(new Obstacle(1058, 310, 310, true));
obstacles.push(new Obstacle(760, 390, 300, true));
obstacles.push(new Obstacle(660, 490, 200, true));
obstacles.push(new Obstacle(760, 582, 200, true));
obstacles.push(new Obstacle(920, 680, 130, false));
obstacles.push(new Obstacle(1040, 310, 650, false));
obstacles.push(new Obstacle(790, 760, 200, false));
obstacles.push(new Obstacle(1150, 400, 400, false));
obstacles.push(new Obstacle(1160, 560, 300, true));
obstacles.push(new Obstacle(1325, 440, 200, false));
obstacles.push(new Obstacle(1240, 325, 150, false));
obstacles.push(new Obstacle(1150, 800, 200, true));
obstacles.push(new Obstacle(1432, 850, 130, false));
obstacles.push(new Obstacle(1240, 720, 200, true));
}