var translateration = 0;
var fps = 0;
var isDebug = false;
var draw_grid = false;
var plummetingSoundNotPlayedBefore = false;
var text_hor_unit;
var text_vert_unit;
var sfx = {};
var mus = {};
var skydecay = 0;
var fps_recent_values = [];
var random_values = []; // from 0 to 1
for (i = 0; i < 500; i++) {
    random_values[i] = Math.random();
}
var memorized_platform;
var amounts = {
    tree: 200,
    cloud: 200,
    mountain: 200,
    canyon: 80,
    collectable: 23,
    enemies: 30,
    platforms: 50,
};
var env = {
    skyColor: 0, //assigned at setup()
    floorPos_y: ((512 + 256) * 3) / 4,
    groundColor: "#7F562A",
    end_x: 3000,
    state: "INTRO",
    isSoundEnabled: false, // never set it true. it causes premature playing of the game sfx and music before loading.
    isMusicEnabled: false,
};
class Entity {
    constructor(x, y) {
        (this.x = x),
            (this.y = y),
            (this.size = 1.5),
            (this.speed = 8),
            (this.score = 0),
            (this.lives = 3),
            (this.health = 100),
            this.prejump_y,
            (this.gravity = 1.5);
        this.vert_speed = -1; // Is negative.
        (this.state = {
            isLeft: false,
            isRight: false,
            isFalling: false,
            isJumping: false,
            isPlummeting: false,
            isDescending: false,
            isAscending: false,
        }),
            (this.projectiles = []);
    }
    resetValues() {
        this.size = 1.5;
        this.speed = 7;
    }
}
var gameChar;
var flagpole = {
    x: env.end_x,
    isReached: false,
    size_vert: 4,
    size_hor: 7,
    cell_size: 20,
    cell_size_v: 20,
    cell_size_h: 20,
};

class Cloud {
    constructor(x, y, size) {
        this.x = x;
        this.y = y;
        this.size = size;
    }
}
class Canyon {
    constructor(x, width) {
        this.x = x;
        this.width = width;
    }
}
class Collectable {
    constructor(x, y, size, worth) {
        this.x = x;
        this.y = y;
        this.size = size;
        this.worth = worth;
    }
}
class Enemy extends Entity {
    constructor(x, y, damage, sin_x_speed, sin_y_speed) {
        super(x, y);
        this.damage = damage;
        this.sin_x_speed = sin_x_speed;
        this.sin_y_speed = sin_y_speed;
    }
    collidesWith(whoCollidesWith) {
        let temp_x = this.x;
        let temp_y = this.y;
        this.x = this.x + 25 * Math.sin(frameCount / this.sin_x_speed) - 15;
        this.y = this.y + 50 * Math.sin(frameCount / this.sin_y_speed) - 20;
        if (
            Math.abs(whoCollidesWith.x - this.x) < 24 * this.size &&
            Math.abs(whoCollidesWith.y - this.y) < 24 * this.size
        ) {
            return true;
        }
        this.y = temp_y;
        this.x = temp_x;
        return false;
    }

    drawEnemy() {
        // lazy so its just a game character model, but with a black hat and no body.
        let temp_y = this.y;
        let temp_x = this.x;
        this.x = this.x + 25 * Math.sin(frameCount / this.sin_x_speed);
        this.y = this.y + 50 * Math.sin(frameCount / this.sin_y_speed);
        drawGameChar(this.x, this.y, 1, this.size, false, color(this.damage, 100));
        this.y = temp_y;
        this.x = temp_x;
    }
}
class Platform {
    constructor(left_x, right_x, y) {
        this.left_x = left_x;
        this.right_x = right_x;
        this.y = y;
    }

    isInXBorders(whoCollidesWith) {
        return whoCollidesWith.x > this.left_x && whoCollidesWith.x < this.right_x;
    }
    isUnder(whoCollidesWith) {
        return this.isInXBorders(whoCollidesWith) && whoCollidesWith.y < this.y;
    }
    isOver(whoCollidesWith) {
        return this.isInXBorders(whoCollidesWith) && whoCollidesWith.y > this.y;
    }
    drawPlatform() {
        push();
        rectMode(CORNERS);
        fill(env.groundColor);
        rect(this.left_x, this.y, this.right_x, this.y + 10);
        pop();
    }
}

var mountains_x = [];
var trees_x = [];
var enemies = [];
var clouds = [];
var canyons = [];
var collectables = [];
var platforms = [];
var cnv;
var win_img;
var gameover_img;

