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();
    }
}
