Source: GirlManager.js

/**
 * Body parts throughout the whole game use these keys
 * @typedef GirlManager.bodyPart
 * @type 'Hands' | 'Feet' | 'Throat' | 'Tits' | 'Pussy' | 'Anal'
 */

/**
 * Function called after fucking a client
 * @callback GirlManager.passiveCallback
 * @param {string} girl
 * @param {object} client
 * @param {GirlManager.bodyPart} bodyPart
 */

/**
 * The GirlManager class lets you check and change anything related to the girls.
 * @class GirlManager
 */
class GirlManager {
    constructor() {
        this._girls = ['Queen', 'Suki', 'Esxea', 'Scarlett'];
        /**
         * currentGirl is the currently selected girl. It's the girl displayed on the HUD.
         * @name GirlManager#currentGirl
         * @type {string}
         */
        this.currentGirl = 'Queen';
        this._girlPassives = {
            "Queen": function (girl, client, bodyPart) {
                if (chance.bool({likelihood: 25}) === true) {
                    let stolenGold = chance.integer({min: client.Gold / 4, max: client.Gold / 2});
                    GAME.addGold(stolenGold);
                    GAME.notify('Queen stole ' + stolenGold + ' gold from the client!');
                }
            },
            "Suki": function (girl, client, bodyPart) {
                if (chance.bool({likelihood: 20}) === true) {
                    GAME.girl.gainExp(girl, bodyPart, GAME.client.getClientExp(client.ID));
                    GAME.notify('Suki gained double the experience from that job!');
                }
            },
            "Esxea": function (girl, client, bodyPart) {
                if (chance.bool({likelihood: 25}) === true) {
                    GAME.girl.gainStamina('Esxea', GAME.client.getClientObj()[client.ID].Stamina);
                    GAME.notify('Esxea used no stamina when fucking ' + GAME.client.getClientObj()[client.ID].Name + '!');
                }
            },
            "Scarlett": function (girl, client, bodyPart) {
                if (chance.bool({likelihood: 25}) === true) {
                    GAME.client.pushClient(client.ID);
                    GAME.notify('Scarlett\'s client stayed for more!');
                }
            }
        }
    }

    /**
     * Returns the gameData object for the girl
     * @method getGirl
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @returns {object}
     */
    getGirl(girlID) {
        return gameData.character[girlID];
    }

    /**
     * Returns an array with the ids of the girls
     * @method getGirls
     * @instance
     * @memberOf GirlManager
     * @return {array<string>}
     */
    getGirls() {
        return this._girls;
    }

