Source: player.js

var BHell = (function (my) {

    /**
     * Player class.
     * @constructor
     * @memberOf BHell
     * @extends BHell.BHell_Sprite
     */
    var BHell_Player = my.BHell_Player = function () {
        this.initialize.apply(this, arguments);
    };

    BHell_Player.prototype = Object.create(my.BHell_Sprite.prototype);
    BHell_Player.prototype.constructor = BHell_Player;

    /**
     * Constructor. Creates a player from the JSON configuration file.
     *
     * Parameters:
     *
     * - name: Player's name,
     * - sprite: Charset image,
     * - speed: Player's speed rank (D = 1, C = 2, B = 3, A = 4, S = 5. The actual speed is 2 * rank pixels per frame),
     * - rate: Player's rate of fire rank (D, C, B, A, S. See {@link BHell.BHell_Emitter_Factory}).
     * - power: Player's power rank (D, C, B, A, S. See {@link BHell.BHell_Emitter_Factory}),
     * - bombs: Player's initial stock of bombs (D = 1, C = 2, B = 3, A = 4, S = 5),
     * - unlocked: If true the player can be used on a stage,
     * - can_be_bought: If true the player can be bought at the shop,
     * - price: The player's price at the shop,
     * - hitbox_w: Width of the player's hitbox,
     * - hitbox_h: Height of the player's hitbox,
     * - index: Charset index,
     * - direction: Charset direction (note: lives displayed on the HUD will always use direction 2),
     * - frame: Initial charset frame index (0-2),
     * - animation_speed: Number of updates required for frame change,
     * - animated: If true the sprite will be animated,
     * - select_se: Sound effect played when selecting the player,
     * - spawn_se: Sound effect played when the player has spawned,
     * - death_se: Sound effect played on player's death,
     * - victory_se: Sound effect played on stage cleared,
     * - bomb: Bomb parameters (see {@link BHell.BHell_Bomb_Base} and derived classes),
     * - emitters: Array of emitters (see {@link BHell.BHell_Emitter_Base} and derived classes).
     *
     * @param id Id of the player to create.
     * @param lives Initial number of lives (-1: unlimited).
     * @param unlimitedBombs If true, the bombs are infinite.
     * @param parent Container for the sprites.
     */
    BHell_Player.prototype.initialize = function (id, lives, unlimitedBombs, parent) {
        var playerData = $dataBulletHell.players[id];
        var playerParams = Object.assign({}, $gamePlayer.bhellPlayers.filter(p => {
            return p.index === id;
        })[0]);

        ["speed", "bombs"].forEach(p => {
            switch (playerParams[p]) {
                case "D":
                    playerParams[p] = 1;
                    break;
                case "C":
                    playerParams[p] = 2;
                    break;
                case "B":
                    playerParams[p] = 3;
                    break;
                case "A":
                    playerParams[p] = 4;
                    break;
                case "S":
                    playerParams[p] = 5;
                    break;
                default:
                    playerParams[p] = 1;
                    break;
            }
        });

        my.BHell_Sprite.prototype.initialize.call(this, playerData.sprite, playerData.index, playerData.direction, playerData.frame, playerData.animated, playerData.animation_speed);

        this.parent = parent;
        this.parent.addChild(this);
        this.emitters = [];
        this.immortal = true;
        this.justSpawned = true;
        this.lives = lives;
        if (unlimitedBombs) {
            this.startingBombs = -1;
        }
        else {
            this.startingBombs = playerParams.bombs;
        }

        this.bombs = this.startingBombs;

        this.spawn_se = playerData.spawn_se;
        this.death_se = playerData.death_se;
        this.victory_se = playerData.victory;

        this.won = false;
        this.bonusLives = 0;

        this.hitboxW = my.parse(playerData.hitbox_w, this.x, this.y, this.patternWidth(), this.patternHeight(), Graphics.width, Graphics.height);
        this.hitboxH = my.parse(playerData.hitbox_h, this.x, this.y, this.patternWidth(), this.patternHeight(), Graphics.width, Graphics.height);
        this.grazingRadius = my.parse(playerData.grazing_radius, this.x, this.y, this.patternWidth(), this.patternHeight(), Graphics.width, Graphics.height);

        this.speed = playerParams.speed * 2;

        playerData.emitters.forEach(e => {
            var emitter = my.BHell_Emitter_Factory.parseEmitter(e, this.x, this.y, this.patternWidth(), this.patternHeight(), playerParams.rate, playerParams.power, this.parent, my.friendlyBullets);
            if (emitter != null) {
                this.emitters.push(emitter);
                this.parent.addChild(emitter);
            }
        });

        if (playerData.bomb != null) {
            var regex = /BHell_Bomb_[A-Za-z0-9_]+/;
            if (regex.exec(playerData.bomb.class) != null) {
                var bombClass = eval("my." + playerData.bomb.class); // Safe-ish, since Only class names can be evaluated.
                this.bomb = new bombClass(this.parent, playerData.bomb, my.friendlyBullets);
            }
            else {
                this.bomb = new my.BHell_Bomb_Base(this.parent, playerData.bomb, my.friendlyBullets);
            }
        }

        this.dx = 0;
        this.dy = 0;
    };

    /**
     * Checks if the player collides at given coordinates.
     * @param x X coordinate.
     * @param y Y coordinate.
     * @returns {boolean} True if (x, y) is inside the player's hitbox.
     */
    BHell_Player.prototype.checkCollision = function (x, y) {
        var dx = Math.abs(this.x - x);
        var dy = Math.abs(this.y - y);
        return (dx < this.hitboxW / 2 && dy < this.hitboxH / 2);
    };

    /**
     * Checks if the player collides at given coordinates.
     * @param x X coordinate.
     * @param y Y coordinate.
     * @returns {boolean} True if (x, y) is inside the player's hitbox.
     */
    BHell_Player.prototype.checkGrazing = function (x, y) {
        var dx = Math.abs(this.x - x);
        var dy = Math.abs(this.y - y);
        return (dx * dx + dy * dy < this.grazingRadius * this.grazingRadius);
    };

    /**
     * Updates the player's sprite, position and bomb. Awards bonus lives if the score allows it.
     */
    BHell_Player.prototype.update = function () {
        my.BHell_Sprite.prototype.update.call(this);
        this.move();
        if (this.bomb != null) {
            this.bomb.update();
        }

        // Make the immortality last 5 seconds.
        if (this.immortal && this.immortalTimeout >= 0) {
            this.immortalTimeout++;

            if (this.immortalTimeout > my.immortalTimeout) {
                this.immortal = false;
                this.immortalTimeout = -1;
                this.opacity = 255;
            }
        }

        this.updateBonusLives();
    };

    /**
     * Checks if the score has reached the plugin's life_bonus_first and life_bonus_next thresholds and awards
     * bonus lives accordingly.
     *
     * For example, if the bonus thresholds are set to 20000 and 50000, the player is awarded a life at: 20000 points,
     * 70000 (20000 + 50000), 120000 (20000 + 50000 + 50000), 170000, etc.
     */
    BHell_Player.prototype.updateBonusLives = function () {
        if (my.lifeBonusFirst > 0 && my.lifeBonusNext > 0) {
            if ($gameBHellResult.score >= this.bonusLives * my.lifeBonusNext + my.lifeBonusFirst) {
                this.bonusLives++;
                this.lives++;
            }
        }
    };

    /**
     * Sets a destination for the player.
     * @param x X coordinate of the destination.
     * @param y Y coordinate of the destination.
     */
    BHell_Player.prototype.moveTo = function (x, y) {
        this.dx = x - this.x;
        this.dy = y - this.y;
    };

    /**
     * Sets a relative destination for the player.
     * @param dx Relative x coordinate of the destination.
     * @param dy Relative y coordinate of the destination.
     */
    BHell_Player.prototype.deltaTo = function (dx, dy) {
        this.dx = dx;
        this.dy = dy;
    };

    /**
     * Sets a destination in steps (multiples of this.speed).
     * @param h Horizontal number of steps.
     * @param v Vertical number of steps.
     */
    BHell_Player.prototype.step = function (h, v) {
        this.dx += h * this.speed;
        this.dy += v * this.speed;
    };

    /**
     * Moves the player and all its emitters.
     * If the player has just been spawned, moves it towards the starting position, otherwise towards its destination
     * (set with moveTo(x, y) and/or step(h, v) ).
     *
     * To avoid a death loop during a respawn, the player will wait until the enemy bullets have disappeared from screen
     * (eventually destroying them after a five seconds timeout).
     */
    BHell_Player.prototype.move = function () {
        // If the player has just been spawned (outside the screen), move to the starting position.
        if (this.justSpawned === true) {
            // Wait until the enemy bullets are cleared. If they are not cleared after five seconds, it destroys them.
            if (my.enemyBullets.length === 0) {
                var dy = Graphics.height * 0.9 - this.y;

                if (Math.abs(dy) <= this.speed * 0.3) {
                    this.y = Math.round(Graphics.height * 0.9);
                    this.immortalTimeout = 0;
                    my.controller.stopShooting = false;
                    this.justSpawned = false;
                    this.usingTouch = false;
                    this.dx = 0;
                    this.dy = 0;
                    if (this.spawn_se != null) {
                        AudioManager.playSe(this.spawn_se);
                    }
                }
                else {
                    this.y += Math.max(dy, -this.speed * 0.3);
                }
            }
            else {
                this.bulletTimeout++;
                if (this.bulletTimeout > my.bulletTimeout) {
                    my.controller.destroyEnemyBullets();
                }
            }
        }
        else if (this.won === true) {
            var dx = Graphics.width / 2 - this.x;
            this.y -= this.speed;
            if (dx > 0) {
                this.x += Math.min(dx, this.speed);
            }
            else if (dx < 0) {
                this.x += Math.max(dx, -this.speed);
            }

            if (this.y < -this.height) {
                my.playing = false;
                if (this.victory_se != null) {
                 my.playing |= AudioManager._seBuffers != null && AudioManager._seBuffers.filter(function(audio) {
                     return audio.isPlaying();
                 }).length !== 0;
                }
                if (my.victoryMe != null) {
                    my.playing |= AudioManager._meBuffer != null && AudioManager._meBuffer.isPlaying();
                }
            }
        }
        else { // Otherwise move towards the destination.
            var angle = Math.atan2(this.dy, this.dx);

            if (this.dx > 0) {
                this.x += Math.min(this.dx, Math.cos(angle) * this.speed);
                this.dx -= Math.min(this.dx, Math.cos(angle) * this.speed);
            }
            else if (this.dx < 0) {
                this.x += Math.max(this.dx, Math.cos(angle) * this.speed);
                this.dx -= Math.max(this.dx, Math.cos(angle) * this.speed);
            }

            if (this.dy > 0) {
                this.y += Math.min(this.dy, Math.sin(angle) * this.speed);
                this.dy -= Math.min(this.dy, Math.sin(angle) * this.speed);
            }
            else if (this.dy < 0) {
                this.y += Math.max(this.dy, Math.sin(angle) * this.speed);
                this.dy -= Math.max(this.dy, Math.sin(angle) * this.speed);
            }

            // Prevent the player from leaving the screen.
            if (this.x < this.width / 2) {
                this.x = this.width / 2;
            }
            if (this.x > Graphics.width - this.width / 2) {
                this.x = Graphics.width - this.width / 2;
            }
            if (this.y < this.height / 2) {
                this.y = this.height / 2;
            }
            if (this.y > Graphics.height - this.height / 2) {
                this.y = Graphics.height - this.height / 2;
            }
        }

        // Move the emitters. A forEach can be used since the emitters' array won't change dynamically.
        this.emitters.forEach(e => {
            e.move(this.x, this.y);
        });
    };

    /**
     * Enables/disables shooting for each helper. If the player has just been spawned, shooting is disabled.
     * @param t True to enable shooting.
     */
    BHell_Player.prototype.shoot = function (t) {
        this.emitters.forEach(e => {
            e.shooting = t && this.justSpawned === false;
        });
    };

    /**
     * If there are bombs available, launch one.
     */
    BHell_Player.prototype.launchBomb = function () {
        if (!this.justSpawned && this.bombs !== 0 && !this.bomb.isActive()) {
            this.bombs--;
            $gameBHellResult.bombsUsed++;
            this.bomb.activate(this.x, this.y);
        }
    };

    /**
     * If the player is not immortal, destroy it and respawn it.
     * @param t If true, death was caused by a bullet, otherwise by a crash.
     */
    BHell_Player.prototype.die = function (t) {
        if (this.immortal === false) {
            if (t && this.bombs > 0) { // If a bullet hits the player and there are bombs available, launch one, but waste all of them.
                this.launchBomb();
                this.bombs = 0;
            }
            else {
                this.lives--;
                $gameBHellResult.livesLost++;
                my.controller.stopShooting = true;
                this.bombs = this.startingBombs;
                this.bomb.deactivate();
                my.explosions.push(new my.BHell_Explosion(this.x, this.y, this.parent, my.explosions));
                if (this.death_se != null) {
                    AudioManager.playSe(this.death_se);
                }
                this.spawn();
            }
        }
    };

    /**
     * Plays the victory SE and moves the player to the top of the map.
     */
    BHell_Player.prototype.win = function () {
        if (this.won === false) {
            this.won = true;
            if (this.victory_se != null) {
                AudioManager.playSe(this.victory_se);
            }

            if (my.victoryMe != null) {
                AudioManager.playMe(my.victoryMe);
            }
        }
    };

    /**
     * Spawn the player outside the screen.
     */
    BHell_Player.prototype.spawn = function () {
        this.x = Math.round(Graphics.width / 2);
        this.y = Math.round(Graphics.height + this.height);
        this.justSpawned = true;
        this.bulletTimeout = 0;
        this.immortal = true;
        this.immortalTimeout = -1;
        this.opacity = 100;
    };

    return my;
}(BHell || {}));