function preload() {
    soundFormats("mp3", "ogg");
    sfx.enemy = loadSound("../assets/sfx/enemy.ogg");
    sfx.falling = loadSound("../assets/sfx/falling.ogg");
    sfx.gameover = loadSound("../assets/sfx/gamover.ogg");
    sfx.jumping = loadSound("../assets/sfx/jumping.ogg");
    sfx.levelcomplete = loadSound("../assets/sfx/levelcomplete.ogg");
    sfx.plummeting = loadSound("../assets/sfx/plummeting.ogg");
    sfx.coin = loadSound("../assets/sfx/coin.mp3");
    /* "Midnight Tale", "Take a Chance" and "SCP-x5x" Kevin MacLeod (incompetech.com) 
    Licensed under Creative Commons: Attribution 4.0 License 
    http://creativecommons.org/licenses/by/4.0/ */
    mus.level = loadSound("../assets/Midnight Tale.mp3");
    mus.win = loadSound("../assets/Take A Chance.mp3");
    mus.gameover = loadSound("../assets/SCP-x5x.mp3");
    win_img = loadImage(
        "https://static.wikia.nocookie.net/someordinarygamers/images/8/88/Bp_1401520973_youre_winner.png"
    );
    gameover_img = loadImage("../assets/gameover.png");
}
function drawFlagpole() {
    push();
    strokeWeight(0);
    fill(0);
    translate(translateration, 0);

    if (flagpole.isReached && flagpole.cell_size_h > -flagpole.cell_size) flagpole.cell_size_h--;
    // the flagpole pole
    rect(flagpole.x, env.floorPos_y / 2, 2, env.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,
                env.floorPos_y / 2 + flagpole.cell_size_v * j,
                flagpole.cell_size_h,
                flagpole.cell_size_v
            );
        }
    }
    pop();
}
function drawGameChar(x, y, sprite, scale, has_body, hat_color, hand, width_mod, height_mod, jump_shift) {
    // Note:
    // The drawing starts at bottom-middlecenter! not at top-left!
    // The aspect ratio is 5:8.
    if (has_body == undefined) has_body = true;
    if (hat_color == undefined) hat_color = "red";
    if (scale == undefined) {
        scale = 1;
    }
    if (width_mod == undefined) {
        width_mod = 1;
    }
    if (height_mod == undefined) {
        height_mod = 1;
    }
    if (jump_shift == undefined) {
        jump_shift = 12;
    }
    var ax_step = 5 * scale * width_mod;
    var ay_step = 8 * scale * height_mod;
    jump_shift = jump_shift * scale;
    function _drawDefaultHat() {
        strokeWeight(1);
        stroke(0);
        fill(hat_color);
        ellipse(x, y - ay_step * 6, ax_step * 10, ay_step * 3);
        ellipse(x, y - ay_step * 6, ax_step * 4, ay_step * 1); // x +0.5 * scale is a minor fix of misalignment. May become a bug.
        strokeWeight(1);
        beginShape();
        vertex(x - ax_step * 2, y - ay_step * 6);
        vertex(x, y - ay_step * 10);
        vertex(x + ax_step * 2, y - ay_step * 6);
        endShape();
    }
    function _drawDefaultBody(jumping) {
        if (jumping) {
            strokeWeight(1);
            stroke(1);
            fill("brown");
            triangle(x - ax_step * 2, y - ay_step * 1.5, x, y - ay_step * 4, x + ax_step * 2, y - ay_step * 1.5);
        } else {
            strokeWeight(1);
            stroke(1);
            fill("brown");
            triangle(x - ax_step * 2, y, x, y - ay_step * 4, x + ax_step * 2, y);
        }
    }
    function _drawDefaultHand(direction) {
        switch (direction) {
            // to left
            case 0: {
                line(x, y - ay_step * 2.5, x + ax_step * 0.8, y - ay_step * 1);
                break;
            }
            // relaxed
            case 1: {
                line(x, y - ax_step * 4, x, y - ay_step);
                break;
            }
            // to right
            case 2: {
                line(x, y - ay_step * 2.5, x - ax_step * 0.8, y - ay_step * 1);
                break;
            }
            // relaxed; doubled for easy animation cycling
            case 3: {
                line(x, y - ax_step * 4, x, y - ay_step);
                break;
            }
            // relaxed
            default: {
                line(x, y - ax_step * 2.5, x, y - ay_step);
                break;
            }
        }
    }
    switch (sprite) {
        // Standing, facing frontwards
        case 1: {
            // Body
            if (has_body) _drawDefaultBody();
            // Head
            fill(0);
            ellipse(x, y - ay_step * 5, ax_step * 6, ay_step * 4);
            // Eyes
            fill(255);
            ellipse(x - ax_step * 1.2, y - ay_step * 4.2, ax_step / 1.5, ay_step / 2);
            ellipse(x + ax_step * 1.2, y - ay_step * 4.2, ax_step / 1.5, ay_step / 2);
            // Hat
            _drawDefaultHat();
            break;
        }
        // Jumping, facing forwards
        case 2: {
            y -= jump_shift;
            // Body
            if (has_body) _drawDefaultBody(true);
            // Hands. (Hands!)
            line(x - ax_step * 1, y - ay_step * 2.5, x - ax_step * 3.2, y - ay_step * 3.2);
            line(x + ax_step * 1, y - ay_step * 2.5, x + ax_step * 3.2, y - ay_step * 3.2);
            // Head
            fill(0);
            ellipse(x, y - ay_step * 5, ax_step * 6, ay_step * 4);
            // Eyes
            fill(255);
            ellipse(x - ax_step * 1, y - ay_step * 3.8, ax_step / 1.5, ay_step / 2);
            ellipse(x + ax_step * 1, y - ay_step * 3.8, ax_step / 1.5, ay_step / 2);

            // Hat
            _drawDefaultHat();
            break;
        }
        // Walking left
        case 3: {
            // Body
            if (has_body) _drawDefaultBody();
            // Hand. (Hand!)
            if (has_body) _drawDefaultHand(hand);
            // Head
            fill(0);
            ellipse(x, y - ay_step * 5, ax_step * 6, ay_step * 4);
            // Eyes
            fill(255);
            ellipse(x + ax_step * 1.2, y - ay_step * 4.2, ax_step / 1.5, ay_step / 2);
            // Hat
            _drawDefaultHat();
            break;
        }
        // Walking right
        case 4: {
            // Body
            if (has_body) _drawDefaultBody();
            // Hand. (Hand!)
            if (has_body) _drawDefaultHand(hand);
            // Head
            fill(0);
            ellipse(x, y - ay_step * 5, ax_step * 6, ay_step * 4);
            // Eyes
            fill(255);
            ellipse(x - ax_step * 1.2, y - ay_step * 4.2, ax_step / 1.5, ay_step / 2);
            // Hat
            _drawDefaultHat();
            break;
        }
        // Jumping left
        case 5: {
            y -= jump_shift;
            // Body
            if (has_body) _drawDefaultBody(true);
            // Hands. (Hands!)
            line(x - ax_step * 1, y - ay_step * 2.5, x - ax_step * 3.2, y - ay_step * 3.2);
            line(x + ax_step * 1, y - ay_step * 2.5, x + ax_step * 3.2, y - ay_step * 3.2);

            // Head
            fill(0);
            ellipse(x, y - ay_step * 5, ax_step * 6, ay_step * 4);
            // Eyes
            fill(255);
            ellipse(x - ax_step * 1, y - ay_step * 3.8, ax_step / 1.5, ay_step / 2);

            // Hat
            _drawDefaultHat();
            break;
        }
        // Jumping right
        case 6: {
            y -= jump_shift;
            // Body
            if (has_body) _drawDefaultBody(true);
            // Hands. (Hands!)
            line(x - ax_step * 1, y - ay_step * 2.5, x - ax_step * 3.2, y - ay_step * 3.2);
            line(x + ax_step * 1, y - ay_step * 2.5, x + ax_step * 3.2, y - ay_step * 3.2);

            // Head
            fill(0);
            ellipse(x, y - ay_step * 5, ax_step * 6, ay_step * 4);
            // Eyes
            fill(255);
            ellipse(x + ax_step * 1, y - ay_step * 3.8, ax_step / 1.5, ay_step / 2);

            // Hat
            _drawDefaultHat();
            break;
        }
        // Error
        default:
            fill("red");
            text("Character error - No or wrong sprite number provided", x, y);
            break;
    }
}
function drawTree(x, y, scale, type, hasLeaves) {
    // x, y are the middle bottom of the tree.
    noStroke();
    fill(255);
    if (hasLeaves == undefined) {
        hasLeaves = true;
    }
    if (scale == undefined) {
        scale = 1;
    }
    if (type == 0) {
        fill(110, 87, 20);
        triangle(x - 15 * scale, y, x, y - 150 * scale, x + 15 * scale, y);
        fill(63, 72, 52);
        if (hasLeaves) triangle(x - 45 * scale, y - 45 * scale, x, y - 120 * scale, x + 45 * scale, y - 45 * scale);
        if (hasLeaves) triangle(x - 45 * scale, y - 85 * scale, x, y - 180 * scale, x + 45 * scale, y - 85 * scale);
    } else if (type == 1) {
        fill(147, 105, 7);
        triangle(x - 10 * scale, y, x, y - 120 * scale, x + 10 * scale, y);
        fill(78, 61, 31);
        if (hasLeaves) {
            ellipse(x, y - 50 * scale, 80 * scale, 30 * scale);
            ellipse(x, y - 70 * scale, 100 * scale, 30 * scale);
            ellipse(x, y - 90 * scale, 80 * scale, 30 * scale);
            ellipse(x, y - 110 * scale, 40 * scale, 30 * scale);
        }
    }
}
function drawCloud(cloud) {
    if (cloud.size == undefined) {
        scale = 1;
    }
    noStroke();

    fill(200);
    ellipse(cloud.x - 20, cloud.y - 10, 50 * cloud.size, 30 * cloud.size);

    fill(220);
    ellipse(cloud.x + 20, cloud.y - 20, 70 * cloud.size, 50 * cloud.size);

    fill(255);
    ellipse(cloud.x, cloud.y, 90 * cloud.size, 40 * cloud.size);
    ellipse(cloud.x + 45, cloud.y - 10, 50 * cloud.size, 35 * cloud.size);
}
function drawMountain(x, m_width, m_height, skew) {
    noStroke();
    fill(60);
    triangle(x - m_width, env.floorPos_y, x + skew, m_height, x + m_width, env.floorPos_y);
    fill(90);
    triangle(x - m_width, env.floorPos_y, x + skew, m_height, x - m_width / 1.5, env.floorPos_y);
    fill(170);
    triangle(
        (4 * x - m_width + 3 * skew) / 4,
        (env.floorPos_y + 3 * m_height) / 4,
        x + skew,
        m_height,
        (4 * x + m_width + 3 * skew) / 4,
        (env.floorPos_y + 3 * m_height) / 4
    );
}
function drawCanyon(canyon) {
    noStroke();
    let x = canyon.x;
    let c_width = canyon.width;
    fill(90, 30, 0);
    quad(
        x - c_width / 2,
        env.floorPos_y,
        x + c_width / 2,
        env.floorPos_y,
        x + c_width / 2,
        height,
        x - c_width / 2,
        height
    );

    fill("blue");
    quad(
        x - c_width / 20,
        env.floorPos_y,
        x + c_width / 20,
        env.floorPos_y,
        x + c_width / 20,
        height,
        x - c_width / 20,
        height
    );

    fill(30);
    quad(
        x - c_width / 2,
        env.floorPos_y,
        x + c_width / 2,
        env.floorPos_y,
        x + c_width / 10,
        env.floorPos_y + env.floorPos_y / 40,
        x - c_width / 10,
        env.floorPos_y + env.floorPos_y / 40
    );
}
function drawCollectable(collectable) {
    let x = collectable.x;
    let y = collectable.y;
    let size = collectable.size;
    let worth = collectable.worth;
    strokeWeight(0.5);
    fill(253, 195, 52);
    ellipse(x, y, 3.5 * size, 3.5 * size);
    fill(171);
    ellipse(x, y, 3 * size, 3 * size);
    fill(253, 195, 52);
    ellipse(x, y, 2.5 * size, 2.5 * size);
    fill(200, 150, 30);
    textSize(size * 1.5);
    textAlign(CENTER, CENTER);
    fill(0);
    text(worth, x, y);

    textSize(12);
    textAlign(LEFT, TOP);
}
function drawSky() {
    background(env.skyColor);
}
function drawAudioSymbol() {
    push();
    fill(0);
    strokeWeight(4);
    triangle(
        width - text_vert_unit * 5,
        text_hor_unit * 7,
        width - text_vert_unit * 4,
        text_hor_unit * 5,
        width - text_vert_unit * 4,
        text_hor_unit * 9
    );
    if (env.isSoundEnabled) {
        arc(width - text_vert_unit * 3, text_hor_unit * 7, text_hor_unit, text_vert_unit * 1.5, (3 * PI) / 2, PI / 2);
        arc(width - text_vert_unit * 3.5, text_hor_unit * 7, text_hor_unit, text_vert_unit, (3 * PI) / 2, PI / 2);
    }
    textSize(text_vert_unit / 2);
    strokeWeight(0);
    pop();
}
function drawMusicSymbol() {
    push();
    fill(0);
    strokeWeight(4);
    rectMode(CORNERS);
    if (env.isMusicEnabled) {
        line(width - text_vert_unit * 4.5, text_hor_unit * 15, width - text_vert_unit * 4.5, text_hor_unit * 11);
        line(width - text_vert_unit * 2.5, text_hor_unit * 14, width - text_vert_unit * 2.5, text_hor_unit * 10);
        line(width - text_vert_unit * 4.5, text_hor_unit * 11, width - text_vert_unit * 2.5, text_hor_unit * 10);
    }
    ellipse(width - text_vert_unit * 4.5, text_hor_unit * 15, text_vert_unit / 2);
    ellipse(width - text_vert_unit * 2.5, text_hor_unit * 14, text_vert_unit / 2);
    pop();
}
function playSound(sound, volume, isMusicEnabled) {
    if (volume == undefined) volume = 1;
    if (env.isSoundEnabled) {
        sound.play(0, 1, volume);
    } else {
        sound.stop();
    }
}
function playMusic(sound, volume) {
    if (volume == undefined) volume = 1;
    if (env.isMusicEnabled) {
        sound.play(0, 1, volume);
    } else {
        sound.stop();
    }
}

