var floorPos_y; var cameraPosX; var processStop_timer; var fps_recent_values = []; var showDebugData = false; var gameChar; var gravity; var game_score; var finish_position_x; var trees_x = []; var clouds = []; var mountains = []; var collectables = []; var canyons = []; var flagpole; var text_size; // Variables to set colors. Set in setup() var palette; function setup() { createCanvas(1024, 576); palette = { body_color: color("white"), head_color: color("darkgreen"), sky_color: color("#8E9887"), ground_color0: color("#684622"), ground_color1: color("#734E26"), ground_color2: color("#7F562A"), canyon_river_color: color("#56C525"), canyon_river_wave_color: color("#4BAD21"), canyon_color: color("#7B672A"), pine_leaves_color: color("#3F4834"), maple_leaves_color: color("#4E3D1F"), pine_stem: color("#644D0D"), maple_stem: color("#936907"), mountain: color("#5a5a5a"), mountain_shadow: color("#3c3c3c"), cloud0: color(200), cloud1: color(220), cloud2: color(255), coin_outer: color("#FDC334"), coin_middle: color("#ababab"), coin_inner: color("#FDC334"), heart_color: color("darkred"), enemy_head_color: color("red"), enemy_body_color: color("red"), }; startGame((full_start = true), (update_objects = true)); } function startGame(full_start, update_objects) { floorPos_y = 432; cameraPosX = 0; if (full_start) { gameChar = { x: width / 2, y: floorPos_y + (height - floorPos_y) / 6, x_step: 5, y_step: 8, scale: 1, sprite: 1, speed: 5, baseLives: 3, curLives: 3, isRight: false, isLeft: false, isFalling: false, isJumping: false, isPlummeting: false, possibleGroundPosY: [ floorPos_y + (height - floorPos_y) / 6, floorPos_y + (3 * (height - floorPos_y)) / 6, floorPos_y + (5 * (height - floorPos_y)) / 6, ], curGroundPosYIndex: 0, draw: function () { push(); strokeWeight(1); stroke(1); this.x_step *= this.scale; this.y_step *= this.scale; function _drawBody(jumping = false) { fill(palette.body_color); if (jumping) { triangle( gameChar.x - gameChar.x_step * 2, gameChar.y - gameChar.y_step * 1.5, gameChar.x, gameChar.y - gameChar.y_step * 4, gameChar.x + gameChar.x_step * 2, gameChar.y - gameChar.y_step * 1.5 ); } else { triangle( gameChar.x - gameChar.x_step * 2, gameChar.y, gameChar.x, gameChar.y - gameChar.y_step * 4, gameChar.x + gameChar.x_step * 2, gameChar.y ); } } function _drawHead() { fill(palette.head_color); ellipse( gameChar.x, gameChar.y - gameChar.y_step * 5, gameChar.x_step * 6, gameChar.y_step * 4 ); } function _drawEyes(draw_left, draw_right, eyes_height) { fill(0); if (draw_left) { ellipse( gameChar.x - gameChar.x_step * 1.2, gameChar.y - gameChar.y_step * eyes_height, gameChar.x_step / 1.5, gameChar.y_step / 2 ); } if (draw_right) { ellipse( gameChar.x + gameChar.x_step * 1.2, gameChar.y - gameChar.y_step * eyes_height, gameChar.x_step / 1.5, gameChar.y_step / 2 ); } } // Standing, facing frontwards if (this.sprite == 1) { // Body _drawBody(); // Head _drawHead(); // Eyes _drawEyes(true, true, 4.2); } // Jumping, facing forwards else if (gameChar.sprite == 2) { // Body _drawBody(true); // Hands. (Hands!) line( this.x - this.x_step * 1, this.y - this.y_step * 2.5, this.x - this.x_step * 3.2, this.y - this.y_step * 3.2 ); line( this.x + this.x_step * 1, this.y - this.y_step * 2.5, this.x + this.x_step * 3.2, this.y - this.y_step * 3.2 ); // Head _drawHead(); // Eyes _drawEyes(true, true, 3.8); } // Walking right else if (this.sprite == 3) { // Body _drawBody(); // Hand. (Hand!) line( this.x, this.y - this.y_step * 2.5, this.x + this.x_step * 0.8, this.y - this.y_step * 1 ); // Head _drawHead(); // Eyes _drawEyes(false, true, 4.2); } // Walking left else if (this.sprite == 4) { // Body _drawBody(); // Hand. (Hand!) line( this.x, this.y - this.y_step * 2.5, this.x - this.x_step * 0.8, this.y - this.y_step * 1 ); // Head _drawHead(); // Eyess _drawEyes(true, false, 4.2); } // Jumping left else if (this.sprite == 5) { // Body _drawBody(true); // Hands. (Hands!) line( this.x - this.x_step * 1, this.y - this.y_step * 2.5, this.x - this.x_step * 3.2, this.y - this.y_step * 3.2 ); line( this.x + this.x_step * 1, this.y - this.y_step * 2.5, this.x + this.x_step * 3.2, this.y - this.y_step * 3.2 ); // Head _drawHead(); // Eyes _drawEyes(true, false, 3.8); } // Jumping right else if (this.sprite == 6) { // Body _drawBody(true); // Hands. (Hands!) line( this.x - this.x_step * 1, this.y - this.y_step * 2.5, this.x - this.x_step * 3.2, this.y - this.y_step * 3.2 ); line( this.x + this.x_step * 1, this.y - this.y_step * 2.5, this.x + this.x_step * 3.2, this.y - this.y_step * 3.2 ); // Head _drawHead(); // Eyes _drawEyes(false, true, 3.8); } else { text("Bad sprite number!", 10, 10); console.error("Bad gameChar sprite number: " + this.sprite); } this.x_step /= this.scale; this.y_step /= this.scale; pop(); }, getCurGroundPosY: function () { return this.possibleGroundPosY[this.curGroundPosYIndex]; }, goUp: function () { memorizedIndex = this.curGroundPosYIndex; this.curGroundPosYIndex = max(0, this.curGroundPosYIndex - 1); if (memorizedIndex != this.curGroundPosYIndex) { gameChar.y -= (height - floorPos_y) / 3; gameChar.scale -= 0.1; } }, goDown: function () { memorizedIndex = this.curGroundPosYIndex; this.curGroundPosYIndex = min( this.possibleGroundPosY.length - 1, this.curGroundPosYIndex + 1 ); if (memorizedIndex != this.curGroundPosYIndex) { gameChar.y += (height - floorPos_y) / 3; gameChar.scale += 0.1; } }, }; } else { gameChar.x = width / 2; gameChar.y = floorPos_y + (height - floorPos_y) / 6; gameChar.scale = 1; gameChar.curGroundPosYIndex = 0; gameChar.isPlummeting = false; gameChar.isFalling = false; gameChar.isJumping = false; gameChar.isLeft = false; gameChar.isRight = false; } gravity = 1; game_score = 0; finish_position_x = 2000; if (update_objects || full_start) { // Assigning values of trees, clouds, mountains, canyons, collectables. for (i = 0; i < 150; i++) { trees_x[i] = random(-100, 10000); } for (i = 0; i < 100; i++) { clouds[i] = { x: random(-100, 10000), y: random(0, floorPos_y / 2), size: random(0.4, 2), }; } // Latter mountains. for (i = 0; i < finish_position_x / 90; i++) { mountains[i] = { x: random(-200, finish_position_x + 200), width: random(60, 150), height: random(floorPos_y - 50, floorPos_y - 150), skew: random(-20, 20), }; // To prevent spiky mountains which are quite ugly mountains[i].height = max(mountains[i].width, mountains[i].height); } // Start mountains. for (i = 0; i < 6; i++) { mountains[i] = { x: width / (i + 1.5), width: width / (i + random(2, 4)), height: height / (2 + 0.8 * i), skew: (random(-10, 20) * i) / 2, }; } for (i = 0; i < 100; i++) { if (i == 0) { canyons[0] = { x: 700, width: 50, points: [], }; } else { if (canyons[i - 1].x + 600 > finish_position_x) break; canyons[i] = { x: canyons[i - 1].x + 300 + 200 * random(0.5, 1), width: 50 + 30 * random(), points: [], }; } } for (i = 0; i < 100; i++) { if (i == 0) { collectables[0] = { x: 600, y: gameChar.possibleGroundPosY[0], size: 75, isFound: false, }; } else { if (collectables[i - 1].x + 200 > finish_position_x) { break; } collectables[i] = { x: collectables[i - 1].x + 50 + 100 * random(0.5, 1), y: gameChar.possibleGroundPosY[ floor(random(0, gameChar.possibleGroundPosY.length)) ], size: 75, isFound: false, }; // Checking whether the coin is over a canyon; // marking it as isFound (making it disabled) in case if. for (k = 0; k < canyons.length; k++) { if ( canyons[k].x - canyons[k].width / 2 < collectables[i].x && canyons[k].x + canyons[k].width / 2 > collectables[i].x ) { collectables[i].isFound = true; } } } } } flagpole = { x: finish_position_x, isReached: false, size_vert: 4, size_hor: 7, cell_size: 20, cell_size_v: 20, cell_size_h: 20, }; text_size = width / 20; textSize(text_size); } function draw() { // -------- SKY ---------------- background(palette.sky_color); // -------- GROUND ------------- drawGround(); push(); // Scrolling fill(0); translate(-cameraPosX, 0); // Scrolling if (gameChar.x < finish_position_x) cameraPosX = gameChar.x - width / 2; // Scrolling // -------- CLOUDS -------------- drawClouds(); // -------- MOUNTAINS ----------- drawMountains(); // -------- CANYONS ------------- for (i = 0; i < canyons.length; i++) { canyon = canyons[i]; drawCanyon(canyon); if (!gameChar.isFalling && !gameChar.isJumping) checkCanyon(canyon); } // -------- TREES --------------- drawTrees(); // -------- COLLECTABLES -------- for (i = 0; i < collectables.length; i++) { collectable = collectables[i]; if (!collectable.isFound) drawCollectable(collectable); checkCollectable(collectable); } // -------- FLAGPOLE ------------ renderFlagpole(); if (!flagpole.isReached) checkFlagpole(); // -------- GAME CHARACTER ------ { // Behavior if (!gameChar.isPlummeting) { if (gameChar.isLeft && !gameChar.isRight) { gameChar.x -= gameChar.speed; } else if (gameChar.isRight && !gameChar.isLeft) { gameChar.x += gameChar.speed; } if (gameChar.isJumping) { gameChar.y -= gameChar.jumpingStrength; gameChar.jumpingStrength -= gravity; if (gameChar.jumpingStrength <= 0) { gameChar.isFalling = true; gameChar.isJumping = false; } } else if (gameChar.isFalling) { gameChar.y -= gameChar.jumpingStrength; gameChar.jumpingStrength -= gravity; if (gameChar.y >= gameChar.getCurGroundPosY()) { gameChar.y = gameChar.getCurGroundPosY(); gameChar.isFalling = false; } } else { } } checkPlayerDie(); // Setting sprite { if (gameChar.isPlummeting) { gameChar.sprite = 2; } else if (gameChar.isFalling) { gameChar.sprite = 2; if (gameChar.isLeft && !gameChar.isRight) gameChar.sprite = 5; else if (gameChar.isRight && !gameChar.isLeft) gameChar.sprite = 6; } else { if (gameChar.isLeft && !gameChar.isRight) gameChar.sprite = 4; else if (gameChar.isRight && !gameChar.isLeft) gameChar.sprite = 3; else gameChar.sprite = 1; } } // Depicting plummeting if (gameChar.isPlummeting) { gameChar.y += 3; } // Drawing a sprite gameChar.draw(); } pop(); // Scrolling // -------- INTERFACE ----------- push(); fill(0); text("Score: " + game_score, 12, text_size); drawLives(); if (gameChar.curLives < 1) text("Game over. Press space to continue...", 0, height / 2); else if (flagpole.isReached) text("Level complete. Press space to continue...", 0, height / 2); if (showDebugData) { text(gameChar.curGroundPosYIndex, 99, 99); text(gameChar.getCurGroundPosY(), 188, 99); text(gameChar.sprite, 277, 99); drawFps(); } pop(); } function checkCanyon(t_canyon) { if ( gameChar.x > canyon.x - canyon.width / 2 && gameChar.x < canyon.x + canyon.width / 2 ) { gameChar.isPlummeting = true; } } function checkCollectable(t_collectable) { if ( !t_collectable.isFound && collectable.y == gameChar.getCurGroundPosY() && dist( t_collectable.x, t_collectable.y, gameChar.x, gameChar.y - gameChar.x_step * 4 ) < t_collectable.size / 2 ) { t_collectable.isFound = true; game_score++; } } function drawGround() { push(); noStroke(); fill(palette.ground_color0); rect(0, floorPos_y, width, height - floorPos_y); fill(palette.ground_color1); rect( 0, floorPos_y + (2 * (height - floorPos_y)) / 6, width, floorPos_y + (2 * (height - floorPos_y)) / 6 ); fill(palette.ground_color2); rect( 0, floorPos_y + (4 * (height - floorPos_y)) / 6, width, floorPos_y + (4 * (height - floorPos_y)) / 6 ); stroke(1); translate(-cameraPosX, 0); for (i = -width / 2; i < finish_position_x + width / 2; i += 20) { line( i, floorPos_y + (2 * (height - floorPos_y)) / 6, i + 1, floorPos_y + (2 * (height - floorPos_y)) / 6 ); line( i, floorPos_y + (4 * (height - floorPos_y)) / 6, i + 1, floorPos_y + (4 * (height - floorPos_y)) / 6 ); } pop(); } function drawCanyon(t_canyon) { push(); fill(palette.canyon_river_color); rect(t_canyon.x - t_canyon.width / 2, floorPos_y, t_canyon.width, height); // Waves animation. if (frameCount % 2 == 0) { PointX = random( t_canyon.x - t_canyon.width / 2 + 5, t_canyon.x + t_canyon.width / 2 - 5 ); PointY = random(floorPos_y, height); if (t_canyon.points.length > 2) { t_canyon.points.shift(); } t_canyon.points.push([PointX, PointY, random(20, 100)]); } stroke(palette.canyon_river_wave_color); strokeWeight(5); for (k = 0; k < t_canyon.points.length; k++) { line( t_canyon.points[k][0], t_canyon.points[k][1], t_canyon.points[k][0], t_canyon.points[k][1] + t_canyon.points[k][2] ); } pop(); } function drawCollectable(t_collectable) { push(); // animating the coin's jiggle a = 0.1; b = 3; c = 0.2; d = 2; // a - vert. intensity, c - hor. intensity, b - vert. speed, d - hor. speed t_collectable.y -= sin(frameCount * a) * b; t_collectable.x += sin(frameCount * c) * d; stroke(0.2); fill(palette.coin_outer); ellipse(t_collectable.x, t_collectable.y, 0.7 * t_collectable.size); fill(palette.coin_middle); ellipse(t_collectable.x, t_collectable.y, 0.6 * t_collectable.size); fill(palette.coin_inner); ellipse(t_collectable.x, t_collectable.y, 0.5 * t_collectable.size); // restoring coin's coordinates to preserve game logic t_collectable.y += sin(frameCount * a) * b; t_collectable.x -= sin(frameCount * c) * d; pop(); } function drawClouds() { for (i = 0; i < clouds.length; i++) { push(); translate((frameCount / clouds[i].y) * 20, 0); // imitating clouds movement, upper ones should go faster // Adding counter-translating to implement parallax, the feeling of depth translate(cameraPosX / 1.1, 0); noStroke(); fill(palette.cloud0); ellipse( clouds[i].x - 20, clouds[i].y - 10, 20 * clouds[i].size, 30 * clouds[i].size ); fill(palette.cloud1); ellipse( clouds[i].x + 20, clouds[i].y - 20, 70 * clouds[i].size, 50 * clouds[i].size ); fill(palette.cloud2); ellipse( clouds[i].x, clouds[i].y, 90 * clouds[i].size, 40 * clouds[i].size ); ellipse( clouds[i].x + 45, clouds[i].y - 10, 50 * clouds[i].size, 35 * clouds[i].size ); pop(); } } function drawMountains() { for (i = 0; i < mountains.length; i++) { push(); // slowing down translation to add parallax, the feeling of depth translate(cameraPosX / 2, 0); noStroke(); fill(palette.mountain_shadow); triangle( mountains[i].x - mountains[i].width, floorPos_y, mountains[i].x + mountains[i].skew, mountains[i].height, mountains[i].x + mountains[i].width, floorPos_y ); fill(palette.mountain); triangle( mountains[i].x - mountains[i].width, floorPos_y, mountains[i].x + mountains[i].skew, mountains[i].height, mountains[i].x - mountains[i].width / 1.5, floorPos_y ); pop(); } } function drawHeart(isEmpty, x, y, size) { push(); if (isEmpty) { noFill(); stroke(0); } else { fill(palette.heart_color); stroke(palette.heart_color); } triangle(x, y, x + size, y - size, x + size * 2, y); triangle(x - size * 2, y, x - size, y - size, x, y); triangle(x, y, x + size, y + size, x - size, y + size); triangle(x - size * 2, y, x, y + size * 2, x + size * 2, y); pop(); } function drawLives() { push(); stroke(255, 0, 0); noFill(); var heart_size = 20; for (i = gameChar.baseLives; i > 0; i--) { drawHeart( true, width - i * heart_size * 4.5, heart_size * 1.5, heart_size ); } for (i = gameChar.curLives; i > 0; i--) { drawHeart( false, width - i * heart_size * 4.5, heart_size * 1.5, heart_size ); } pop(); } function drawTrees() { for (i = 0; i < trees_x.length; i++) { // Draw pine in case the integer part of tree // coordinate is even. if (Math.trunc(trees_x[i]) % 2 == 0) { fill(palette.pine_stem); triangle( trees_x[i] - 15, floorPos_y, trees_x[i], floorPos_y - 150, trees_x[i] + 15, floorPos_y ); fill(palette.pine_leaves_color); triangle( trees_x[i] - 45, floorPos_y - 45, trees_x[i], floorPos_y - 120, trees_x[i] + 45, floorPos_y - 45 ); triangle( trees_x[i] - 45, floorPos_y - 85, trees_x[i], floorPos_y - 180, trees_x[i] + 45, floorPos_y - 85 ); } // Draw maple else { fill(palette.maple_stem); triangle( trees_x[i] - 10, floorPos_y, trees_x[i], floorPos_y - 120, trees_x[i] + 10, floorPos_y ); fill(palette.maple_leaves_color); ellipse(trees_x[i], floorPos_y - 50, 80, 30); ellipse(trees_x[i], floorPos_y - 70, 100, 30); ellipse(trees_x[i], floorPos_y - 90, 80, 30); ellipse(trees_x[i], floorPos_y - 110, 40, 30); } } } function renderFlagpole() { // NB - This function is implemented a bit differently than how it was requested in Part 6. // It has no states to switch between - instead, the flag gradually goes into the opposite direction // as the player crosses it, what is done by manipulating flagpole's parameters. // This is done for animation purposes. push(); strokeWeight(0); fill(0); // Movement animation, as mentioned in NB. if (flagpole.isReached && flagpole.cell_size_h > -flagpole.cell_size) flagpole.cell_size_h -= 0.8; // the flagpole pole rect(flagpole.x, floorPos_y / 2, 2, floorPos_y / 2); // the flagpole flag for (i = 0; i < flagpole.size_hor; i++) { for (j = 0; j < flagpole.size_vert; j++) { fill(((i + j) % 2) * 255); rect( flagpole.x + flagpole.cell_size_h * i, floorPos_y / 2 + flagpole.cell_size_v * j, flagpole.cell_size_h, flagpole.cell_size_v ); } } pop(); } function checkFlagpole() { // Considering the linear nature of the game, the flagpole is triggered whenever player is beyond it by x. // If to imagine a hypothetical jump where the player somehow jumps beyond the pole in one frame, // this function will trigger anyway. if (gameChar.x >= finish_position_x) { flagpole.isReached = true; } } function checkPlayerDie() { if (gameChar.y >= height) { if (gameChar.curLives > 1) { gameChar.curLives--; startGame(); } else if (gameChar.curLives == 1) { gameChar.curLives--; } else { gameChar.curLives = 0; } } } function drawFps() { push(); fps = Math.round(frameRate() * 10) / 10; if (fps_recent_values.length < 200) fps_recent_values.push(fps); else fps_recent_values.shift(); fps_recent_values.push(fps); fill("red"); text(fps, 400, 99); stroke("black"); beginShape(LINES); for (i = 1; i < fps_recent_values.length; i++) { vertex(i, fps_recent_values[i]); } endShape(); pop(); } function keyPressed() { console.log(frameCount + " pressed " + key + " " + keyCode); if (gameChar.curLives < 1 || flagpole.isReached) { if (keyCode == 32 /*Space*/) { /*Hard resets the game*/ startGame((full_start = true), (update_objects = true)); } } else if (!gameChar.isPlummeting) { if (keyCode == 65 /*A*/) gameChar.isLeft = true; if (keyCode == 68 /*D*/) gameChar.isRight = true; if (keyCode == 83 /*S*/) gameChar.goDown(); if (keyCode == 87 /*W*/) gameChar.goUp(); // Rewrote jumping routine to make it more natural and be able to support platforms and different player dimensions if ( keyCode == 32 /*Space*/ && !gameChar.isFalling && !gameChar.isJumping ) { gameChar.isJumping = true; gameChar.jumpingStrength = 15; } } if (keyCode == 77 /*M*/) showDebugData = !showDebugData; } function keyReleased() { console.log(frameCount + " released " + key + " " + keyCode); if (keyCode == 65 /*A*/) gameChar.isLeft = false; if (keyCode == 68 /*D*/) gameChar.isRight = false; }