    /**
     * @method getGirlContainer
     * @memberOf GirlManager
     * @instance
     * @param {Phaser.Scene} context
     * @param {string} girlID
     * @param {object} [layers] - If you want to change any of the layers manually
     * @param {string} [layers.body]
     * @param {string} [layers.face]
     * @param {string} [layers.clothes]
     * @param {array} [layers.additional] - An optional array of textures that will be stacked on top of the finished image. This is for anything extra like cum
     * @param {boolean} [input] - If true, clicking the container will display the girl's image
     * @returns Phaser.GameObjects.Container
     */
    getGirlContainer(context, girlID, layers, input) {
        let girlObj = GAME.girl.getGirl(girlID);

        layers = layers || {};
        layers.body = layers.body || "Default";
        layers.face = layers.face || "Neutral";
        layers.clothes = layers.clothes || girlObj.Clothes;
        layers.additional = layers.additional || [];

        let container = context.add.container();

        container.add(context.add.image(0, 0, girlID + "-Body-" + layers.body).setOrigin(0.5, 1).setName('body'));
        container.add(context.add.image(0, 0, girlID + "-Face-" + layers.face).setOrigin(0.5, 1).setName('face'));
        container.add(context.add.image(0, 0, girlID + "-Clothes-" + layers.clothes).setOrigin(0.5, 1).setName('clothes').setTint(gameData.clothes[layers.clothes].Tint[0]));
        container.getByName('clothes').clothID = layers.clothes;

        for (let i in layers.additional) {
            container.add(context.add.image(0, 0, layers.additional[i]).setOrigin(0.5, 1).setName(layers.additional[i]));
        }

        if (input === true) {
            container.setInteractive({
                hitArea: container.getByName('body').getBounds(),
                hitAreaCallback: Phaser.Geom.Rectangle.Contains,
                useHandCursor: true
            })
                .on('pointerup', () => {
                    let imageLayers = [];
                    container.iterate((child) => {
                        if (child.visible === true) {
                            let tint = 0xFFFFFF;
                            if (child.hasOwnProperty('clothID')) {
                                tint = gameData.clothes[child.clothID].Tint[0];
                            }
                            imageLayers.push({Key: child.texture.key, Tint: tint});
                        }
                    });
                    GAME.viewImage(imageLayers);
                })
        }

        container.setNaked = function (boolean) {
            container.getByName('clothes').setVisible(!boolean);
            return container;
        };

        container.setBody = function (key) {
            container.getByName('body').setTexture(key);
            return container;
        };

        container.setFace = function (key) {
            container.getByName('face').setTexture(key);
            return container;
        };

        container.setClothes = function (clothID) {
            let girl = GAME.clothes.getAllClothes()[clothID].Girl;

            container.getByName('clothes').setTexture(girl + "-Clothes-" + clothID).setTint(gameData.clothes[clothID].Tint[0]);
            container.getByName('clothes').clothID = clothID;
            return container;
        };

        container.addAdditional = function (key) {
            container.add(context.add.image(0, 0, key).setOrigin(0.5, 1).setName('additional' + layers.additional.length + 1));
            layers.additional.push(key);
            return container;
        };

        container.removeAdditional = function (key) {
            container.remove(container.getByName(key), true);
            layers.additional.splice(layers.additional.indexOf(key), 1);
            return container;
        };

        container.removeAllAdditionals = function () {
            for (let i = layers.additional.length - 1; i >= 0; i--) {
                container.remove(container.getByName(layers.additional[i]), true);
                layers.additional.splice(i, 1);
            }
            return container;
        };

        container.setGirl = function (girlID) {
            let girlObj = GAME.girl.getGirl(girlID);

            container.removeAllAdditionals();

            container.iterate((child) => {
                child.setVisible(true);
            });

            container.setBody(girlID + "-Body-" + girlObj.Body);
            container.setFace(girlID + "-Face-Neutral");
            container.setClothes(girlObj.Clothes);
            return container;
        };

        return container;
    }

    /**
     * Returns the girl's clothes
     * @method getGirlClothes
     * @instance
     * @memberOf GirlManager
     * @param {string} girlID
     * @returns {string}
     */
    getGirlClothes(girlID) {
        return gameData.character[girlID].Clothes;
    }

    /**
     * Subtracts the amount from the girl's current stamina
     * @method loseStamina
     * @instance
     * @memberOf GirlManager
     * @param girl
     * @param amount
     */
    loseStamina(girl, amount) {
        gameData.character[girl].Stamina -= amount;
        globalEvents.emit('loseStamina', {girl: girl, amount: amount});
    }

    /**
     * Adds the amount to the girl's current stamina
     * @method gainStamina
     * @instance
     * @memberOf GirlManager
     * @param girl
     * @param amount
     */
    gainStamina(girl, amount) {
        gameData.character[girl].Stamina += amount;
        globalEvents.emit('gainStamina', {girl: girl, amount: amount});
    };

    /**
     * Unlocks a girl. Checks to see if there is available space in the house
     * @method unlockGirl
     * @instance
     * @memberOf GirlManager
     * @param {string} girl
     * @returns {boolean}
     */
    unlockGirl(girl) {
        if (GAME.checkMax() === true) {
            return false;
        } else {
            if (gameData.character[girl].Unlocked === true) {
                return false;
            }
            gameData.character[girl].Unlocked = true;
            globalEvents.emit('refresh');

            return true;
        }
    }

    /**
     * Returns the total stamina for all the girls
     * @method getTotalStamina
     * @instance
     * @memberOf GirlManager
     * @returns {number}
     */
    getTotalStamina() {
        let total = 0;

        for (let i in gameData.character) {
            if (gameData.character[i].Unlocked === true) {
                total += gameData.character[i].Stamina;
            }
        }
        return parseFloat(total.toFixed(2));
    }

    /**
     * Returns the stamina of a girl
     * @method getStamina
     * @instance
     * @memberOf GirlManager
     * @memberOf Gameplay
     * @param {String} [girl]
     * @return {number}
     */
    getStamina(girl) {
        return parseFloat(gameData.character[girl].Stamina.toFixed(2));
    }