function drawMountains() {
    push();
    translate(translateration * 0.3, 0);
    for (i = 0; i < mountains_x.length; i++) {
        drawMountain(
            mountains_x[i],
            100 + 200 * random_values[i + 121],
            400 - 50 * random_values[i + 211],
            50 * random_values[i + 8]
        );
    }
    pop();
}
function drawClouds(areMoving) {
    push();
    translate(translateration * 0.6, 0);
    for (i = 0; i < clouds.length; i++) {
        push();
        if (areMoving) translate(frameCount * random_values[i], 0);
        drawCloud(clouds[i]);
        pop();
    }
    pop();
}
function drawCanyons() {
    push();
    translate(translateration, 0);
    for (i = 0; i < canyons.length; i++) {
        drawCanyon(canyons[i]);
        fill(200, 150, 0);
    }
    pop();
}
function drawTrees() {
    push();
    translate(translateration, 0);
    for (i = 0; i < trees_x.length; i++) {
        drawTree(trees_x[i], env.floorPos_y, 0.5 + random_values[i], Math.round(trees_x[i]) % 2);
    }
    pop();
}
function drawCollectables() {
    push();
    translate(translateration, 0);
    for (i = 0; i < collectables.length; i++) {
        drawCollectable(collectables[i]);
    }
    pop();
}
function drawPlatforms() {
    push();
    translate(translateration, 0);
    for (i = 0; i < platforms.length; i++) {
        platforms[i].drawPlatform();
    }
    pop();
}
function manageEnemies() {
    push();
    translate(0, 0);
    for (i = 0; i < enemies.length; i++) {
        enemies[i].drawEnemy();
        for (j = 0; j < gameChar.projectiles.length; j++) {
            if (enemies[i].collidesWith(gameChar.projectiles[j])) {
                enemies.splice(i, 1);
                gameChar.projectiles.splice(j, 1);
                i = 0;
                j = 0;
                playSound(sfx.enemy);
            }
        }
        if (enemies[i].collidesWith(gameChar) && env.state == "LEVEL") {
            background(255 * random(), 255 * random(), 255 * random());
            playSound(sfx.enemy, enemies[i].damage / 10);
            gameChar.state.isPlummeting = true;
        }
    }
    pop();
}
function drawInterface() {
    push();
    translate(-translateration, 0);
    fill("black");
    rect(0, 0, width, text_vert_unit * 2);

    stroke("red");
    line(0, text_vert_unit * 2, width, text_vert_unit * 2);
    fill("red");
    textFont("Courier New");
    textSize(text_vert_unit);
    text("Score: " + gameChar.score, width - text_vert_unit * 7, text_hor_unit * 2);
    text("Lives: " + gameChar.lives, width - text_vert_unit * 14, text_hor_unit * 2);
    // Debug things
    drawAudioSymbol();
    drawMusicSymbol();
    if (isDebug) debug();
    pop();
}
function shootProjectile() {
    playSound(sfx.coin, 0, 1, 0.5);
    let projectile = { color: "red", x: gameChar.x, y: gameChar.y - 45, direction: 0 };
    if (gameChar.state.isLeft) {
        projectile.direction = -15;
        gameChar.projectiles.push(projectile);
    } else {
        projectile.direction = 15;
        gameChar.projectiles.push(projectile);
    }
}
function updateProjectiles() {
    for (i = 0; i < gameChar.projectiles.length; i++) {
        push();
        stroke(gameChar.projectiles[i].color);
        strokeWeight(4);
        line(
            gameChar.projectiles[i].x,
            gameChar.projectiles[i].y,
            gameChar.projectiles[i].x - 30,
            gameChar.projectiles[i].y
        );
        pop();
        gameChar.projectiles[i].x += gameChar.projectiles[i].direction;
        if (gameChar.projectiles[i].x > width - translateration || gameChar.projectiles[i].x < 0 - translateration) {
            gameChar.projectiles.splice(i, 1);
        }
    }
}
function manageMovement() {
    if (gameChar.state.isPlummeting) {
        gameChar.y += gameChar.speed / 2;
        gameChar.size *= 0.98;
        gameChar.speed *= 0.96;
        if (gameChar.size < 0.2) restart(1);
    } else if (!(gameChar.state.isJumping || gameChar.state.isFalling)) {
        // Checking if the character is beyond the platform, then making them fall.
        if (memorized_platform != undefined && !memorized_platform.isInXBorders(gameChar)) {
            gameChar.state.isFalling = true;
            memorized_platform = undefined;
        } else {
            // Falling into canyons
            if (isOverCanyon(gameChar) && memorized_platform == undefined) {
                gameChar.state.isPlummeting = true;
                if (!plummetingSoundNotPlayedBefore) {
                    playSound(sfx.plummeting);
                    plummetingSoundNotPlayedBefore = true;
                }
            }
            if (gameChar.state.isAscending && gameChar.y > floorPos_y) {
                gameChar_y -= gameChar.speed;
            }
            if (gameChar.state.isDescending && gameChar_y < height) {
                gameChar_y += gameChar.speed;
            }
            if (gameChar.state.isLeft) {
                gameChar.x -= gameChar.speed;
            }
            if (gameChar.state.isRight) {
                gameChar.x += gameChar.speed;
            }
        }
    } else if (gameChar.state.isJumping) {
        // Forget the platform as it is irrelevant when we gain height.
        memorized_platform = undefined;
        gameChar.y += gameChar.vert_speed;
        gameChar.vert_speed += gameChar.gravity;
        if (gameChar.vert_speed > 0) {
            gameChar.state.isJumping = false;
            gameChar.state.isFalling = true;
            playSound(sfx.falling);
        }
        if (gameChar.state.isLeft) {
            gameChar.x -= gameChar.speed;
        }
        if (gameChar.state.isRight) {
            gameChar.x += gameChar.speed;
        }
    } else if (gameChar.state.isFalling) {
        // Checking is a platform under the player, and therefore, if it is, save it and check the next frame did the character
        // go under the platform - if so, end them on the platform.
        for (i = 0; i < platforms.length; i++) {
            if (platforms[i].isUnder(gameChar)) memorized_platform = platforms[i];
        }
        if (memorized_platform != undefined && memorized_platform.isOver(gameChar)) {
            gameChar.y = memorized_platform.y;
            gameChar.state.isJumping = false;
            gameChar.state.isFalling = false;
        } else {
            gameChar.y += gameChar.vert_speed;
            gameChar.vert_speed += gameChar.gravity;
            if (gameChar.y >= env.floorPos_y + 50) {
                gameChar.y = env.floorPos_y + 50;
                gameChar.state.isJumping = false;
                gameChar.state.isFalling = false;
            }
            if (gameChar.state.isLeft) {
                gameChar.x -= gameChar.speed;
            }
            if (gameChar.state.isRight) {
                gameChar.x += gameChar.speed;
            }
        }
    }
    // Borders of the world check
    if (gameChar.x <= 50) {
        gameChar.x = 50;
    }
}

