Source: GirlManager.js

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

/**
 * @typedef GirlManager.girlLayer
 * @property {string} girl - ID of girl this layer is for
 * @property {boolean} beforeClothes - If this layer should be added before the girl's clothes
 * @property {string} layerID - ID of layer to be added. Ex: 'Queen-Layer-Bukkake' layer ID is 'Bukkake'
 */

/**
 * The GirlManager class lets you check and change anything related to the girls.
 * @class GirlManager
 */
class GirlManager {
    constructor() {
        this._girls = ['Queen', 'Suki', 'Esxea', 'Scarlett', 'Ardura'];

        /**
         * currentGirl is the currently selected girl. It's the girl displayed on the HUD.
         * @name GirlManager#currentGirl
         * @type {string}
         */
        this.currentGirl = 'Queen';
    }

    /**
     * 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 {boolean} [input] - If true, clicking the container will display the girl's image
     * @returns Phaser.GameObjects.Container
     */
    getGirlContainer(context, girlID, input) {
        let girlObj = GAME.girl.getGirl(girlID);

        let container = context.add.container();

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

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

            container.buildGirl();
            return container;
        };

        container.setClothes = function (clothesID) {
            let clothesObj = GAME.clothes.getClothes(clothesID);
            let girl = clothesObj.Girl;

            if (GAME.clothes.getGirlDye(clothesID) !== false) {
                container.getByName('clothes').setTexture(girl + "-Clothes-" + clothesObj.ID + "-" + GAME.clothes.getGirlDye(clothesObj.ID));
            } else {
                container.getByName('clothes').setTexture(girl + "-Clothes-" + clothesObj.ID);
            }
            container.getByName('clothes').clothID = clothesObj.ID;
        };

        container.setDye = function (clothesID, dyeID) {
            let clothesObj = GAME.clothes.getClothes(clothesID);
            let girl = clothesObj.Girl;

            if (dyeID !== false) {
                container.getByName('clothes').setTexture(girl + "-Clothes-" + clothesObj.ID + "-" + dyeID);
            } else {
                container.getByName('clothes').setTexture(girl + "-Clothes-" + clothesObj.ID);
            }
        };