    /**
     * Returns the limit of stamina a girl can have
     * @method getMaxStamina
     * @instance
     * @memberOf GirlManager
     * @param {String} girl
     */
    getMaxStamina(girl) {
        return parseFloat((gameData.character[girl].MaxStamina + Math.floor(GAME.girl.getStaminaGain(girl)) + gameData.character[girl].Bonus.Stamina).toFixed(2))
    };

    /**
     * @private
     * @method getStaminaGain
     * @instance
     * @memberOf GirlManager
     * @param girl
     * @returns {number}
     */
    getStaminaGain(girl) {
        return Math.sqrt(gameData.character[girl].GuysFucked) * STAMINA_GAIN;
    }

    /**
     * Returns the exp of a girl
     * @method getGirlExp
     * @instance
     * @memberOf GirlManager
     * @param {String} girl
     * @param {GirlManager.bodyPart} body
     * @returns {number}
     */
    getGirlExp(girl, body) {
        return gameData.character[girl][body];
    }

    /**
     * Returns the bonus object for the girl
     * @private
     * @method getGirlBonus
     * @memberOf GirlManager
     * @instance
     * @param {String} girl
     * @returns {object}
     */
    getGirlBonus(girl) {
        return gameData.character[girl].Bonus;
    }

    /**
     * Returns the girl's body part level
     * @method getGirlLevel
     * @memberOf GirlManager
     * @instance
     * @param {String} girl
     * @param {GirlManager.bodyPart} body
     * @param {boolean} [bonus=true] - Bonus is the extra levels you get from clothes/items/etc. Set this to false if you just want the raw level of the girl
     * @returns {number}
     */
    getGirlLevel(girl, body, bonus) {
        if (bonus === undefined) {
            bonus = true;
        }
        if (bonus === true) {
            return GAME.getLevel(this.getGirlExp(girl, body)) + this.getGirlBonus(girl)[body];
        } else {
            return GAME.getLevel(this.getGirlExp(girl, body));
        }
    }

    /**
     * Returns the total level of the girl. All of the body part levels combined
     * @method getTotalLevel
     * @memberOf GirlManager
     * @instance
     * @param {String} girl
     * @param {boolean} bonus - Bonus is the extra levels you get from clothes/items/etc. Set this to false if you just want the raw level of the girl
     */
    getTotalLevel(girl, bonus) {
        if (bonus === undefined) {
            bonus = true;
        }
        let total = 0;

        for (let i in skills) {
            if (gameData.body[skills[i]] !== false) {
                total += this.getGirlLevel(girl, skills[i], bonus);
            }
        }

        return total;
    }

    /**
     * Equips clothes based on the clothes ID
     * @method equipClothes
     * @memberOf GirlManager
     * @instance
     * @param {string} clothID
     */
    equipClothes(clothID) {
        if (gameData.clothes[clothID].Unlocked === false) {
            return null;
        }
        let girlName = GAME.clothes.getAllClothes()[clothID].Girl;
        let currentClothes = gameData.character[girlName].Clothes;
        let newClothes = clothID;

        if (GAME.girl.getTotalLevel(girlName, false) < GAME.clothes.getAllClothes()[newClothes].Level) {
            GAME.notify("She does not have a high enough total level to wear that!");
            return null;
        }

        for (let skill in skills) {
            if (GAME.clothes.getAllClothes()[currentClothes].Stats.hasOwnProperty(skills[skill])) {
                gameData.character[girlName].Bonus[skills[skill]] -= GAME.clothes.getAllClothes()[currentClothes].Stats[skills[skill]];
            }
            if (GAME.clothes.getAllClothes()[newClothes].Stats.hasOwnProperty(skills[skill])) {
                gameData.character[girlName].Bonus[skills[skill]] += GAME.clothes.getAllClothes()[newClothes].Stats[skills[skill]];
            }
        }
        if (GAME.clothes.getAllClothes()[currentClothes].Stats.hasOwnProperty("Stamina")) {
            gameData.character[girlName].Bonus.Stamina -= GAME.clothes.getAllClothes()[currentClothes].Stats.Stamina;
            GAME.girl.loseStamina(girlName, GAME.clothes.getAllClothes()[currentClothes].Stats.Stamina);
        }
        if (GAME.clothes.getAllClothes()[newClothes].Stats.hasOwnProperty("Stamina")) {
            gameData.character[girlName].Bonus.Stamina += GAME.clothes.getAllClothes()[newClothes].Stats.Stamina;
            GAME.girl.gainStamina(girlName, GAME.clothes.getAllClothes()[newClothes].Stats.Stamina);
        }
        if (GAME.clothes.getAllClothes()[currentClothes].Stats.hasOwnProperty("Recovery")) {
            gameData.character[girlName].Bonus.Recovery -= GAME.clothes.getAllClothes()[currentClothes].Stats.Recovery;
        }
        if (GAME.clothes.getAllClothes()[newClothes].Stats.hasOwnProperty("Recovery")) {
            gameData.character[girlName].Bonus.Recovery += GAME.clothes.getAllClothes()[newClothes].Stats.Recovery;
        }
        gameData.character[girlName].Clothes = newClothes;

        globalEvents.emit('refreshGirls');
    }