// Fun fact - the following function is made using ChatGPT!
function drawIsogonalBrickWall(x, y, size, hSkew, vSkew) {
    push();
    translate(translateration, 0);
    // Set the fill color of the bricks to a redish
    fill(150, 100, 110);

    // Calculate the width and height of each brick
    let brickWidth = size / 5;
    let brickHeight = size / 10;

    // Draw the bricks in a 5x10 grid, with a small gap between each brick
    for (let i = 0; i < 5; i++) {
        for (let j = 0; j < 10; j++) {
            let xPos = x + i * (brickWidth + 1) + j * hSkew;
            let yPos = y + j * (brickHeight + 1) + i * vSkew;
            rect(xPos, yPos, brickWidth, brickHeight);
        }
    }
    pop();
}

function checkCollectable(whoCollidesWith) {
    for (i = 0; i < collectables.length; i++) {
        if (
            Math.abs(whoCollidesWith.x - collectables[i].x) < collectables[i].size * 2 &&
            Math.abs(whoCollidesWith.y - collectables[i].y) < collectables[i].size * 2
        ) {
            whoCollidesWith.score += collectables[i].worth;
            collectables.splice(i, 1);
            playSound(sfx.coin);
        }
    }
}
function isOverCanyon(who) {
    for (i = 0; i < canyons.length; i++) {
        if (Math.abs(who.x - canyons[i].x) < 43 + 25 * random_values[i + 124]) {
            return true;
        }
    }
    return false;
}
function scrolling(centerOnWho) {
    if (centerOnWho.x > width - width / 2.5 - translateration) {
        translateration -= centerOnWho.speed;
    } else if (centerOnWho.x < width / 2.5 - translateration) {
        translateration += centerOnWho.speed;
    }
}
function checkFlagpole(withWho) {
    if (withWho.x > env.end_x) {
        flagpole.isReached = true;
        env.state = "WIN";
        withWho.isWinner = true;
        playSound(sfx.levelcomplete);
    }
}
function restart(decreaseLives) {
    if (env.state != "LEVEL") window.location.reload(); // Reloads the web page.
    gameChar.state.isPlummeting = false;
    gameChar.lives -= decreaseLives;
    if (gameChar.lives < 1) {
        env.state = "GAMEOVER";
        playSound(sfx.gameover);
        return;
    }
    gameChar.x = width / 2;
    gameChar.y = env.floorPos_y + 50;
    gameChar.resetValues();
    translateration = -gameChar.x;
    plummetingSoundNotPlayedBefore = false;
}