        container.buildGirl = function () {
            container.removeAll(true);

            container.add(context.add.image(0, 0, girlObj.ID + "-Body-" + girlObj.Body).setOrigin(0.5, 1).setName('body'));
            container.add(context.add.image(0, 0, girlObj.ID + "-Face-" + girlObj.Face).setOrigin(0.5, 1).setName('face'));

            if (GAME.clothes.getGirlDye(girlObj.Clothes) !== false) {
                container.add(context.add.image(0, 0, girlObj.ID + "-Clothes-" + girlObj.Clothes + "-" + GAME.clothes.getGirlDye(girlObj.Clothes)).setOrigin(0.5, 1).setName('clothes'));
            } else {
                container.add(context.add.image(0, 0, girlObj.ID + "-Clothes-" + girlObj.Clothes).setOrigin(0.5, 1).setName('clothes'));
            }
            container.getByName('clothes').clothID = girlObj.Clothes;

            for (let i in girlObj.Layers) {
                if (girlObj.Layers[i].beforeClothes === true) {
                    let clothesIndex = container.getIndex(container.getByName('clothes'));
                    container.addAt(context.add.image(0, 0, girlObj.ID + "-Layer-" + girlObj.Layers[i].layerID).setOrigin(0.5, 1).setName(girlObj.Layers[i].layerID), clothesIndex - 1)
                } else {
                    container.add(context.add.image(0, 0, girlObj.ID + "-Layer-" + girlObj.Layers[i].layerID).setOrigin(0.5, 1).setName(girlObj.Layers[i].layerID));
                }
            }

            if (girlObj.Naked === true) {
                container.getByName('clothes').setVisible(false);
            }

            if (input === true) {
                if (container.input) {
                    container.input.hitArea.setSize(container.getByName('body').getBounds().width, container.getByName('body').getBounds().height);
                } else {
                    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) {
                                    imageLayers.push({Key: child.texture.key});
                                }
                            });
                            GAME.viewImage(imageLayers);
                        })
                }
            }
        };

        container.buildGirl();

        globalEvents.on('refreshGirls', container.buildGirl);

        container.on(Phaser.GameObjects.Events.DESTROY, () => {
            globalEvents.off('refreshGirls', container.buildGirl)
        });

        return container;
    }

    /**
     * Sets the girl as naked or not
     * Clothes stats are not affected, the girl is technically still wearing the clothes
     * @method setNaked
     * @memberOf GirlManager
     * @instance
     * @param {string|Array<string>} girlID - ID or array of IDs to set
     * @param {boolean} boolean
     */
    setNaked(girlID, boolean) {
        if (typeof girlID === "object") {
            for (let i of girlID) {
                gameData.character[i].Naked = boolean;
            }
        } else {
            gameData.character[girlID].Naked = boolean;
        }
        globalEvents.emit('refreshGirls');
    }

    /**
     * Returns if the girl is naked or not
     * @method getNaked
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @returns {boolean}
     */
    getNaked(girlID) {
        return gameData.character[girlID].Naked;
    }

    /**
     * Changes the girl's body texture
     * @method setBody
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @param {string} key
     */
    setBody(girlID, key) {
        gameData.character[girlID].Body = key;
        globalEvents.emit('refreshGirls');
    }

    /**
     * Changes the girl's face texture
     * @method setFace
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @param {string} key
     */
    setFace(girlID, key) {
        gameData.character[girlID].Face = key;
        globalEvents.emit('refreshGirls');
    }

    /**
     * Adds a layer to the girl's body. Automatically creates a {@link GirlManager.girlLayer}
     * @method addLayer
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @param {GirlManager.girllayer} girlLayer
     */
    addLayer(girlID, beforeClothes, layerID) {
        gameData.character[girlID].Layers.push({
            girl: girlID,
            beforeClothes: beforeClothes,
            layerID: layerID
        });
        globalEvents.emit('refreshGirls');
    }

    /**
     * Removes layer from girl's body
     * @method removeLayer
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @param {string} layerID
     */
    removeLayer(girlID, layerID) {
        gameData.character[girlID].Layers.splice(gameData.character[girlID].Layers.findIndex(layer => layer.layerID === layerID), 1);
        globalEvents.emit('refreshGirls');
    }

    /**
     * Removes all layers on girl's body
     * @method removeAllLayers
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     */
    removeAllLayers(girlID) {
        gameData.character[girlID].Layers = []
        globalEvents.emit('refreshGirls');
    }


    /**
     * 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) {
        if (amount > 0) {
            gameData.character[girl].Stamina -= amount;
            globalEvents.emit('loseStamina', {girl: girl, staminaAmount: -amount});
        }
    }

    /**
     * Adds the amount to the girl's current stamina
     * @method gainStamina
     * @instance
     * @memberOf GirlManager
     * @param girl
     * @param amount
     * @param [overMax=true] - If the added stamina should go over her maximum stamina amount
     */
    gainStamina(girl, amount, overMax) {
        if (overMax === undefined) {
            overMax = true;
        }
        if (amount > 0) {
            if (overMax === true) {
                gameData.character[girl].Stamina += amount;
            } else if (gameData.character[girl].Stamina < GAME.girl.getMaxStamina(girl)) {
                gameData.character[girl].Stamina += amount;
                if (gameData.character[girl].Stamina > GAME.girl.getMaxStamina(girl)) {
                    gameData.character[girl].Stamina = GAME.girl.getMaxStamina(girl);
                }
            }
            globalEvents.emit('gainStamina', {girl: girl, staminaAmount: amount});
        }
    };

    /**
     * Unlocks a girl. Checks to see if there is available space in the house
     * @method unlockGirl
     * @instance
     * @memberOf GirlManager
     * @param {string} girl
     * @param {boolean} force - Will raise the house capacity if it is maxed
     * @returns {boolean}
     */
    unlockGirl(girl, force) {
        if (GAME.checkMax() === true && gameData.character[girl].Unlocked === false && force === true) {
            gameData.houseCapacity += 1;
            gameData.houseUpgrades += 1;
            gameData.character[girl].Unlocked = true;
            globalEvents.emit('refresh');
            return false;
        }

        if (gameData.character[girl].Unlocked === true) {
            return false;
        }
        gameData.character[girl].Unlocked = true;
        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
     * @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;
        gameData.character[girlName].Face = GAME.clothes.getClothes(newClothes).Face;
        gameData.character[girlName].Body = GAME.clothes.getClothes(newClothes).Body;
        GAME.girl.setNaked(girlName, false);

        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 girl's total recovery each turn of a battle
     * @method getRecovery
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @returns {number}
     */
    getRecovery(girlID) {
        return 0.5 + gameData.character[girlID].Bonus.Recovery;
    }

    /**
     * Returns an array of body parts that are unlocked
     * @method getUnlockedBodyParts
     * @memberOf GirlManager
     * @instance
     * @returns {Array<GirlManager.bodyPart>}
     */
    getUnlockedBodyParts() {
        let array = [];

        for (let i in skills) {
            if (gameData.body[skills[i]] !== false) {
                array.push(skills[i]);
            }
        }

        return array;
    }

    /**
     * Returns an array of girls that are unlocked
     * @method getUnlocked
     * @memberOf GirlManager
     * @instance
     * @param {boolean} [hasStamina] - Only get girls that have stamina
     * @returns {Array}
     */
    getUnlocked(hasStamina) {
        hasStamina = hasStamina || false;
        let girlArray = [];
        let characters = GAME.girl.getGirls();

        for (let char in characters) {
            if (gameData.character[characters[char]].Unlocked === true) {
                if (hasStamina === true) {
                    if (GAME.girl.getStamina(characters[char]) > 0) {
                        girlArray.push(characters[char]);
                    }
                } else {
                    girlArray.push(characters[char]);
                }
            }
        }

        return girlArray;
    }

    /**
     * Sets the stamina for a girl, ignores all rules
     * @method setStamina
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @param {number} amount
     */
    setStamina(girlID, amount) {
        gameData.character[girlID].Stamina = amount;
        globalEvents.emit('gainStamina', {girl: girlID, staminaAmount: 0});
    }

    /**
     * Checks if a girl is unlocked
     * @method isUnlocked
     * @memberOf GirlManager
     * @instance
     * @param {string} girlID
     * @returns {boolean}
     */
    isUnlocked(girlID) {
        return gameData.character[girlID].Unlocked;
    }
}