    /**
     * Increases the amount of guys a girl has fucked by the amount
     * The amount of guys a girl has fucked determines the stamina gain, so this is useful if you create a gang bang quest and want to reward the player with extra points
     * @method fuckGuys
     * @memberOf GirlManager
     * @instance
     * @param {string} girl
     * @param {number} amount
     */
    fuckGuys(girl, amount) {
        gameData.character[girl].GuysFucked += amount;
    }

    /**
     * Adds the exp to a girl's body part
     * @method gainExp
     * @memberOf GirlManager
     * @instance
     * @param {string} girl
     * @param {GirlManager.bodyPart} type
     * @param {number} exp
     */
    gainExp(girl, type, exp) {
        if (exp === 0) {
            return null;
        }

        let before = GAME.girl.getGirlLevel(girl, type, false);
        let after;

        gameData.character[girl][type] += exp;

        if (gameData.character[girl][type] > GAME.getExp(MAX_LEVEL)) {
            gameData.character[girl][type] = GAME.getExp(MAX_LEVEL);
        }

        after = GAME.girl.getGirlLevel(girl, type, false);

        if (before !== after) {
            globalEvents.emit('levelUp', {girl: girl, type: type});
        }

        globalEvents.emit('gainExp', exp);
    }

    /**
     * Splits the exp amongst the girls in the array
     * @method splitExp
     * @memberOf GirlManager
     * @instance
     * @param {Array<string>} girls - Array of girl ids
     * @param {number} totalExp - This number will be divided by the amount of girls and then by the amount of body parts
     * @param {Array<GirlManager.bodyPart>} bodyParts - Array of body parts
     */
    splitExp(girls, totalExp, bodyParts) {
        let eachGirlGets = totalExp / girls.length;
        let eachBodyPartGets = Math.ceil(eachGirlGets / bodyParts.length);

        for (let i in girls) {
            for (let j in bodyParts) {
                GAME.girl.gainExp(girls[i], bodyParts[j], eachBodyPartGets);
            }
        }
    }

    /**
     * Returns the current passive function of a girl
     * @method getGirlPassive
     * @memberOf GirlManager
     * @instance
     * @param {string} girl
     * @returns {GirlManager.passiveCallback}
     */
    getGirlPassive(girl) {
        return this._girlPassives[girl];
    }

    /**
     * Overwrites the current passive function for the girl with a new function
     * @method setGirlPassive
     * @memberOf GirlManager
     * @instance
     * @param {string} girl
     * @param {GirlManager.passiveCallback} callback
     */
    setGirlPassive(girl, callback) {
        this._girlPassives[girl] = callback;
    }

    /**
     * Calls the girl's passive function
     * @method girlPassive
     * @memberOf GirlManager
     * @instance
     * @param {string} girl
     * @param {string} client
     * @param {GirlManager.bodyPart} bodyPart
     */
    girlPassive(girl, client, bodyPart) {
        client = GAME.client.getClientObj()[client];
        this._girlPassives[girl](girl, client, bodyPart);
    }

    /**
     * Opens up a menu where you choose a girl. Returns the girl's id or false if the user did not select any girl
     * @method chooseGirl
     * @memberOf GirlManager
     * @instance
     * @returns {Promise<string>}
     */
    chooseGirl() {
        return new Promise((resolve) => {
            game.scene.start('ChooseGirl', {pauseAllScenes: true});
            game.scene.getScene('ChooseGirl').events.once('shutdown', (scene, data) => {
                resolve(data.answer);
            })
        });
    }
}