function getSpriteNumber() {
    if (gameChar.state.isJumping && gameChar.state.isLeft) return 5;
    else if (gameChar.state.isJumping && gameChar.state.isRight) return 6;
    else if (gameChar.state.isJumping) return 2;
    else if (gameChar.state.isFalling && gameChar.state.isLeft) return 5;
    else if (gameChar.state.isFalling && gameChar.state.isRight) return 6;
    else if (gameChar.state.isFalling) return 2;
    else if (gameChar.state.isPlummeting) return 2;
    else if (gameChar.state.isLeft) return 4;
    else if (gameChar.state.isRight) return 3;
    else if (gameChar.state.isDescending) return 1;
    else return 1;
}
function debug() {
    function _drawGridLines() {
        push();
        console.log("works");
        stroke(255);
        strokeWeight(1);
        for (i = text_vert_unit; i < width; i += text_vert_unit) {
            line(0, i, width, i);
        }
        for (j = text_hor_unit; j < height * 2; j += text_hor_unit) {
            line(j, 0, j, height);
        }
        pop();
    }
    this.drawGrid = false;

    if (gameChar.state.isJumping) text("JUMPING", text_vert_unit * 6, text_hor_unit * 2);
    if (gameChar.state.isFalling) text("FALLING", text_vert_unit * 6, text_hor_unit * 2);
    if (gameChar.state.isPlummeting) text("PLUMMETING", text_vert_unit * 11, text_hor_unit * 2);

    if (frameCount % 2 == 0) {
        fps = Math.round(frameRate() * 10) / 10;
        if (fps_recent_values.length < 200) fps_recent_values.push(fps);
        else fps_recent_values.splice(0, 1);
        fps_recent_values.push(fps);
    }
    fill("red");
    text(fps, text_vert_unit, text_hor_unit * 2);
    stroke("black");
    beginShape(LINES);
    for (i = 1; i < fps_recent_values.length; i++) {
        vertex((text_vert_unit + i) * 1.5, (text_hor_unit * 6 - fps_recent_values[i]) * 2);
    }
    endShape();
    text(gameChar.x, text_vert_unit * 18, text_hor_unit * 2);
    if (draw_grid) drawGridLines();
}

