var floorPos_y; var cameraPosX; var camera_speed; var fps_recent_values = []; var showDebugData = false; var debug_charTrace = []; var shadows_enabled = true; var text_size; // Variables to set colors. Set in setup() var palette; var gameChar; var flagpole; var gravity; var game_score; var finish_position_x; var audio = {}; var sound_enabled = false; var music_enabled = false; var game_state; var trees_x = []; var clouds = []; var mountains = []; var collectables = []; var rivers = []; var platforms = []; var enemies = []; function GameCharacter() { this.x = width / 2; this.y = floorPos_y + (height - floorPos_y) / 6; this.x_step = 5; this.y_step = 8; this.scale = 1; this.sprite = 1; this.speed = 5; this.baseLives = 3; this.curLives = 3; this.baseJumpingStrength = 16; this.curJumpingStrength = 0; this.isRight = false; this.isLeft = false; this.isFalling = false; this.isJumping = false; this.isPlummeting = false; this.isOnPlatform = false; this.curGroundIndex = 0; this.memorizedPlatform; this.invincibility_time = 0; this.death_timer; this._updateSprite = function () { if (this.isPlummeting) { this.sprite = 2; } else if (this.isFalling) { this.sprite = 2; if (this.isLeft && !this.isRight) this.sprite = 5; else if (this.isRight && !this.isLeft) this.sprite = 6; } else { if (this.isLeft && !this.isRight) this.sprite = 4; else if (this.isRight && !this.isLeft) this.sprite = 3; else this.sprite = 1; } }; this.draw = function () { this._drawBody = function (jumping = false) { fill(palette.body_color); if (jumping) { triangle( this.x - this.x_step * 2, this.y - this.y_step * 1.5, this.x, this.y - this.y_step * 4, this.x + this.x_step * 2, this.y - this.y_step * 1.5, ); } else { triangle( this.x - this.x_step * 2, this.y, this.x, this.y - this.y_step * 4, this.x + this.x_step * 2, this.y, ); } }; this._drawHead = function () { fill(palette.ground_colors[this.curGroundIndex]); ellipse( this.x, this.y - this.y_step * 5, this.x_step * 6, this.y_step * 4, ); }; this._drawEyes = function (draw_left, draw_right, eyes_height) { fill(0); if (game_state == "FAILURE") { textAlign(CENTER); textSize(this.x_step * 2); text( 'x', this.x - this.x_step * 1.2, this.y - this.y_step * eyes_height, ); text( 'x', this.x + this.x_step * 1.2, this.y - this.y_step * eyes_height, ); } else { if (draw_left) { ellipse( this.x - this.x_step * 1.2, this.y - this.y_step * eyes_height, this.x_step / 1.5, this.y_step / 2, ); } if (draw_right) { ellipse( this.x + this.x_step * 1.2, this.y - this.y_step * eyes_height, this.x_step / 1.5, this.y_step / 2, ); } } }; this._updateSprite(); push(); strokeWeight(1); stroke(1); this.x_step *= this.scale; this.y_step *= this.scale; // Standing, facing frontwards if (this.sprite == 1) { // Body this._drawBody(); // Head this._drawHead(); // Eyes this._drawEyes(true, true, 4.2); } // Jumping, facing forwards else if (this.sprite == 2) { // Body this._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 this._drawHead(); // Eyes this._drawEyes(true, true, 3.8); } // Walking right else if (this.sprite == 3) { // Body this._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 this._drawHead(); // Eyes this._drawEyes(false, true, 4.2); } // Walking left else if (this.sprite == 4) { // Body this._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 this._drawHead(); // Eyess this._drawEyes(true, false, 4.2); } // Jumping left else if (this.sprite == 5) { // Body this._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 this._drawHead(); // Eyes this._drawEyes(true, false, 3.8); } // Jumping right else if (this.sprite == 6) { // Body this._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 this._drawHead(); // Eyes this._drawEyes(false, true, 3.8); } else { text('Bad sprite number!', 10, 10); console.error('Bad this sprite number: ' + this.sprite); noLoop(); } // Drawing an aura of invincibility if (this.invincibility_time > 0 && floor(frameCount / 10) % 2 == 0) { fill('rgba(255,255,255,0.2)'); noStroke(); ellipse( this.x, this.y - this.y_step * 3, this.x_step * 9, this.y_step * 9, ); } this.x_step /= this.scale; this.y_step /= this.scale; pop(); }; this.drawShadow = function () { push(); noStroke(); this.x_step *= this.scale; this.y_step *= this.scale; fill('rgba(0,0,0,0.2)'); if (!this.isPlumetting) shadow_y = groundPositions[this.curGroundIndex]; else shadow_y = NaN; ellipse(this.x, shadow_y, this.x_step * 5, this.y_step * 1.5); this.x_step /= this.scale; this.y_step /= this.scale; pop(); }; this.takeLife = function () { playSound(audio.hurt); gameChar.curLives--; gameChar.invincibility_time += 180; if (this.curLives == 0) { playSound(audio.failure_drum); game_state = "FAILURE"; // recheck music to stop playing playMusic(); } }; this._checkPlayerDie = function () { if (frameCount - this.death_timer > 60) { if (this.curLives > 1) { this.curLives--; this.death_timer = undefined; startGame(); } else if (this.curLives == 1) { this.curLives--; playSound(audio.failure_drum); } else { this.curLives = 0; game_state = "FAILURE"; // recheck music to stop playing playMusic(); } } }; this.act = function () { // undercapping invincibility time this.invincibility_time = max(0, this.invincibility_time - 1); // capping x coordinate to the left this.x = max(-150, this.x); if (!this.isPlummeting) { if (this.isLeft && !this.isRight) { this.x -= this.speed; } else if (this.isRight && !this.isLeft) { this.x += this.speed; } if (this.isJumping) { this.y -= this.curJumpingStrength; this.curJumpingStrength -= gravity; // If the jump peak is reached, the character is considered to be falling instead. if (this.curJumpingStrength <= 0) { this.isFalling = true; this.isJumping = false; } } else if (this.isFalling) { this.y -= this.curJumpingStrength; this.curJumpingStrength -= gravity; for (i = 0; i < platforms[this.curGroundIndex].length; i++) { // If a character is falling above some platform, memorize it if (platforms[this.curGroundIndex][i].isAbove(this)) { this.memorizedPlatform = platforms[this.curGroundIndex][i]; } // If a character is below a memorized platform, make them stand on that platform if ( typeof this.memorizedPlatform !== 'undefined' && this.memorizedPlatform.isBelow(this) ) { this.y = this.memorizedPlatform.y; this.isFalling = false; this.curJumpingStrength = 0; this.isOnPlatform = true; } } // if a character is below the ground, make them stand on the ground if (this.y >= this.getCurGroundY()) { this.y = this.getCurGroundY(); this.isFalling = false; this.curJumpingStrength = 0; this.isOnPlatform = false; } } else if (this.isOnPlatform) { // Not plummeting, standing or falling means being on a platform. // Perform checks whether the player is still on the same platform, // and fall them in case they aren't. if ( !this.memorizedPlatform._isWithinX(this) || this.curGroundIndex != this.memorizedPlatform.curGroundIndex ) { this.isFalling = true; this.isOnPlatform = false; this.memorizedPlatform = undefined; } } } this._checkPlayerDie(); // Doing plummeting if (this.isPlummeting) { // using OR operator to set to frameCount in case death_timer is undefined, // but not updating the death_timer again in case it is not undefined. this.death_timer = this.death_timer || frameCount; this.y += 3; } }; this.getCurGroundY = function () { return groundPositions[this.curGroundIndex]; }; this.goUp = function () { memorizedIndex = this.curGroundIndex; this.curGroundIndex = max(0, this.curGroundIndex - 1); if (memorizedIndex != this.curGroundIndex) { this.y -= (height - floorPos_y) / 3; this.scale -= 0.1; if (this.curGroundIndex == 0) playSound(audio.a_note); if (this.curGroundIndex == 1) playSound(audio.b_note); if (this.curGroundIndex == 2) playSound(audio.c_note); } }; this.goDown = function () { memorizedIndex = this.curGroundIndex; this.curGroundIndex = min( groundPositions.length - 1, this.curGroundIndex + 1, ); if (memorizedIndex != this.curGroundIndex) { this.y += (height - floorPos_y) / 3; this.scale += 0.1; if (this.curGroundIndex == 0) playSound(audio.a_note); if (this.curGroundIndex == 1) playSound(audio.b_note); if (this.curGroundIndex == 2) playSound(audio.c_note); } }; } // NB: All the platforms and enemies mechanics were done // before the task was seen on Week 20. // Therefore, the implementation may // differ from one in the lecture. function Platform(curGroundIndex, x, width, y) { this.curGroundIndex = curGroundIndex; this.x = x; this.width = width; // To make platforms jumpable to each other, define a max vertical difference // thru an arithmetic progression dependent on the game character's jumping strength and gravity. this.max_y_deviation = ((gameChar.baseJumpingStrength / gravity) * gameChar.baseJumpingStrength) / 2; this.y = y || groundPositions[curGroundIndex] - this.max_y_deviation; this.draw = function () { push(); stroke(0); strokeWeight(2); fill(palette.ground_colors[curGroundIndex]); rect(this.x - this.width, this.y, width * 2, height / 100); pop(); }; this.drawShadow = function () { push(); // drawing shadow on the ground noStroke(); fill('rgba(0,0,0, 0.08)'); rect( this.x - this.width, groundPositions[curGroundIndex], width * 2, height / 100, ); pop(); }; this._isWithinX = function (who) { if (who.x >= this.x - this.width && who.x <= this.x + this.width) { return true; } return false; }; this.isBelow = function (who) { if (this._isWithinX(who) && who.y >= this.y) { return true; } return false; }; this.isAbove = function (who) { if (this._isWithinX(who) && who.y < this.y) { return true; } return false; }; } function Enemy(curGroundIndex, x, y, size) { this.x = x; this.y = y; this.size = size; this.curGroundIndex = curGroundIndex; this.draw = function () { push(); fill( lerpColor( palette.enemy_head_color, palette.ground_colors[this.curGroundIndex], 0.7, ), ); ellipse(this.x, this.y, this.size); fill(255, 0, 0); ellipse(this.x - this.size / 2, this.y, this.size / 8, this.size / 4); ellipse(this.x + this.size / 2, this.y, this.size / 8, this.size / 4); textSize(this.size / 1.5); textAlign(CENTER); fill(palette.enemy_body_color); text(this.curGroundIndex, this.x, this.y + this.size / 4); pop(); }; this.drawShadow = function () { push(); fill('rgba(0,0,0,0.2)'); noStroke(); ellipse( this.x, groundPositions[this.curGroundIndex], this.size / 2, this.size / 4, ); pop(); }; this.collidesWith = function (who) { if ( dist(who.x, who.y - 20 /*to account for gameChar's center*/, this.x, this.y) < this.size / 2 && who.curGroundIndex == this.curGroundIndex ) return true; return false; }; this.updatePosition = function () { this.x = this.x + sin((frameCount / random(20, 50)) * random(0.5, 10)); this.y = this.y + cos((frameCount / random(10, 20)) * random(0.5, 10)); }; } function preload() { soundFormats('mp3', 'wav'); audio = { a_note: loadSound('assets/audio/a_note.wav'), b_note: loadSound('assets/audio/b_note.wav'), c_note: loadSound('assets/audio/c_note.wav'), coin: loadSound('assets/audio/coin.wav'), failure_drum: loadSound('assets/audio/failure_drum.mp3'), hurt: loadSound('assets/audio/hurt.wav'), jump: loadSound('assets/audio/jump.wav'), stream_short: loadSound('assets/audio/stream-short.wav'), victory: loadSound('assets/audio/victory.wav'), music: loadSound('assets/audio/Floating Cities.mp3') } audio.music.playMode('untilDone'); audio.stream_short.playMode('untilDone'); } function setup() { createCanvas(1024, 576); palette = { body_color: color('white'), head_color: color('darkgreen'), sky_color: color('#8E9887'), ground_colors: [color('#874321'), color('#636721'), color('#634345')], river_river_color: color('#56C525'), river_river_wave_color: color('#4BAD21'), river_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('rgba(0,0,0,0.5)'), enemy_body_color: color('rgba(255,255,255,0.3)'), }; startGame((level_start = true)); } function startGame(level_start) { game_state = "LEVEL"; floorPos_y = 432; text_size = sqrt(width) + sqrt(height); if (music_enabled) playMusic(); audio.stream_short.stop(); // levels of depth where the character, collectable and enemies stand. groundPositions = [ floorPos_y + (height - floorPos_y) / 6, floorPos_y + (3 * (height - floorPos_y)) / 6, floorPos_y + (5 * (height - floorPos_y)) / 6, ]; camera_speed = 0.9; gravity = 1; game_score = 0; finish_position_x = 4000; textSize(width / 20); flagpole = { x: finish_position_x, isReached: false, size_vert: 4, size_hor: 7, cell_size: 20, cell_size_v: 20, cell_size_h: 20, }; if (level_start) { gameChar = new GameCharacter(); cameraPosX = gameChar.x - width / 2; generateObjects(); } else { gameChar.x = width / 2; gameChar.y = floorPos_y + (height - floorPos_y) / 6; gameChar.scale = 1; gameChar.curGroundIndex = 0; audio.stream_short.stop(); gameChar.isPlummeting = false; gameChar.isFalling = false; gameChar.isJumping = false; gameChar.isLeft = false; gameChar.isRight = false; gameChar.isOnPlatform = false; } } function generateObjects() { // Creating trees, clouds, mountains, rivers, collectables, platforms, enemies. // Trees coords for (i = 0; i < 150; i++) { trees_x[i] = random(-100, 10000); } // Clouds 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, }; } // Rivers for (i = 0; i < 100; i++) { if (i == 0) { rivers[0] = { x: 700, width: 50, points: [], }; } else { if (rivers[i - 1].x + 600 > finish_position_x) break; rivers[i] = { x: rivers[i - 1].x + 100 + 200 * random(0.5, 1), width: 50 + 30 * random(), points: [], }; } } // Collectables for (i = 0; i < 100; i++) { if (i == 0) { collectables[0] = { x: 600, curGroundIndex: 0, y: groundPositions[0] - 10 /*taking 10 for better visuals*/, size: 75, isFound: false, }; } else { // Skipping generating coins after the finish line if (collectables[i - 1].x + 200 > finish_position_x) { break; } r = floor(random(0, groundPositions.length)); collectables[i] = { x: collectables[i - 1].x + 50 + 100 * random(0.5, 1), curGroundIndex: r, y: groundPositions[r] - 10 /*taking 10 for better visuals*/, size: 75, isFound: false, }; // Checking whether the coin is over a river; // marking it as isFound (making it disabled) in case if. for (k = 0; k < rivers.length; k++) { if ( rivers[k].x - rivers[k].width / 2 < collectables[i].x && rivers[k].x + rivers[k].width / 2 > collectables[i].x ) { collectables[i].isFound = true; } } } } // Platform rows for (i = 0; i < groundPositions.length; i++) { platforms[i] = []; } // Platforms for (i = 0; i < 100; i++) { if (i == 0) { platforms[0][0] = new Platform( 0, 700, 50, groundPositions[0] - Platform.max_y_deviation, ); platforms[1][0] = new Platform( 1, 800, 50, groundPositions[1] - Platform.max_y_deviation, ); platforms[2][0] = new Platform( 2, 900, 50, groundPositions[2] - Platform.max_y_deviation, ); } else { let groundPosIndex = floor(random(0, groundPositions.length)); // array.slice(-1)[0] gets the last element of an array without removing it, // contrary to array.pop(). prev = platforms[groundPosIndex].slice(-1)[0]; // stopping generation after the flagpole if (prev.x + prev.width + 155 > finish_position_x) break; // adding platforms platforms[groundPosIndex].push( new Platform( groundPosIndex, prev.x + random(prev.width + 70, prev.width + 155), random(15, 60), max( random( prev.y - prev.max_y_deviation, floorPos_y - prev.max_y_deviation / 2, ), height / 8, ), ), ); } } // Enemies enemies = []; for (i = 0; i < floor(finish_position_x / 130); i++) { index = floor(random(0, groundPositions.length)); enemies.push( new Enemy( index, random(500, finish_position_x), groundPositions[index] - random(50, floorPos_y), random(40, 60), ), ); } } function draw() { // -------- SKY ---------------- background(palette.sky_color); // -------- GROUND ------------- drawGround(); push(); fill(0); translate(-cameraPosX, 0); // Scrolling everything // Focusing on the character. lerp() ensures fluid camera movement. if (gameChar.x < finish_position_x) cameraPosX = lerp(gameChar.x - width / 2, cameraPosX, camera_speed); else cameraPosX = lerp( finish_position_x - width / 2, cameraPosX, camera_speed, ); // -------- CLOUDS -------------- drawClouds(); // -------- MOUNTAINS ----------- drawMountains(); // -------- CANYONS ------------- for (i = 0; i < rivers.length; i++) { river = rivers[i]; drawRiver(river); if (!gameChar.isFalling && !gameChar.isJumping) checkRiver(river); } // -------- TREES --------------- drawTrees(); // -------- PLATFORMS SHADOWS --- if (shadows_enabled) for (i = 0; i < groundPositions.length; i++) { drawPlatformsShadows(i); } // -------- COLLECTABLES -------- for (i = 0; i < collectables.length; i++) { collectable = collectables[i]; if (!collectable.isFound) drawCollectable(collectable); checkCollectable(collectable); } // -------- FLAGPOLE ------------ renderFlagpole(); if (!flagpole.isReached) checkFlagpole(); // GAMECHAR & PLATFORMS RENDER -- complexDraw(); // -------- ENEMIES ------------- for (i = 0; i < enemies.length; i++) { enemies[i].updatePosition(); enemies[i].draw(); if (shadows_enabled) enemies[i].drawShadow(); if ( enemies[i].collidesWith(gameChar) && gameChar.invincibility_time <= 0 ) { gameChar.takeLife(); } } // - ------ GAME CHARACTER ------ gameChar.act(); pop(); // Scrolling end // -------- INTERFACE ----------- drawInterface(); } function complexDraw() { // Since platforms require being drawn in three different ordrs relative to the game character, // a function is made to handle this and to decrease clutter. for (i = 0; i <= gameChar.curGroundIndex; i++) { drawPlatforms(i); } if (shadows_enabled) { gameChar.drawShadow(); } gameChar.draw(); for (i = gameChar.curGroundIndex + 1; i < groundPositions.length; i++) { drawPlatforms(i); } } function drawInterface() { push(); fill(0); text('Score: ' + game_score, 12, text_size); textSize(text_size/2); if (music_enabled) phrase = "enabled"; else phrase = "disabled"; text('Music - ' + phrase, 300, text_size/2); if (sound_enabled) phrase = "enabled"; else phrase = "disabled"; text('Sound - ' + phrase, 300, text_size); textSize(text_size); stroke(255); strokeWeight(2); drawLives(); if (game_state == "FAILURE") { 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.curGroundIndex, 99, 99); text(gameChar.getCurGroundY(), 188, 99); text(gameChar.sprite, 277, 99); console.log(keyCode, key); // draw game character's trajectory drawCharTrace(); drawFps(); } pop(); } function drawCharTrace() { debug_charTrace.push([gameChar.x, gameChar.y]); if (debug_charTrace.length > 100) debug_charTrace.shift(); push(); stroke(255, 0, 0); strokeWeight(3); translate(-cameraPosX, 0); for (i = 0; i < debug_charTrace.length; i++) { point(debug_charTrace[i][0], debug_charTrace[i][1]); } translate(cameraPosX, 0); pop(); } function checkRiver(t_river) { if ( gameChar.x > river.x - river.width / 2 && gameChar.x < river.x + river.width / 2 && gameChar.y > floorPos_y ) { playSound(audio.stream_short); gameChar.isPlummeting = true; } } function checkCollectable(t_collectable) { if ( !t_collectable.isFound && collectable.curGroundIndex == gameChar.curGroundIndex && dist( t_collectable.x, t_collectable.y, gameChar.x, gameChar.y - gameChar.x_step * 4, ) < t_collectable.size / 2 ) { playSound(audio.coin); t_collectable.isFound = true; game_score++; } } function drawGround() { push(); noStroke(); fill(palette.ground_colors[0]); rect(0, floorPos_y, width, height - floorPos_y); fill(palette.ground_colors[1]); rect( 0, floorPos_y + (2 * (height - floorPos_y)) / 6, width, floorPos_y + (2 * (height - floorPos_y)) / 6, ); fill(palette.ground_colors[2]); 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 drawRiver(t_river) { push(); fill(palette.river_river_color); rect(t_river.x - t_river.width / 2, floorPos_y, t_river.width, height); // Waves animation. if (frameCount % 2 == 0) { PointX = random( t_river.x - t_river.width / 2 + 5, t_river.x + t_river.width / 2 - 5, ); PointY = random(floorPos_y, height); if (t_river.points.length > 2) { t_river.points.shift(); } t_river.points.push([PointX, PointY, random(20, 100)]); } stroke(palette.river_river_wave_color); strokeWeight(5); for (k = 0; k < t_river.points.length; k++) { line( t_river.points[k][0], t_river.points[k][1], t_river.points[k][0], t_river.points[k][1] + t_river.points[k][2], ); } pop(); } function drawCollectable(t_collectable) { push(); // animating the coin's jiggle // a - vert. intensity, c - hor. intensity, b - vert. speed, d - hor. speed a = 0.1; b = 3; c = 0.2; d = 2; 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(); // imitating clouds movement, upper ones should go faster translate((frameCount / clouds[i].y) * 20, 0); // 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 drawPlatforms(rowIndex) { push(); for (k = 0; k < platforms[rowIndex].length; k++) { platforms[rowIndex][k].draw(); } pop(); } function drawPlatformsShadows(rowIndex) { push(); for (k = 0; k < platforms[rowIndex].length; k++) { platforms[rowIndex][k].drawShadow(); } pop(); } 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; playSound(audio.victory); game_state = "WIN"; // Stops music by recheking the game state playMusic(); } } 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() { if (game_state == "FAILURE" || game_state == "WIN") { if (keyCode == 32 /*Space*/) { /*Hard resets the game*/ startGame((level_start = true)); } } else if (!gameChar.isPlummeting) { if (keyCode == 65 /*A*/ || keyCode == LEFT_ARROW) gameChar.isLeft = true; if (keyCode == 68 /*D*/ || keyCode == RIGHT_ARROW) gameChar.isRight = true; if (keyCode == 83 /*S*/ || keyCode == DOWN_ARROW) gameChar.goDown(); if (keyCode == 87 /*W*/ || keyCode == UP_ARROW) gameChar.goUp(); // Rewrote jumping routine to make it more natural and be able to // support platforms and different player dimensions if ( (keyCode == 32 /*Space*/ || keyCode == 88) /*X*/ && !gameChar.isFalling && !gameChar.isJumping ) { playSound(audio.jump); gameChar.isJumping = true; gameChar.curJumpingStrength = gameChar.baseJumpingStrength; } } if (keyCode == 77 /*M*/) showDebugData = !showDebugData; if (keyCode == 78 /*N*/) shadows_enabled = !shadows_enabled; if (keyCode == 79 /*P*/) sound_enabled = !sound_enabled; if (keyCode == 80 /*O*/) {music_enabled = !music_enabled; playMusic()}; } function keyReleased() { if (keyCode == 65 /*A*/ || keyCode == LEFT_ARROW) gameChar.isLeft = false; if (keyCode == 68 /*D*/ || keyCode == RIGHT_ARROW) gameChar.isRight = false; } function playSound(sound) { if (sound_enabled) sound.play(); } function playMusic() { if (music_enabled && game_state == "LEVEL") { audio.music.play(); } else { audio.music.stop(); } }