function createEnemies() {
    enemies[0] = new Enemy(1100, 600, random(10, 50), random(20, 50), random(10, 30));

    for (let i = 1; i < amounts.enemies; i++) {
        let elevation;
        if (random_values[i] <= 1 / 3) elevation = 350;
        else if (random_values[i] > 1 / 3 && random_values <= 2 / 3) elevation = 300;
        else elevation = 400;

        enemies[i] = new Enemy(
            enemies[i - 1].x + 200 + 250 * random(),
            elevation,
            random(10, 50),
            random(20, 50),
            random(10, 30)
        );
    }
}
function createTrees() {}
function setup() {
    cnv = createCanvas(1024 + 256, 512 + 256);

    env.skyColor = color(142, 152, 135);
    gameChar = new Entity();
    gameChar.x = width / 2;
    gameChar.y = env.floorPos_y + 50;

    platforms[0] = new Platform(500, 600, 350);
    for (i = 1; i < amounts.platforms; i++) {
        let left_x = platforms[i - 1].right_x + 100 + 300 * random();
        let right_x = left_x + 50 + 100 * random();
        if (left_x > env.end_x && right_x > env.end_x) break;
        platforms[i] = new Platform(left_x, right_x, 300 + 100 * random());
    }
    createEnemies();

    trees_x = [50];
    for (i = 1; i <= amounts.tree; i++) {
        trees_x[i] = trees_x[i - 1] + 20 + 50 * random_values[i];
    }
    clouds[0] = new Cloud(-1000, 60, 1);
    for (i = 1; i <= amounts.cloud; i++) {
        clouds[i] = new Cloud(
            clouds[i - 1].x + 20 + 50 * random_values[i],
            (env.floorPos_y / 1.9) * random_values[i + 60],
            0.5 + random_values[i + 100]
        );
    }

    mountains_x = [50];
    for (i = 1; i <= amounts.mountain; i++) {
        mountains_x[i] = mountains_x[i - 1] + 20 + 400 * random_values[i + 100];
    }

    canyons[0] = new Canyon(gameChar.x + 200, 100 + 50 * random_values[124]);
    for (i = 1; i <= amounts.canyon; i++) {
        let x = canyons[i - 1].x + 200 + 600 * random_values[i + 100];
        if (x > flagpole.x - 250) break;
        canyons[i] = new Canyon(x, 100 + 50 * random_values[i + 124]);
    }

    collectables[0] = new Collectable(gameChar.x + 350, env.floorPos_y + 50, 20, 10);
    for (i = 1; i <= amounts.collectable; i++) {
        let worth;
        if (random_values[i] <= 1 / 3) worth = 15;
        else if (random_values[i] > 1 / 3 && random_values <= 2 / 3) worth = 20;
        else worth = 25;
        let x = collectables[i - 1].x + 70 + 400 * random_values[i + 213];
        if (x > flagpole.x) break;
        collectables[i] = new Collectable(x, env.floorPos_y + 50, worth, worth);
    }

    // Pruning over-canyon trees. Literally.
    for (i = 0; i < trees_x.length; i++) {
        for (j = 0; j < canyons.length; j++) {
            if (
                trees_x[i] > canyons[j].x - (40 + 50 * random_values[j + 124]) &&
                trees_x[i] < canyons[j].x + (40 + 50 * random_values[j + 124])
            ) {
                trees_x[i] = undefined;
            }
        }
    }
    // Doing the same with collectables, for honesty.
    for (i = 0; i < collectables.length; i++) {
        for (j = 0; j < canyons.length; j++) {
            if (
                collectables[i].x > canyons[j].x - (40 + 50 * random_values[j + 124]) &&
                collectables[i].x < canyons[j].x + (40 + 50 * random_values[j + 124])
            ) {
                collectables[i].x = NaN;
            }
        }
    }
}
function draw() {
    text_hor_unit = height / 40;
    text_vert_unit = width / 40;

    drawSky(); //fill the sky
    fill(env.groundColor);
    rect(0, env.floorPos_y, width, height);
    drawMountains();
    // Also updates clouds movement using translate(). May be disabled.
    drawClouds(true);
    drawTrees();
    drawCanyons();
    drawFlagpole();
    drawIsogonalBrickWall(-89, env.floorPos_y - 89, 200, 0, -30);
    drawCollectables();
    drawPlatforms();

    switch (env.state) {
        case "INTRO": {
            push();
            textAlign(CENTER);
            textSize(text_vert_unit * 3);
            strokeWeight(10);
            textFont("Courier");
            textStyle(BOLD);
            fill(0);
            text("The Dead Magic Run", width / 2, height / 2);
            textSize(text_vert_unit);
            text("Press any button to attempt a run", width / 2, height / 2 + text_vert_unit * 2);
            pop();
            break;
        }
        case "INSTRUCTIONS": {
            push();
            textAlign(LEFT);
            textSize(text_vert_unit);
            strokeWeight(3);
            textFont("Courier");
            rectMode(CORNERS);
            fill(0, 100);
            rect(text_hor_unit, text_vert_unit * 3, text_hor_unit * 55, text_vert_unit * 11);
            fill(255);
            text("The goal is to survive and reach the right end.", text_hor_unit, text_vert_unit * 4);
            text("Press WASD to move, Space to jump", text_hor_unit, text_vert_unit * 5);
            text("E or mouse click to shoot.", text_hor_unit, text_vert_unit * 6);
            text("Press P to enable and disable sounds,", text_hor_unit, text_vert_unit * 7);
            text("and O letter to enable and disable music.", text_hor_unit, text_vert_unit * 8);
            text("Press Space to continue and begin the run. Good luck.", text_hor_unit, text_vert_unit * 10);
            pop();
            break;
        }
        case "LEVEL": {
            // Update scrolling based on the character movement
            scrolling(gameChar);
            translate(translateration, 0); // Translating everything else to prevent next elements from scrolling too.
            // Collectables collision
            checkCollectable(gameChar);
            checkFlagpole(gameChar);
            manageMovement();
            // the sprite state machine
            var sprite = getSpriteNumber();
            drawGameChar(
                gameChar.x,
                gameChar.y,
                sprite,
                gameChar.size,
                true,
                "red",
                (hand = Math.floor(frameCount / 12) % 4)
            );
            manageEnemies();
            updateProjectiles();
            playMusic(mus.level);
            break;
        }
        case "WIN": {
            manageEnemies();
            push();
            image(win_img, width / 2 - win_img.width / 2 + 10, height / 2 - win_img.height / 2);
            fill("yellow");
            textSize(text_vert_unit * 1.5);
            textAlign(CENTER);
            strokeWeight(2);
            textFont("Courier");
            text("Level complete. Press R to replay.", width / 2, height / 2 - win_img.height / 2);
            translate(translateration, frameCount);
            pop();
            scrolling(flagpole); // fix the viewpoint on the flagpole
            translate(translateration, 0); // Translating everything else to prevent next elements from scrolling too.
            // Forcing to run right
            gameChar.state.isRight = true;
            gameChar.state.isLeft = false;
            // the sprite state machine
            var sprite = getSpriteNumber();
            manageMovement();
            drawGameChar(gameChar.x, gameChar.y, sprite, gameChar.size, true, "red", Math.floor(frameCount / 12) % 4);
            playMusic(mus.win);
            break;
        }
        case "GAMEOVER": {
            manageEnemies();
            push();
            image(gameover_img, width / 2 - 256 / 2, height / 2 - 256 / 2);
            fill("black");
            textSize(text_vert_unit * 1.5);
            textAlign(CENTER);
            strokeWeight(2);
            textFont("Courier");
            text("Gamover. Press R to replay.", width / 2, height / 2 - 256 / 2);
            pop();
            translate(translateration, 0);
            playMusic(mus.gameover);
            break;
        }
        default: {
            push();
            textSize(50);
            fill("red");
            text("Game state error!", width / 2, height / 2);
            pop();
            break;
        }
    }
    drawInterface();
}

function playMusic(requestedMelody) {
    // spaghetti code.
    if (requestedMelody != mus.level) mus.level.stop();
    if (requestedMelody != mus.win) mus.win.stop();
    if (requestedMelody != mus.gameover) mus.gameover.stop();
    if (!requestedMelody.isPlaying() && env.isMusicEnabled) {
        requestedMelody.play(0, 1, 0.3);
    } else if (!env.isMusicEnabled) requestedMelody.stop();
}

function mouseClicked() {
    shootProjectile();
}
function keyPressed() {
    if (env.state == "INTRO") {
        env.state = "INSTRUCTIONS";
    } else if (key == " " && env.state == "INSTRUCTIONS") {
        env.state = "LEVEL";
    }
    if (keyCode == 32 && !(gameChar.state.isJumping || gameChar.state.isFalling)) {
        gameChar.state.isJumping = true;
        gameChar.prejump_y = gameChar.y;
        gameChar.vert_speed = -30;
        playSound(sfx.jumping);
    }
    if (key == "E" && env.state == "LEVEL") {
        shootProjectile();
    }
    if (key == "A") {
        gameChar.state.isLeft = true;
    }
    if (key == "D") {
        gameChar.state.isRight = true;
    }
    if (key == "R" && (env.state == "WIN" || env.state == "GAMEOVER")) {
        restart();
    }
    if (key == "P" && !env.isSoundEnabled) {
        env.isSoundEnabled = true;
    } else if (key == "P" && env.isSoundEnabled) {
        env.isSoundEnabled = false;
    }
    if (key == "O" && !env.isMusicEnabled) {
        env.isMusicEnabled = true;
    } else if (key == "O" && env.isMusicEnabled) {
        env.isMusicEnabled = false;
    }
    if (key == "G" && isDebug) draw_grid = true;

    console.log("keyPressed: " + key);
    console.log("keyPressed: " + keyCode);
    return false;
}
function keyReleased() {
    if (key == "M") {
        if (isDebug) isDebug = false;
        else isDebug = true;
    }
    if (key == "A") {
        gameChar.state.isLeft = false;
    }
    if (key == "D") {
        gameChar.state.isRight = false;
    }
    if (key == "G" && isDebug) draw_grid = false;
    console.log("keyReleased: " + key);
    console.log("keyReleased: " + keyCode);
}
