code:
"use strict";

    var Girl = function(id) {
        if (typeof(id) === 'object') {
            this.id = 0;
        } else {
        if (id == undefined)
            id = getAsmSys_time();
        }

        this.id = id;

        this.body = new Body();
        this.clear();

        Object.preventExtensions(this.sexp);
        Object.preventExtensions(this.skill);
        Object.preventExtensions(this.attr);
        Object.preventExtensions(this);

        if (typeof(id) === 'object') {
            var keys = Object.keys(this);
            for (var i = 0; i< keys.length; i++) {
                switch(keys[i]) {
                    case 'body':
                        this.body = cast(BodyType, id[keys[i]]);
                        break;
                    case 'job':
                        this.job = Data.getJob(id.job); // avoid setJob logic
                        break;
                    default:
                        if (this.hasOwnProperty(keys[i]) && id.hasOwnProperty(keys[i]))
                            this[keys[i]] = deepclone(id[keys[i]]);
                }
            }

            var keys = Object.keys(id);
            for (var i = 0; i < keys.length; i++)
                if (id.hasOwnProperty(keys[i]) && ! this.hasOwnProperty(keys[i]))
                    console.log("orphaned Girl property " + keys[i]);
        }
    }

    Object.defineProperty(Girl.prototype, "sex", {
        get: function() { throw new Error("Using old Girl.sex instead of Girl.body.gender"); },
        set: function(v) { throw new Error("Using old Girl.sex instead of Girl.body.gender"); },
    });

    // can't call any prototype functions in here due to the way things work.
    Girl.prototype.clear = function() {
        this.attr = {}
        this.attr.willpower = 1;
        this.attr.courage = 1;
        this.attr.confidence = 1;
        this.attr.wit = 1;
        this.attr.charm = 1;

        this.backstory = 'I have no backstory';

        if (this.body == undefined) {
            this.body = new Body();
        }

        this.busy = 0;

        this.corruption = 0;
        this.experience = 0;
        this.extra_description = 0;
        this.father = 0;
        this.mother = 0;

        this.health = 100;
        this.hide = 0;
        this.job = Data.getJob("Rest");
        this.lastname = 'FIXME';
        this.level = 0;
        this.lost_skill_314 = 0;
        this.loyalty = 0;
        this.lust = 0;
        this.mission = 0;

        this.name = 'FIXME';
        this.obedience = 0;
        this.potion_effect = 0;
        this.prisoner_feeding = 1;
        this.rules = 11111111111;

        this.skill = {}
        this.skill.allure = 0;
        this.skill.bodycontrol = 0;
        this.skill.combat = 0;
        this.skill.magicarts = 0;
        this.skill.management = 0;
        this.skill.service = 0;
        this.skill.sex = 0;
        this.skill.survival = 0;

        this.skillmax = 0;
        this.skillpoints = 0;

        this.spell_effect = 0;

        this.stress = 0;
        this.trait = 0;
        this.trait_known = 0;
        this.trait2 = 0;
        this.unique = 0;
        this.where_sleep = 1;

        this.sexp = {};
        this.sexp.oral = 0;
        this.sexp.petting = 0;
        this.sexp.pussy = 0;
        this.sexp.anal = 0;
        this.sexp.titjob = 0;
        this.sexp.cock = 0;
    }

    /* add 'risky' sex type if pregnancy is possible.  contraception is checked here. */

    Girl.prototype.sexAct = function(sexact, particpant) {
        var pregnancy = false;
        if (!isArray(sexact))
            sexact = [ sexact ];

        if (sexact.indexOf('petting') != -1)
            this.sexp.petting++;

        if (sexact.indexOf('oral') != -1)
            this.sexp.oral++;

        if (sexact.indexOf('pussy') != -1)
            this.sexp.pussy++;

        if (sexact.indexOf('risky') != -1) {
            /* maybe pregnant? */
            if (this.body.contraception == 0 && this.body.pregnancy == 0 && Math.tossCoin(0.25)) {
                this.newPregnancy(particpant);
                pregnancy = true;
            }
        }

        if (sexact.indexOf('anal') != -1)
            this.sexp.anal++;

        if (sexact.indexOf('titjob') != -1)
            this.sexp.titjob++;

        if (sexact.indexOf('cock') != -1) // for futa/boys?
            this.sexp.cock++;

        return pregnancy;
    }

    /* Figure out how sexually experienced she is */
    Girl.prototype.calcSexp = function() {
        var xp = 0;

        xp += this.sexp.petting * 2;

        xp += this.sexp.oral * 4;

        xp += this.sexp.titjob * 4;

        xp += this.sexp.cock * 4;

        xp += this.sexp.pussy * 5;

        xp += this.sexp.anal * 5;

        return xp;
    }

    Girl.prototype.requestSex = function(sexact) {
        if (!isArray(sexact))
            sexact = [ sexact ];

        var reluctance = 0;

        if (this.sexp.petting == 0 && sexact.indexOf('petting') != -1)
            reluctance += 30;

        if (this.sexp.oral == 0 && sexact.indexOf('oral') != -1)
            reluctance += 50;

        if (this.sexp.anal == 0 && sexact.indexOf('anal') != -1)
            reluctance += 120;

        if (this.sexp.pussy == 0 && sexact.indexOf('pussy') != -1)
            reluctance += 150;

        if (this.trait2 == Data.mental_trait.indexOf('Prude'))
            reluctance += 75;

        // bisexual is a big swing, pardon the pun.
        if (sexact.indexOf('bisexual') != -1) {
            if (this.trait == Data.sex_trait.indexOf('bisexual'))
                reluctance -= 50;
            else
                reluctance += 50;
        }

        if (this.trait != Data.sex_trait.indexOf('deviant') && sexact.indexOf('incest') != -1)
            reluctance += 100;

        if (this.trait == Data.sex_trait.indexOf('frigid'))
            reluctance += 75;

        reluctance += this.attr.willpower * 20;

        /* things that make her more willing. */

        /* is she experienced? */
        reluctance -= this.calcSexp();

        if (this.trait == Data.sex_trait.indexOf('nymphomaniac'))
            reluctance -= 100;

        if (this.hasEffect("coerced"))
            reluctance -= 100;

        reluctance -= this.lust/3;

        reluctance -= this.obedience;

        reluctance -= this.loyalty*3;

        reluctance -= this.corruption;

        reluctance = Math.round(reluctance);

        var rq = '';
        for (var i = 0; i < sexact.length; i++)
            rq += (i == 0 ? "" : " ") + sexact[i];

        console.log(this.name + " " + this.lastname + " requestSex: " + reluctance + " (" + rq + ")");

        if (reluctance > 20)
            return false;

        if (reluctance > 10) {
            ToText(this.name + " considers it, but decides against it.

");
            return false;
        }

        if (reluctance > 0) {
            ToText(this.name + " is very tempted.

");
            return false;
        }

        return true;
    }

    Girl.prototype.WillRecounter = function() {
        var attrsum = Math.ceil(this.attr.courage / 20) + Math.ceil(this.attr.confidence / 20)
                    + Math.ceil(this.attr.wit / 20) + Math.ceil(this.attr.charm / 20);

        if (attrsum <= 4) {
            this.attr.willpower = 0;
        } else if (attrsum <= 6) {
            this.attr.willpower = 1;
        } else if (attrsum <= 8) {
            this.attr.willpower = 2;
        } else if (attrsum <= 10) {
            this.attr.willpower = 3;
        } else {
            if (attrsum >= 15 && (this.attr.courage >= 81 || this.attr.confidence >= 81 || this.attr.wit >= 81 || this.attr.charm >= 81)) {
                this.attr.willpower = 6;
            } else if (attrsum >= 13 && (this.attr.courage >= 81 || this.attr.confidence >= 81 || this.attr.wit >= 81 || this.attr.charm >= 81)) {
                this.attr.willpower = 5;
            } else if (attrsum >= 11 && (this.attr.courage >= 61 || this.attr.confidence >= 61 || this.attr.wit >= 61 || this.attr.charm >= 61)) {
                this.attr.willpower = 4;
            } else {
                this.attr.willpower = 3;
            }
        }
        this.skillmax = 3 + Math.round(this.attr.wit / 10) + Math.floor(this.level / 5);
        if (this.body.age <= 1) {
            this.skillmax++;
        }
        if (this.isRace("gnome")) {
            this.skillmax = this.skillmax + Math.round(this.skillmax / 2);
        }

        /* bump level up to minimum */
        var skills = this.skillsum();

        if (skills > this.level) {
            this.level = skills;
            this.skillpoints = 0;
        } else {
            this.skillpoints = Math.min(15, this.level) - skills;
        }
    }

    Girl.prototype.mindbroken = function(count) {
        if (this.attr.willpower > 0 && !this.hasMentalTrait("Iron Will")) {
            if (typeof(count) !== "number" || count < 0) count = 6;
            ToText("

Due to constant stress " + this.name + '' + " had a severe breakdown.

");
            while (count >= 1) {
                switch(Math.tossCase(3)) {
                case 0:
                    this.attr.courage = Math.max(this.attr.courage - 4, 0);
                    break;
                case 1:
                    this.attr.confidence = Math.max(this.attr.confidence - 4, 0);
                    break;
                case 2:
                    this.attr.wit = Math.max(this.attr.wit - 4, 0);
                    break;
                case 3:
                    this.attr.charm = Math.max(this.attr.charm - 4, 0);
                    break;
                }
                count--;
            }
            this.WillRecounter()
        }
    }

    Girl.prototype.skillsum = function() {
        var skills = 0;
        for (var i in this.skill) {
            if (this.skill.hasOwnProperty(i))
                skills += this.skill[i];
        }
        return skills;
    }

    Girl.prototype.value = function() {
        var v = Math.round(50 * this.body.race.marketValue);
        v = v * this.body.face.beauty.marketValue;
        v = v * this.body.age.marketValue;
        return v;
    }

    Girl.prototype.mutate = function() {
        this.stress += 30;

        Data.RandomMutation()(this);

        if (this.body.toxicity >= 60) {
            this.body.toxicity = this.body.toxicity - 25;
        }
    }

    Girl.prototype.randomizeAttrs = function(will) {
        this.attr.courage = 1;
        this.attr.confidence = 1;
        this.attr.wit = 1;
        this.attr.charm = 1;

        will = will * 2 + Math.tossDice(0, 1);
        var prime_attr = Math.tossCase(3);
        while (will > 0) {
            switch (Math.tossCase(3)) {
            case 0:
                if ((this.attr.courage < 5 && prime_attr != 0) || this.attr.courage < 2)
                    this.attr.courage++;
                break;
            case 1:
                if ((this.attr.confidence < 5 && prime_attr != 1) || this.attr.confidence < 2)
                    this.attr.confidence++;
                break;
            case 2:
                if ((this.attr.wit < 5 && prime_attr != 2) || this.attr.wit < 2)
                    this.attr.wit++;
                break;
            case 3:
                if ((this.attr.charm < 5 && prime_attr != 3) || this.attr.charm < 2)
                    this.attr.charm++;
                break;
            }
            will--;
        }
        this.attr.courage = this.attr.courage * 20 - 10;
        this.attr.confidence = this.attr.confidence * 20 - 10;
        this.attr.wit = this.attr.wit * 20 - 10;
        this.attr.charm = this.attr.charm * 20 - 10;

        this.WillRecounter();
    }

    Girl.prototype.generate = function(race, age, will) {
        this.body = new Body();

        if (race === undefined)
            this.body.race = Data.listRaces().random();
        else {
            this.setRace(race);
        }

        // FIXME should generate be the place the furry setting is actually checked?
        if (Game.settings.furry == 0) {
            if (this.isRace("beastkin cat")) {
                this.setRace('halfkin cat');
            } else if (this.isRace("beastkin fox")) {
                this.setRace('halfkin fox');
            } else if (this.isRace("beastkin wolf")) {
                this.setRace('halfkin wolf');
            } else if (this.isRace("bunny")) {
                this.setRace('halfkin bunny');
            } else if (this.isRace("tanuki")) {
                this.setRace('halfkin tanuki');
            }
        }

        if (age === undefined) {
            this.body.age = Math.tossDice(0, 2);
        } else {
            this.body.age = age;
        }
        if (Game.settings.loli == 0 && this.body.age == 0) {
            this.body.age = Math.tossDice(1, 2);
        }
        if (this.body.age == 3)
            this.body.age = 2;

        if (will === undefined) {
            will = Math.tossDice(1, 3);
        }

        this.body.face.beauty = Math.tossDice(0, 100);
        if (Math.tossCoin(0.4) && this.body.age != 0) {
            this.body.piercings = this.body.piercings.toString().substr(0, 1 - 1) + '2' + this.body.piercings.toString().substr(1);
        }

        var chance = 1;
        if (this.body.age == 0) {
            chance = 0.2;
        } else if (this.body.age == 1) {
            chance = 0.5;
        } else {
            chance = 0.7;
        }
        if (Math.tossCoin(chance)) {
            this.body.pussy.virgin = false;
        }

        this.body.cock.size = 0;
        this.body.balls.size = 0;
        if (Game.settings.futanari == 1) {
            if (Math.tossCoin(0.04)) {
                this.body.cock.size = Math.tossDice(1, 2);
            }
            if (Game.settings.futa_balls == 1 && this.body.cock.size > 0) {
                this.body.balls.size = Math.tossDice(1, 3);
            }
            if (this.body.age < 2 && this.body.cock.size > 1) {
                this.body.cock.size--;
            }
            if (this.body.age < 2 && this.body.balls.size > 1) {
                this.body.balls.size--;
            }
        }

        this.body.hair.length = BodyType.c.hair.c.length.random();
        this.body.hair.style = BodyType.c.hair.c.style.random();

        if (this.body.hair.length == 0) {
            this.body.hair.style = 0;
        }

        this.body.skin.color = Math.tossDice(0, 4);

        var maxtits = 3;
        if (this.body.age == 1) {
            maxtits = 4;
        } else if (this.body.age == 2) {
            maxtits = 5;
        }
        this.body.chest.size = Math.tossDice(1, maxtits);

        var maxass = 3;
        if (this.body.age == 1) {
            maxass = 4;
        } else if (this.body.age == 2) {
            maxass = 5;
        }
        this.body.ass.size = Math.tossDice(1, maxass);

        // Race specific tweaks.
        this.body.race.generateName(this);
        this.body.race.generateBody(this.body);

        // Generate attributes
        this.randomizeAttrs(will);

        if (Math.tossCoin(0.3)) {
            this.trait = Math.tossIndex(Data.sex_trait.length);
        }

        if (Math.tossCoin(0.3)) {
            this.trait = Math.tossIndex(Data.mental_trait.length);
        }

        this.backstory = Data.backstory1.random();

        if (this.body.age == 2) {
            /* can't use [].random() here, because the skills are based
             * on the backstory. */

            var tmp = Math.tossIndex(Data.backstory2.length);
            this.backstory += Data.backstory2[tmp];

            if (tmp == 0) {
                this.skill.combat++;
            } else if (tmp == 1) {
                this.skill.bodycontrol++;
            } else if (tmp == 2) {
                this.skill.survival++;
            } else if (tmp == 3) {
                this.skill.management++;
            } else if (tmp == 4) {
                this.skill.service++;
            } else if (tmp == 5) {
                this.skill.allure++;
            } else if (tmp == 6) {
                this.skill.sex++;
            } else if (tmp == 7) {
                this.skill.magicarts++;
            }
        }

        this.WillRecounter();
    }

    Girl.prototype.willpowerDesc = [
        " She's a broken mess(0); more of an animal than a person. ",
        " She's a pushover(1) and easily submits to others. ",
        " She's meek(2) and barely fights back, even when hard-pressed. ",
        " Overall, she's reserved(3) and tries to retain her rights when pushed. ",
        " She has a certain degree of willfulness(4) going for her when she's dealing with others. ",
        " She's headstrong(5) and always ready to oppose something she does not agree with. ",
        " The strength of her personality is incontestable. In her eyes, she's nothing less than a Heroine(6). ",
    ];

    Girl.prototype.courageDesc = new TierArray({
        80: " Heroic(5)  ",
        60: " Brave(4)  ",
        40: " Restrained(3)  ",
        20: " Cautious(2)  ",
        0: " Coward(1)  ",
    });

    Girl.prototype.confidenceDesc = new TierArray({
        80: " Self-assured(5)  ",
        60: " Assertive(4)  ",
        40: " Balanced(3)  ",
        20: " Indecisive(2)  ",
        0: " Doormat(1)  ",
    });

    Girl.prototype.witDesc = new TierArray({
        80: " Prodigy(5)  ",
        60: " Smart(4)  ",
        40: " Reasonable(3)  ",
        20: " Dim(2)  ",
        0: " Dunce(1)  ",
    });

    Girl.prototype.charmDesc = new TierArray({
        80: " Crowd's favorite(5)  ",
        60: " Eye-catching(4)  ",
        40: " Memorable(3)  ",
        20: " Likable(2)  ",
        0: " Dull(1)  ",
    });

    Girl.prototype.sexualTraitDesc = [
        '',
        " She's a natural submissive, uncontrollably getting off on being used and dominated. ",
        " She's a nymphomaniac, possessing little self control when it comes to sex, and actively seeking it out. ",
        " She's a deviant and can enjoy even the most unusual and immoral activities. ",
        " She's frigid and uninterested in sex in general. ",
        " She's frivolous and will enjoy random sexual encounters and having multiple partners at a time. ",
        " She's masochistic and gets off from pain and torture. ",
        " She's bisexual and will enjoy sleeping with any gender. ",
        " She's monogamous and strongly detests sexual activity with anyone but her chosen partner. ",
    ];

    Girl.prototype.mentalTraitDesc = [
        '',
        " She's a coward and has a difficult time handling herself in physical confrontations. ",
        " She has an iron will, and is much more resistant to enormous stress. ",
        " She's naturally passive and will obey without much resistance.  ",
        " She's clingy, and will easily grow attached to others.  ",
        " She's something of a prude, and will not easily take part in sexual acts.  ",
        " She's a bit of a loner, and pays little attention to the opinions of others. ",
        " She's rather frail mentally, and her mind is easily damaged by abuse. ",
        " She has a very pliable personality, and will be easily shaped by your influence. ",
        " She has grown devoted to you, and has come to see you like a father to her. ",
        " She has become quite slutty, and now enjoys sex to a great degree. ",
    ];

    Girl.prototype.spellDesc = [
    '',
    " She's under an entrancement spell.",
    " She's under a coercion spell.",
    ];

    Girl.prototype.potionDesc = [
    '',
    " She's sensitized by a potion.",
    " She's desensitized by a potion.",
    " She's more fertile.",
    " She's extremely fertile.",
    ];

    Girl.prototype.obedienceArray = new TierArray({
        0: " She barely pays any attention to you, as if demonstrating her independence. ",
        20: " She prefers to avoid looking at you and reacts poorly to your commands. ",
        40: " She shows some respect to you, but it's clear that she forces herself to do it. ",
        60: " She is pretty considerate and tries to appeal to you, showing that your opinion is important to her. ",
        80: " She grasps your every word and gives you all of the attention that she can muster. ",
    });

    Girl.prototype.lustArray = new TierArray({
        80: " She seems very aroused and is looking at you passionately. ",
        60: " She looks aroused, and is restless. ",
        40: " She looks slightly horny and provocative. ",
        20: " She seems to be a bit playful. ",
        0:  " She looks calm and uninterested in sex. ",
    });

    Girl.prototype.stressArray = new TierArray({
        0:  " Overall {{name}} acts  content and lively. ",
        20: " {{name}} looks slightly down. ",
        40: " {{name}} looks somewhat  depressed. ",
        60: " {{name}} looks  really stressed. ",
        80: " {{name}} looks on the verge of breaking. ",
    });

    Girl.prototype.healthArray = new TierArray({
        80: " She looks reasonably healthy.",
        60: " She looks injured.",
        40: " Her condition looks poor ",
        20: " She looks very weak and morbid.",
        0:  "  She's unconscious.",
    });

    Girl.prototype.profile = {
        resident: [ 'detailed', 'genitals', 'sexual', 'attributes', 'traits', 'brand', 'clothing', 'standings', 'status', 'pregnancy', 'skills', 'experience' ],
        enemy: [ 'detailed', 'pregnancy',  ],
        market: [ 'detailed', 'genitals', 'pregnancy', ],
        shortdesc: [ 'shortdesc' ],
        oneline: [ 'oneline' ],
        descriptres: [ 'detailed', 'genitals', 'sexual' ],
        hidedesc: [ 'hidedesc', 'attributes', 'traits', 'brand', 'clothing', 'standings', 'status', 'pregnancy', 'skills', 'experience' ],
        raceonly: [ 'race' ],
        startinggirl: [ 'name', 'detailed', 'genitals', 'sexual', 'attributes', 'traits', 'skills' ],
        pcinfo: [ 'name', 'detailed', 'genitals', 'sexual', 'you', 'siblings' ],
    };

    Girl.prototype.describe = function(args) {

        var profile = [];

        if ('profile' in args)
            profile = this.profile[args.profile];
        else if ('requests' in args)
            profile = args.requests;

        if (profile.length == 0)
            return 'Girl.describe(): no valid profile selected
';

        // turn profile into a dict
        var pnew = {}
        profile.forEach(function(v) { pnew[v]=true; });

        profile = pnew;

        var replacements = {};
        replacements.merge = function(b) {
            for(var key in b)
                if (b.hasOwnProperty(key))
                    this[key] = b[key]
        }

        replacements.merge({
            name: this.name,
            fullname: (this.name + ' ' + this.lastname).trim(),
            female: 'female', // FIXME
            race:    "" + this.body.race.name + "",
            bodyshape:   this.body.shape.type.longDesc,

            age:     this.body.age.longDesc,
            agerace: this.body.age.raceDesc,
            ageshort: this.body.age.shortDesc,

            pretty:  this.body.face.beauty.longDesc,
            pretty2: this.body.face.beauty.shortDesc,

            hair:    this.body.hair.describe(),
            eyes:    "" + this.body.eyes.color + ". ",
            skinverbose: this.body.skin.describe(),

            tits:    this.body.chest.size.longDesc,
            tits1:   this.body.chest.size.shortDesc2,
            tits2:   this.body.chest.size.shortDesc3,
            extratits: this.body.chest.describe(true),
            extratits2: this.body.chest.describe(false),
            ears:    this.body.ears.type.shortDesc,
            earsverbose: this.body.ears.type.longDesc,
            tailverbose: this.body.tail.type.longDesc,

            earpiercing: this.body.earPiercing[this.body.piercings.toString().charAt(1)],
            nosepiercing: this.body.nosePiercing[this.body.piercings.toString().charAt(1)],
            lippiercing: this.body.lipPiercing[this.body.piercings.toString().charAt(2)],
            tonguepiercing: this.body.tonguePiercing[this.body.piercings.toString().charAt(3)],
            eyebrowpiercing: this.body.eyebrowPiercing[this.body.piercings.toString().charAt(4)],
            breastpiercing: this.body.showBreastPiercing(),
            navelpiercing: this.body.navelPiercing[this.body.piercings.toString().charAt(6)],
            labiapiercing: this.body.labiaPiercing[this.body.piercings.toString().charAt(7)],
            clitpiercing: this.body.clitPiercing[this.body.piercings.toString().charAt(8)],
            cockpiercing: this.body.cockPiercing[this.body.piercings.toString().charAt(9)],

            horns: this.body.horns.type.longDesc,
            wings: this.body.wings.type.longDesc,

            lactating: this.body.lactation == 1 ? " It seems that she's lactating." : "",
            vagina:   this.body.pussy.virgin.longDesc,
            ass:     this.body.ass.size.longDesc,
            cock:    this.body.cock.describe(),
            balls:   this.body.balls.size.longDesc,
            hair_color: this.body.hair.color,
            titalter: this.body.chest.nipples.elastic.longDesc,
            eyecolor: this.body.eyes.color,
            eyetype:  this.body.eyes.type.longDesc,
            eyealter:  this.body.eyes.pupils.longDesc,
            eyesize:  this.body.eyes.size.longDesc,
            eyeref:  this.body.eyes.reflectors.longDesc,
            eyeacu:  this.body.eyes.acuity.longDesc,
        });

        // secondary expansion
        Object.forEach(replacements, function(k, v) {
            if (typeof(v) == 'string') {
                replacements[k] = simple_template(v, replacements);
            }
            if (replacements[k] === undefined || replacements[k] == 'undefined')
                replacements[k] = "{{Y}" + k + "}";
        });

        var out = ' ';

        if (profile.shortdesc) {
            out += "{{fullname}}, {{race}}";
        }

        if (profile.name)
            out += 'Her name is {{fullname}}.
';

        if ( profile.race ) {
            out += "She's a {{race}}.
";
        }

        if ( profile.oneline) {
            /* She's an exceptionally beautiful young teenage girl */
            /* She's a human teen, and is exceptionally beautiful. */
            out += "She's a {{pretty2}} {{race}} {{ageshort}}.
";
        }

        if ( profile.hidedesc ) {
            out += "She's a {{race}}.";
            out += "{{bodyshape}}";
            out += "{{age}}
";
            out += "Her face is {{pretty}}";
        } else if (profile.detailed) {
            out += "She's a {{race}}. {{bodyshape}} {{age}}

";
            if (profile.you)
                out += 'You are handsome.';
            else
                out += "Her face is {{pretty}}.";
            out += "{{hair}}";
            out += "Her eyes are {{eyecolor}}.{{eyetype}}{{eyealter}}{{eyesize}}{{eyeref}}{{eyeacu}}";
            out += "{{eyebrowpiercing}}";
            out += "{{nosepiercing}}";
            out += "{{lippiercing}}";
            out += "{{tonguepiercing}}";

            out += "{{horns}}";
            out += "{{earsverbose}}";
            out += "{{earpiercing}}";

            out += "{{skinverbose}}";
            out += "{{navelpiercing}}";
            out += "{{wings}}";
            out += "{{tailverbose}}";

            out += this.body.showAugmentations();
        }

        if (profile.genitals || profile.sexual) {
            out += "
{{tits}}";
            if (profile.genitals)
                out += "{{extratits}}{{titalter}}{{breastpiercing}}{{lactating}}";
            else
                out += "{{extratits2}}";
            out += "{{ass}}";
            if (profile.sexual) {
                if (this.body.pussy.size > 0)
                    out += "{{vagina}}";
            }
            if (profile.genitals) {
                out += "{{labiapiercing}}{{clitpiercing}}";
                out += "{{cock}}{{cockpiercing}}{{balls}}";
            }
        }

        if (profile.detailed && this.extra_description != 0)
            out += "

" + this.extra_description;

        if (profile.traits) {
            out += "

";
            if(this.trait_known == 1)
                out += this.sexualTraitDesc[this.trait];

            out += this.mentalTraitDesc[this.trait2];
        }

        if (profile.backstory)
            out += '

Backstory: ' + this.backstory;

        if (profile.siblings) {
            out += "

Father: "
            if (this.father <= 0)
                out += "Unknown";
            else if (this.father == 1)
                out += "You";
            else {
                for (var i = 0; i < Game.Girls.length; i++) {
                    if (this.father == Game.Girls[i].id) {
                        out += Game.Girls[i].describe({profile: 'shortdesc' });
                        break;
                    }
                }
            }

            out += "
Mother: "
            if (this.mother <= 0)
                out += "Unknown";
            else if (this.mother == 1)
                out += "You";
            else {
                for (var i = 0; i < Game.Girls.length; i++) {
                    if (this.mother == Game.Girls[i].id) {
                        out += Game.Girls[i].describe({profile: 'shortdesc' });
                        break;
                    }
                }
            }

            out += "
Siblings: "
            if ((this.father == -1 || this.mother == -2) && this != Game.player) {
                out += "You; ";
            }
            for (var i = 0; i < Game.Girls.length; i++) {
                if (Game.Girls[i] == this)
                    continue; // not our own sister.
                if ((this.father != 0 && (this.father == Game.Girls[i].father || this.father == Game.Girls[i].mother))
                 || (this.mother != 0 && (this.mother == Game.Girls[i].father || this.mother == Game.Girls[i].mother))) {
                    out += Game.Girls[i].describe({profile: 'shortdesc' }) + "; ";
                }
            }

            out += "
Children: "
            for (var i = 0; i < Game.Girls.length; i++) {
                if (this.id == Game.Girls[i].mother || this.id == Game.Girls[i].father) {
                    out += Game.Girls[i].describe({profile: 'shortdesc' }) + "; ";
                }
            }
            out += "
"
        }

        if (profile.brand)
            out += '
' + this.body.brand.type.longDesc + " She wears " + Data.clothing_name[this.body.clothing] + ". ";

        if (profile.standings) {
            out += '
' + this.obedienceArray.get(this.obedience);
        }
        if (profile.status) {
            out += '
' + this.stressArray.get(this.stress) + this.healthArray.get(this.health) + this.lustArray.get(this.lust);
            if (this.spell_effect)
                out += this.spellDesc[this.spell_effect];
            if (this.potion_effect)
                out += this.potionDesc[this.potion_effect];
        }

        if (profile.pregnancy && this.body.pregnancy > 0) {
            out += "
She appears to be pregnant. ";
            out += Body.prototype.pregnancyArray.get(this.body.pregnancy)
        }

        if (profile.attributes) {
            out += "
WillpowerWillpower is the summary of person’s nature. The more strong aspects they have, the stronger overall Willpower is. Low Willpower makes them easy to influence and manipulate, while strong Willpower cause them to rebound easily from your intimidation. Breaking someone’s Willpower is the most obvious solution to producing obedient slave, but beware: it is not hard to traumatize even tough person, but it’s very hard to refine feeble character.: ";

            out += this.willpowerDesc[this.attr.willpower] + "

";

            out += "CourageCourage represents how willing a person is to face any sort of threat. High courage allows them to venture into dangerous regions and overcome their fears. Low courage allows discipline through fear to be way more effective, but makes combat and adventure unbearable.: ";
            out += this.courageDesc.get(this.attr.courage);

            out += "
ConfidenceConfidence in one’s self represents how dependable a person is when put into a position of authority and must deal with other's opinions. High confidence is a must for any leadership or managerial role, otherwise their indecision will lead to poor performance. High confidence causes obedience to drop more quickly and refuse certain actions, while low confidence allows the slave to adopt new positions faster.  : ";

            out += this.confidenceDesc.get(this.attr.confidence);

            out += "
WitWit represents the cognitive capabilities of a person; how quickly they think, how fast they learn, and how proactive they are at this. A high wit allows for quicker training and learning while a low wit would make the person incompetent at some occupations. High wit also makes it harder to influence a slave's mind via magic.  : ";
            out += this.witDesc.get(this.attr.wit);

            out += "
CharmCharm represents how well a person is perceived by others. Charming people attract everyone's attention and are easily liked, while those of the opposite case are overlooked. A high charm considerably affects a slave's overall value, and their ability to entertain.  : ";
            out += this.charmDesc.get(this.attr.charm);

            out += '
';
        }

        if (profile.experience || profile.skills)
            out += "

";

        if (profile.experience)
            out += "Experience: " + this.experience + "/100  Level " + this.level + (profile.skills ? "
" : "

");

        if (profile.skills) {
            var count = 0;
            out += "Skills: (maximum potential: " + this.skillmax + ", she has " + this.skillpoints + " available.)
";
            var _map = {
                'allure':
                    "AllureAllure represents ability to attract other people by your apperance and actions.",
                'bodycontrol':
                    "Body ControlBody Control represents agility and dexterity of a person. It plays major role in both combat and entertainment.",

                'combat':
                    "CombatCombat is a primal fighting skill representing both offensive and defensive capabilities of a person as well as knowledge of battle tools. Main use — figthing.",
                'magicarts':
                    "Magic ArtsMagic arts represents various knowledge about nature and functions of magic. It is highly required to those actively working in such direction.",
                'management':
                    "ManagementManagement relates to the capability of holding administrative roles. Is a must for anyone involved in working with subordinates.",
                'service':
                    "ServiceService represents householding skills and etiquette. Affects house workers and escort.",
                'sex':
                    "Sexual ProficiencySexual Proficiency represents how good person can satisfy partner in sex.",
                'survival':
                    "SurvivalSurvival represents how good person can manage in the wild. Affects tracking, forage and hunting.",
            };
            Object.forEach(this.skill, function(k, v) {
                if (v > 0)
                    out += _map[k] + ": "+ Data.skill_adjective[v] + "
";
            });
        }

        out = simple_template(out, replacements);

        // fix a vowel to an vowel.
        out = out.replace(/ a ([aeiouAEIOU])/g, " an $1");

        // pronouns here.
        if (profile.you) {
            out = out
                .replace(/([^a-z])She's([^a-z])/g, "$1You're$2")
                .replace(/([^a-z])she's([^a-z])/g, "$1you're$2")

                .replace(/([^a-z])She"s([^a-z])/g, "$1You"re$2")
                .replace(/([^a-z])she"s([^a-z])/g, "$1you"re$2")

                .replace(/([^a-z])she([^a-z])/g, "$1you$2")
                .replace(/([^a-z])She([^a-z])/g, "$1You$2")

                .replace(/([^a-z])her([^a-z])/g, "$1your$2")
                .replace(/([^a-z])Her([^a-z])/g, "$1Your$2")

        // fix up some grammar.  Add more if they're found later.
                .replace(/([Yy])ou has/g, "$1ou have")
                .replace(/([Yy])ou is/g, "$1ou are")
                .replace(/([Yy])ou ({appear|wear|look|talk})s/g, "$1ou $2");

        } else if (this.gender == 2) { // gender = male not finished
            out = out
                .replace(/([^a-z])She's([^a-z])/g, "$1He's$2")
                .replace(/([^a-z])she's([^a-z])/g, "$1he's$2")

                .replace(/([^a-z])She"s([^a-z])/g, "$1He"re$2")
                .replace(/([^a-z])she"s([^a-z])/g, "$1he"re$2")

                .replace(/([^a-z])she([^a-z])/g, "$1he$2")
                .replace(/([^a-z])She([^a-z])/g, "$1He$2")

                .replace(/([^a-z])her([^a-z])/g, "$1his$2")
                .replace(/([^a-z])Her([^a-z])/g, "$1His$2")
                .replace(/Heroine/, "Hero");
            // do the same for guys
        }
        return out;
    }

    Girl.prototype.check_new_props = function() {
        var orig = new Girl();

        var keys = Object.keys(this);

        for (var i = 0; i < keys.length; i++) {
            if (this.hasOwnProperty(keys[i]) && !(keys[i] in orig))
                console.log("New key " + keys[i]);
        }

        keys = Object.keys(this.attr);
        for (var i = 0; i < keys.length; i++) {
            if (this.attr.hasOwnProperty(keys[i]) && !(keys[i] in orig.attr))
                console.log("New key attr." + keys[i]);
        }
        keys = Object.keys(this.body);
        for (var i = 0; i < keys.length; i++) {
            if (this.body.hasOwnProperty(keys[i]) && !(keys[i] in orig.body))
                console.log("New key body." + keys[i]);
        }
        keys = Object.keys(this.skill);
        for (var i = 0; i < keys.length; i++) {
            if (this.skill.hasOwnProperty(keys[i]) && !(keys[i] in orig.skill))
                console.log("New key skill." + keys[i]);
        }
    }

    Girl.prototype.setJob = function(id) {
        var job = Data.getJob(id);

        this.job.quitJob(this);
        job.takeJob(this);
        this.job = job;
    };

    Girl.prototype.getIndex = function() {
        return (Game.Girls.indexOf(this));
    }

    // delegate all sanity checks to Data.getRace()
    Girl.prototype.setRace = function(race) {
        this.body.race = Data.getRace(race);
    }

    Girl.prototype.orgasm = function() {
        var essence = this.body.race.essence;
        var extra = 0;
        if (this.isRace("drow"))
            extra++;

        this.lust = Math.max(this.lust - 20, 0);
        this.stress = Math.max(this.stress - 5, 0);

        var rnd = Math.tossDice(0, 101);
        if (rnd < (this.skill.sex * 5 + this.skill.allure)) {
            extra = Math.tossDice(4, 8);
            ToText("That was spectacular.

");
        } else if (rnd < (this.skill.sex * 15 + this.skill.allure * 5)) {
            extra = Math.tossDice(2, 4);
            ToText("That was really good.

");
        } else
            extra = Math.tossDice(1, 2);

        if (essence !== undefined) {
            ToText("You have collected " + extra + " " +
                    Data.item[essence].name + ".

");
            Game.inventory[essence] += extra;
        }
        console.log("Extra: " + extra);
        return extra;
    }

    Girl.prototype.possiblePregnancy = function(father, extrachance) {
        if (extrachance == undefined || !Number.isInteger(extrachance))
            extrachance = 0;

        if (this.body.pregnancy > 0)
            return false;

        var chance = 15;

        if (this.body.contraception == 1)
            chance -= 50;

        if (father != undefined)
            chance += 5 * father.body.balls.size;

        if (this.hasEffect("greater fertility"))
            chance += 65;
        else if (this.hasEffect("fertility"))
            chance += 25;

        chance += extrachance;

        // pregnancy is always possible, and always possible to avoid.
        console.log(this.name + " pregnancy chance: " + chance + "%");
        if (!Math.tossCoin(Math.withChance(chance)))
            return false;

        if (this.hasEffect("fertility"))
            this.removeEffect("fertility");

        if (this.hasEffect("greater fertility"))
            this.removeEffect("greater fertility");

        this.newPregnancy(father);

        if (father == undefined)
            ToText("" + this.name + " has become pregnant.

");
        else if (father == Game.player)
            ToText("" + this.name + " has become pregnant with your child.

");
        else
            ToText("" + this.name + " has become pregnant with " + father.name + "'s child.

");

        return true;
    }

    Girl.prototype.newPregnancy = function(father) {
        if (father == undefined) {// human father if undefined.
            father = new Girl();
            father.generate("human", 1);
            father.attr.willpower = Math.tossDice(1, 4);
        }
        var baby = new Girl();
        this.body.pregnancy = 1;
        this.body.pregnancyID = [ baby.id ];

        baby.mother = this.id;
        baby.father = father.id;

        var mr = this.body.race.name;
        var fr = father.body.race.name;

        var br = mr; // takes after mom, generally.

        if (Math.tossCoin() && (mr == 'human' || fr == 'human')) {
            if (mr == 'beastkin fox' || fr == 'beastkin fox')
                br = 'halfkin fox';
            else if (mr == 'beastkin cat' || fr == 'beastkin cat')
                br = 'halfkin cat';
            else if (mr == 'beastkin wolf' || fr == 'beastkin wolf')
                br = 'halfkin wolf';
            else if (mr == 'bunny' || fr == 'bunny')
                br = 'halfkin bunny';
            else if (mr == 'tanuki' || fr == 'tanuki')
                br = 'halfkin tanuki';
        }

        var takesafter = this; // mom.
        baby.setRace(br); // either a string or Race() object, both are acceptable

        /* takes after test */
        if (baby.body.race.name.match(/halfkin/)) {
            if (mr == 'human')
                takesafter = father;
        }

        baby.body.gender = Math.tossDice(0, 2);

        baby.body.shape.type = takesafter.body.shape.type;

        /* beauty somewhere between parents */
        var min = Math.min(father.body.face.beauty, this.body.face.beauty) - 5;
        var max = Math.max(father.body.face.beauty, this.body.face.beauty) + 10;
        baby.body.face.beauty = Math.tossDice(min, max);

        if (Math.tossCoin())
            baby.body.skin.color = this.body.skin.color;
        else
            baby.body.skin.color = father.body.skin.color;

        if (baby.body.race.name.match(/halfkin/))
            baby.body.skin.coverage = 0;
        else
            baby.body.skin.coverage = takesafter.body.skin.coverage;

        if (Math.tossCoin())
            baby.body.hair.color = this.body.hair.color;
        else
            baby.body.hair.color = father.body.hair.color;

        baby.body.hair.length = 3;
        baby.body.hair.style = 0;

        if (Math.tossCoin())
            baby.body.eyes.color = this.body.eyes.color;
        else
            baby.body.eyes.color = father.body.eyes.color;

        baby.body.augmentations = takesafter.body.augmentations;
        baby.body.tail.type = takesafter.body.tail.type;
        baby.body.ears.type = takesafter.body.ears.type;
        baby.body.wings.type = takesafter.body.wings.type;
        baby.body.horns.type = takesafter.body.horns.type;

        baby.body.chest.size = this.body.chest.size + Math.tossDice(-1, 1);
        baby.body.ass.size = Math.clamp(1, this.body.ass.size + Math.tossDice(-1, 1), 5);

        if (baby.body.gender.index != 1) {
            baby.body.cock.size = Math.tossDice(0, 1);
            if (Game.settings.futa_balls == 1) {
                baby.body.balls.size = Math.tossDice(0, 1);
            }
        }

        baby.loyalty = 25;
        baby.obedience = 80;
        baby.backstory = Data.backstory1[0];

        Game.baby.push(baby);
    }

    Girl.prototype.obedienceChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.min(Math.max(0, this.obedience + amt), 100);
        if (newval == Math.floor(newval))
            this.obedience = newval;
        else
            console.log("Failed to change obedience: " + amt);
    }

    Girl.prototype.loyaltyChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.min(Math.max(0, this.loyalty + amt), 100);
        if (newval == Math.floor(newval))
            this.loyalty = newval;
        else
            console.log("Failed to change loyalty: " + amt);
    }

    Girl.prototype.healthChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.min(this.health + amt, 100);
        if (newval == Math.floor(newval))
            this.health = newval;
        else
            console.log("Failed to change health: " + amt);
        if (this.health < 1)
            this.health = -10000; // prevent healing before the reaper comes.
    }

    Girl.prototype.corruptionChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.min(Math.max(0, this.corruption + amt), 100);
        if (newval == Math.floor(newval))
            this.corruption = newval;
        else
            console.log("Failed to change corruption: " + amt);
    }

    Girl.prototype.lustChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.min(Math.max(0, this.lust + amt), 100);
        if (newval == Math.floor(newval))
            this.lust = newval;
        else
            console.log("Failed to change lust: " + amt);
    }

    Girl.prototype.stressChange = function(amt, newmax) {
        amt = Math.round(amt);

        var newval = Math.max(0, this.stress + amt);
        if (newmax != undefined)
            newval = Math.min(newval, newmax);

        if (newval == Math.floor(newval))
            this.stress = newval;
        else
            console.log("Failed to change stress: " + amt);
    }

    Girl.prototype.toxicityChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.max(0, this.body.toxicity + amt);
        if (newval == Math.floor(newval))
            this.body.toxicity = newval;
        else
            console.log("Failed to change toxicity: " + amt);
    }

    Girl.prototype.experienceChange = function(amt) {
        amt = Math.round(amt);
        var newval = Math.max(0, this.experience + amt);
        if (newval == Math.floor(newval)) {
            while (newval >= 100) {
                ToText(" " + this.name + " has gained a level!");
                this.level++;
                if (this.level < 16)
                    this.skillpoints++;
                newval -= 100;
            }
            this.experience = newval;
        } else {
            console.log("Failed to change xp: " + amt);
        }
    }

    Girl.prototype.showSkillReqs = function(i) {
        var met = " class='green'";
        var need = " class='red'";
        var girl = this;

        if (i == undefined)
            i = 0;
        var requirements = Data.skillMap[i].requirements[this.skill[i]+1];

        var skill = function(x, y) {
            var ret = '';
            if (y == undefined)
                y = x.substr(0,1).toUpperCase() + x.substr(1);
            if (girl.skill[x] >= requirements[x])
                ret += ', ";
            else
                ret += ', ";
            ret += y + " " + Data.skill_adjective[requirements[x]] + "";
            return ret;
        }

        var attr = function(x) {
            var ret = '';
            var y = x.substr(0,1).toUpperCase() + x.substr(1);
            if (girl.attr[x] >= requirements[x])
                ret += ', ";
            else
                ret += ', ";

            ret += y + " " + Job.prototype.reqnames[x].get(requirements[x]) + "";
            return ret;
        }

        var reqs = '';
        for (var x in requirements) {
            switch(x) {
                case 'survival':
                case 'combat':
                case 'service':
                case 'allure':
                case 'management':
                    reqs += skill(x);
                    break;
                case 'bodycontrol':
                    reqs += skill('bodycontrol', 'Body Control');
                    break;
                case 'magicarts':
                    reqs += skill('magicarts', 'Magic Arts');
                    break;
                case 'sex':
                    reqs += skill('sex', 'Sexual Proficiency');
                    break;
                case 'willpower':
                case 'courage':
                case 'confidence':
                case 'wit':
                case 'charm':
                    reqs += attr(x);
                    break;
                case 'face':
                    if (this.body.face.beauty < requirements[x])
                        reqs += ", " + Job.prototype.reqnames[x].get(requirements[x]).capitalize() + " face ";
                    break;
            }
        }
        return reqs.replace(/^, /, "");
    }

    Girl.prototype.canTrainSkill = function(i) {
        if (this.skillmax - this.skillsum() <= 0 || this.skillpoints <= 0 || this.skill[i] == 5 || (this.skill[i]+1)*100 > Game.gold)
            return false;
        var ret = true;
        var requirements = Data.skillMap[i].requirements[this.skill[i]+1];
        var exceptions = Data.skillMap[i].exceptions;
        for (var x in requirements) {
            var skip = false;
            if (exceptions && (exceptions[0] == 'all' || exceptions[0] == x)) skip = true;
            switch(x) {
                case 'survival':
                case 'combat':
                case 'management':
                case 'service':
                case 'allure':
                case 'bodycontrol':
                case 'magicarts':
                case 'sex':
                    if (!skip && this.skill[x] < requirements[x]) {
                        ret = false;
                    }
                    break;
                case 'willpower':
                    if (this.attr[x] < requirements[x]) {
                        ret = false;
                    }
                    break;
                case 'courage':
                case 'confidence':
                case 'wit':
                case 'charm':
                    if (!skip && this.attr[x] < requirements[x]) {
                        ret = false;
                    }
                    break;
                case 'face':
                    if (this.body.face.beauty < requirements[x])
                        ret = false;
                    break;
                default:
                    break;
            }
        }
        return ret;
    }

    Girl.prototype.isRace = function(race) {
        if (Number.isInteger(race))
            throw new Error("Numeric races are not supported.");

        if (race instanceof Race)
            return this.body.race === race;

        if (Array.isArray(race)) {
            for (var i = 0; i < race.length; i++)
                if (this.isRace(race[i]))
                    return true;
            return false;
        }

        return this.body.race === Data.getRace(race);
    }

    Girl.prototype.sexualTraits = [
        { name: "none", desc: "A completely normal, ordinary girl" },
        { name: "submissive", desc: "Submissive, getting aroused while being dominated" },
        { name: "nymphomaniac", desc: "A Nympho, relishing the more perverse acts" },
        { name: "deviant", desc: "Deviant, having some bizarre fantasies" },
        { name: "frigid", desc: "Frigid, barely interested in sex" },
        { name: "frivolous", desc: "Frivolous, interested in sexual diversity" },
        { name: "masochistic", desc: "Masochistic, finding pleasure in pain" },
        { name: "bisexual", desc: "Bisexual, sometimes paying excessive attention to her own gender" },
        { name: "monogamous", desc: "Monogamous, considering only one partner to be her true love" },
    ];
    Girl.prototype.mentalTraits = [
        { name: "None", desc: "Pretty Normal" },
        { name: "Coward", desc: "A Coward, greatly stressed by phyisical confrontations" },
        { name: "Iron Will", desc: "Possess iron will and is quite resistant to stress" },
        { name: "Passive", desc: "Passive, and bend quickly" },
        { name: "Clingy", desc: "Clingy and rapidly becomes attached" },
        { name: "Prude", desc: "Prude and sexually shy" },
        { name: "Loner", desc: "A Loner, not easily influenced by others" },
        { name: "Frail", desc: "Frail, taking stress badly" },
        { name: "Pliable", desc: "Pliable, having some raw potential" },
        { name: "Devoted", desc: "Devoted to you" },
        { name: "Slutty", desc: "Slutty, enjoying sex to a great degree" },
    ];

    Girl.prototype.hasSexTrait = function(trait) {
        if (Number.isInteger(trait))
            return this.trait == trait;

        if (Array.isArray(trait)) {
            for (var i = 0; i < trait.length; i++)
                if (this.hasSexTrait(trait[i]))
                    return true;
            return false;
        }

        var num = Data.sex_trait.indexOf(trait);
        if (num == -1)
            throw new Error("Trait \"" + trait + "\" is unknown.");

        return this.trait == num;
    }

    Girl.prototype.addSexTrait = function(trait) {
        if (Number.isInteger(trait)) {
            this.trait = trait;
            return;
        }

        var num = Data.sex_trait.indexOf(trait);
        if (num == -1)
            throw new Error("Trait \"" + trait + "\" is unknown.");

        this.trait = num;
    }

    Girl.prototype.hasMentalTrait = function(trait) {
        if (Number.isInteger(trait))
            return this.trait2 == trait;

        if (Array.isArray(trait)) {
            for (var i = 0; i < trait.length; i++)
                if (this.hasMentalTrait(trait[i]))
                    return true;
            return false;
        }

        var num = Data.mental_trait.indexOf(trait);
        if (num == -1)
            throw new Error("Trait \"" + trait + "\" is unknown.");

        return this.trait2 == num;
    }

    Girl.prototype.addMentalTrait = function(trait) {
        if (Number.isInteger(trait)) {
            this.trait2 = trait;
            return;
        }

        var num = Data.mental_trait.indexOf(trait);
        if (num == -1)
            throw new Error("Trait \"" + trait + "\" is unknown.");

        this.trait2 = num;
    }

    Girl.prototype.hasJob = function(job) {
        if (job instanceof Job)
            return job === this.job;

        if (Array.isArray(job)) {
            for (var i = 0; i < job.length; i++)
                if (this.hasJob(job[i]))
                    return true;
            return false;
        }

        return this.job === Data.getJob(job);
    }

    Girl.prototype.jobName = function() {
        return this.job.name;
    }

    Girl.prototype.getJob = function() {
        return this.job;
    }

    // Provide the onclick you want.
    // You need to properly filter the girl in the calling function.
    Girl.prototype.displaySelect = function(onclick) {
        var out = '';
        out += "" + this.name + " " + this.lastname + " ";
        out += "" + this.body.race.name + " ";
        out += "" + this.job.name + " ";
        out += "Select";
        return out;
    }

    /* what what is {girl} to {this}? */
    Girl.prototype.relationshipTo = function(girl) {
        if ((this.father != 0 && this.father == girl.father) &&
                (this.mother != 0 && this.mother == girl.mother)) {
            if (this.gender == 'male')
                return 'brother'
            else
                return "sister";
        }

        if ((this.father != 0 && this.father == girl.father) ||
                (this.mother != 0 && this.mother == girl.mother)) {
            if (this.gender == 'male')
                return 'step brother'
            else
                return "step sister";
        }

        if (this.id == girl.father || this.id == girl.mother) {
            if (this.gender == 'male')
                return "son";
            else
                return "daughter";
        }

        if (this.father == girl.id)
            return "father";

        if (this.mother == girl.id)
            return "mother";

        return "";
    }

    Girl.prototype.relationshipName = function(girl) {
        var rel = this.relationshipTo(girl);
        if (rel == "")
            return girl.name;

        if (this == Game.player)
            return "your " + rel;

        if (this.body.gender == 'male')
            return "his " + rel;
        else
            return "her " + rel;
    }

    Girl.prototype.hasEffect = function(effect) {
        if (Array.isArray(effect)) {
            for (var i = 0; i < effect.length; i++)
                if (this.hasEffect(effect[i]))
                    return true;
            return false;
        }

        switch (effect.toLowerCase()) {
            case "stimulant":
            case "sensitized":
                return this.potion_effect == 1;
            case "deterrance":
            case "desensitized":
                return this.potion_effect == 2;
            case "entranced":
                return this.spell_effect == 1;
            case "coerced":
                return this.spell_effect == 2;
            case "fertility":
                return this.potion_effect == 3;
            case "greater fertility":
                return this.potion_effect == 4;
            default:
                throw new Error("unknown effect name: " + effect);
        }
        return false;
    }

    Girl.prototype.addEffect = function(effect) {
        switch (effect.toLowerCase()) {
            case "stimulant":
            case "sensitized":
                this.potion_effect = 1;
                break;
            case "deterrance":
            case "desensitized":
                this.potion_effect = 2;
                break;
            case "entranced":
                this.spell_effect = 1;
                break;
            case "coerced":
                this.spell_effect = 2;
                break;
            case "fertility":
                this.potion_effect = 3;
                break;
            case "greater fertility":
                this.potion_effect = 4;
                break;
            default:
                throw new Error("unknown effect name: " + effect);
        }
    }

    Girl.prototype.removeEffect = function(effect) {
        if (!this.hasEffect(effect))
            return false;

        switch (effect.toLowerCase()) {
            case "stimulant":
            case "sensitized":
                this.potion_effect = 0;
                break;
            case "deterrance":
            case "desensitized":
                this.potion_effect = 0;
                break;
            case "entranced":
                this.spell_effect = 0;
                break;
            case "coerced":
                this.spell_effect = 0;
                break;
            case "fertility":
                this.potion_effect = 0;
                break;
            case "greater fertility":
                this.potion_effect = 0;
                break;
            default:
                throw new Error("unknown effect name: " + effect);
        }
        return true;
    }

Test.tests.girls = {}
var t = Test.tests.girls;

t.makeAllRaces = function(name, callback, level) {
    var sub = {};

    forEach(Data.race, function(k, v) {
        var func = function () {
            var girl;

            for (var j = 0; j < 10; j++) {
                girl = new Girl();
                girl.generate(k);

                if ( girl.name == "FIXME" || girl.lastname == "FIXME" ||
                     girl.body.eyes.color == "FIXME" || girl.body.hair.color == "FIXME" )
                    throw new Error("Some FIXMEs left");
            }

            return Test.tryMutationsOn(girl).apply(this, arguments) && Test.tryItemsOn(girl).apply(this, arguments);
        };
        sub[v.name] = func;
    });

    return Test.runTestTree(sub, name, callback, level);
}


code:
"use strict";

var Help = {
    topic: {},
    addTopic: function(b) {
        this.topic[unixName(b.name)] = b;
   },
};

var HelpTopic = function(name, text) {
    this.name = name;
    this.text = text;
    Help.addTopic(this);
}

new HelpTopic("Pregnancy and consequences", ""
    + "Pregnancy is the usual result of vanilla sex ending in internal ejaculation. To prevent it, your best bet is to keep girls on contraceptives, which currently is free of consequences. If a girl gets pregnant you may want to use a certain item to stop pregnancy, although that may damage and stress her. After some time, pregnancy will result in lactation.

"
    + "Pregnancy lasts a couple of weeks, after which you will be prompted on what you want to do with baby. Basic option will only allow you to give baby away as you can't reliably raise it on your own. More advanced option — accelerate its growth so you will be able to use it just as any servant. Although, if it's a male, you will be forced to permanently change its gender to futanari or female first.

"
    + "Children inherit some of their look from parents, partly inheriting assets and get greater average maximum amount of potential skills. Race is nearly always will be inherited from mother though. While a girl is pregnant, other interactions may be possible.

");

new HelpTopic("Apiary", ""
    + "The Apiary is managed by your lab assistant. It is primarily used to utilize your servants in the process of the constant automated production of mana or special ingredients. As with farms, the constant pleasure and overwhelming sensations will quickly destroy a person's character, but its usefulness in the generation of manais undeniable.

"
    + "Your lab assistant's Magic Arts and Service skills will affect the Apiary's output."
);

new HelpTopic("Rules", ""
    + "Rules will be obeyed by your servants with no question and will mostly impact either their obedience or corruption. Most rules will likely damage their willpower in return so used it wisely to mold servants to the new lifestyle.");

new HelpTopic("Food", ""
    + "When starting out, your main concern will be food; you don't want to acquire too many residents or prisoners until you have found a way to ensure that they can all be fed. Your residents can gather food by foraging, and this is a good method to utilize from the start. A girl's Harvesting and Education skills will allow her to bring home more food, while her awareness reduces the chance of her being attacked by wild animals while foraging. However, it is rather difficult to obtain the right skills at the start of the game.

"
    + "While hunting may look attractive, it is inadvisable, as your girl’s skills will play a large role in determining the success of a hunt. You will want your huntress to be skilled in Tracking, Awareness, and Endurance.

"
    + "You will have the option of obtaining a farm later in the game; a very efficient source of food. However, it does require a manager, and more girls. Your manager's Harvesting, Education and Household skills will determine how much of an impact she has over the amount of food or money produced. On the other hand, the various farm assignments will generally rely on the characteristics of a slave's body, rather than her skills. So, look forward to using crappy slaves for these assignments.

"
    + "Return"
);

new HelpTopic("Combat", ""
    + "Combat is performed by your companion, whom you may assign at the mansion screen. You won't be able to engage in any fights when traveling without a companion, so consider training a proper bodyguard.

"
    + "As your companion takes on the role of being your sword, you will still be able to help and lead her to victory, especially if you know the right moves.

"
    + "Combat is turn based. Every turn, your companion will suffer from both physical damage and stress. They may also be affected by special attacks, which may impact them in surprising ways. If their health reaches zero, they will be knocked unconscious and you will have to retreat. If their lust overwhelms them, they will reach climax and will be unable to fight, forcing you to retreat. Lust may be affected by some special skills or conditions, so watch out.

"
    + "Lastly, if their stress gets too high they will be unable to fight efficiently and will receive more damage. If girl’s stress won’t decrease by days, her spirit will be broken same way as in training and she will be unable to use any special skills.

"
);

Location.push(new Locations("help", function() {
    ToText("If this is your first time playing, I recommend that at the very least, you read over the 'Food' and 'Combat' sections, to help you understand some of the game's basic mechanics.

");
    Game.tmp1 = -1;
    for (var i in Help.topic) {
        if (Help.topic.hasOwnProperty(i))
            ToText("" + Help.topic[i].name + "

");
    }

    ToText("Back", false);
}, 1));

repo_name: spdevel/sfp

license: unknown


code:
"use strict";

var Job = function(name, desc) {
    this.name = name;
    this.desc = desc;
    this.requirements = [{
        health: 21,
    }];
    Data.addJob(this);
}

Job.prototype.toJSON = function() {
    return this.name;
}

Job.prototype.reqs = function(obj, index) {
    if (typeof obj !== "object")
        throw new Error("Unexpected arg to reqs: " + obj);
    if (index == undefined)
        index=0;
    if (this.requirements[index] == undefined)
        this.requirements[index] = {};
    for(var key in obj)
        if (obj.hasOwnProperty(key))
            this.requirements[index][key]=obj[key];
}

Job.prototype.reqnames = {};

Job.prototype.reqnames.willpower = new TierArray({
    6: "heroine",
    5: "headstrong",
    4: "willfull",
    3: "reserved",
    2: "meek",
    1: "pushover",
    0: "broken",
});

Job.prototype.reqnames.courage = new TierArray({
    80: "heroic",
    60: "brave",
    40: "restrained",
    20: "cautious",
    0: "coward",
});

Job.prototype.reqnames.confidence = new TierArray({
    80: "self-assured",
    60: "assertive",
    40: "balanced",
    20: "indecisive",
    0: "doormat",
});

Job.prototype.reqnames.wit = new TierArray({
    80: "prodigy",
    60: "smart",
    40: "reasonable",
    20: "dim",
    0: "dunce",
});

Job.prototype.reqnames.charm = new TierArray({
    80: "crowd's favorite",
    60: "eye-catching",
    40: "memorable",
    20: "likable",
    0: "dull",
});

Job.prototype.reqnames.face = new TierArray({
    86: "beautiful",
    71: "pretty",
    51: "cute",
    31: "average looking",
    16: "plain looking",
    0:  "hideous",
});

Job.prototype.reqnames.loyalty = new TierArray({
    85: "devoted",
    65: "enthusiastic",
    45: "respectful",
    35: "grateful",
    15: "unenthusiastic",
    0: "none",
});

Job.prototype.showReqs = function(girl, i) {
    var met = " class='green'";
    var need = " class='red'";

    if (i == undefined)
        i = 0;
    var requirements = this.requirements[i];

    var skill = function(x, y) {
        var ret = '';
        if (y == undefined)
            y = x.substr(0,1).toUpperCase() + x.substr(1);
        if (girl.skill[x] >= requirements[x])
            ret += ', ";
        else
            ret += ', ";
        ret += y + " " + Data.skill_adjective[requirements[x]] + "";
        return ret;
    }

    var attr = function(x) {
        var ret = '';
        var y = x.substr(0,1).toUpperCase() + x.substr(1);
        if (girl.attr[x] >= requirements[x])
            ret += ', ";
        else
            ret += ', ";

        ret += y + " " + Job.prototype.reqnames[x].get(requirements[x]) + "";
        return ret;
    }

    var reqs = '';
    var excuses = '';

    for (var x in requirements) {
        switch(x) {
            case 'survival':
            case 'combat':
            case 'service':
            case 'allure':
            case 'management':
                reqs += skill(x);
                break;
            case 'bodycontrol':
                reqs += skill('bodycontrol', 'Body Control');
                break;
            case 'magicarts':
                reqs += skill('magicarts', 'Magic Arts');
                break;
            case 'sex':
                reqs += skill('sex', 'Sexual Proficiency');
                break;
            case 'willpower':
            case 'courage':
            case 'confidence':
            case 'wit':
            case 'charm':
                reqs += attr(x);
                break;
            case 'loyalty':
                if (girl[x] >= requirements[x])
                    reqs += ', ";
                else
                    reqs += ', ";
                reqs += x + " " + Job.prototype.reqnames[x].get(requirements[x]) + "";
                break;
            case 'reluctance':
                /* job -1 == no girl passed */
                if (!girl.hasJob('') && girl.obedience + girl.loyalty < requirements[x])
                    excuses = "" + girl.name + " refuses to take the job.";
                break;
            case 'obedience':
                if (!girl.hasJob('') && girl[x] < requirements[x])
                    excuses = " " + girl.name + " refuses to take the job.";
                break;
            case 'corruption':
                if (!girl.hasJob('') && girl[x] < requirements[x])
                    excuses = " " + girl.name + " is too pure to do that.";
                break;
            case 'health':
                if (!girl.hasJob('') && girl[x] < requirements[x])
                    excuses = " " + girl.name + " isn't healthy enough.";
                break;
        }
    }
    return [ reqs.replace(/^, /, ""), excuses ];
}

Job.prototype.display = function(girl, returnto) {
    if (girl === undefined) {
        throw new Error("currently unsupported");
    }

    if (returnto == undefined)
        returnto = 'population';

    var cantake = this.canTake(girl);

    var out = '';
    if (girl.hasJob(this.name))
        out += "";
    else if (cantake)
        out += "";
    else
        out += "";

    out += this.name + " — " + this.desc;

    var reqs_real = '';
    var excuses = '';

    for (var i = 0; i < this.requirements.length; i++) {
        var tmp = this.showReqs(girl, i);
        if (tmp[1] != '')
            excuses = tmp[1];
        if (tmp[0] != '') {
            if (reqs_real == '')
                reqs_real += " (requires ";
            else
                reqs_real += " OR ( ";
            reqs_real += tmp[0] + ")";
        }
    }

    out += reqs_real;

    if (excuses && !cantake)
        out += "
" + excuses;

    return out + "

";
}

Job.prototype.nightly = function(girl) {
}

Job.prototype.excuse = {
    // skills
    survival: 'is not skilled enough in survival to do her job.',
    combat: 'is not skilled enough in combat to do her job.',
    management: 'is not skilled enough in management to do her job.',
    service: 'is not skilled enough in service to do her job.',
    allure: 'is not skilled enough in allure to do her job.',
    bodycontrol: 'is not skilled enough in bodycontrol to do her job.',
    magicarts: 'is not skilled enough in the magical arts to do her job.',
    sex: 'is not skilled enough in sexual proficiency to do her job.',

    // attrs
    willpower: 'is too weak-willed to do her job.',
    courage: 'is too cowardly to work.',
    charm: 'is not personable enough for her job.',
    wit: 'is not smart enough to do her job.',
    confidence: "doesn't think she will be able to handle her job.",

    // other
    health: 'is in no condition to work and will rest.',
    obedience: 'outright refused to work.',
    loyalty: 'will not do her job for you.',
    reluctance: 'refused to work',
    corruption: 'is too pure to do that.',
}

Job.prototype._canTake = function(girl, i) {
    var ret = true;
    var excuse = '';
    for (var x in this.requirements[i]) {
        switch(x) {
            case 'survival':
            case 'combat':
            case 'management':
            case 'service':
            case 'allure':
            case 'bodycontrol':
            case 'magicarts':
            case 'sex':
                if (girl.skill[x] < this.requirements[i][x]) {
                    excuse = girl.name + " " + this.excuse[x];
                    ret = false;
                }
                break;
            case 'willpower':
            case 'courage':
            case 'confidence':
            case 'wit':
            case 'charm':
                if (girl.attr[x] < this.requirements[i][x]) {
                    excuse = girl.name + " " + this.excuse[x];
                    ret = false;
                }
                break;
            case 'loyalty':
            case 'corruption':
            case 'obedience':
            case 'lust':
            case 'health':
                if (girl[x] < this.requirements[i][x]) {
                    excuse = girl.name + " " + this.excuse[x];
                    ret = false;
                }
                break;
            case 'reluctance':
                if (girl.obedience+girl.loyalty < this.requirements[i].reluctance) {
                    excuse = girl.name + " " + this.excuse[x];
                    ret = false;
                }
                break;
            default:
                break;
        }
    }
    return [ ret, excuse ];
}

Job.prototype.canTake = function(girl, outfn) {
    if (outfn == undefined)
        outfn = function() {}

    var ret_real = false;
    var excuse = '';
    for (var i = 0; i < this.requirements.length; i++) {
        var tmp = this._canTake(girl, i);
        if (tmp[0] == true)
            ret_real = true;
        else
            excuse = tmp[1];
    }
    if (!ret_real)
        outfn(excuse);
    return ret_real;
}

Job.prototype.quitJob = function(girl) {}

Job.prototype.takeJob = function(girl) {}

var j;

j = new Job('Rest', "will rest and relax.");
j.nightly = function(girl) {
    if (girl.busy == 0) //FIXME, imlement this some other way
        ToText(girl.name + " rests.");
    girl.stress = Math.max(0, girl.stress - 10);
    girl.health = Math.min(100, girl.health + 10);
}
j.requirements[0] = {};

var animalEncounter = function(girl, hunt) {
    if (hunt) {
        // trying to find
        var chance = 40 + girl.skill.survival * 10;
        if (girl.isRace('arachna'))
            chance += 20;
        if (girl.body.ears.acuity >= 200)
            chance += 10;
    } else {
        // trying to hide
        var chance = 19 - girl.skill.survival*2;
    }

    chance = Math.withChance(100 - chance);
    if (Math.tossCoin(chance)) {
        if (hunt) {
            if (girl.isRace('arachna'))
                ToText(" Sadly, nothing got caught in her net.");
            else
                ToText(" Sadly, she could not find any fresh animal trails.");

            girl.experienceChange(Math.tossDice(5, 10)); //learn from failure
        } else {
            ToText(" The forest was serene."); //TODO some beautiful description goes here
        }
        return false;
    }

    if (hunt) {
        if (girl.isRace('arachna'))
            ToText(" After several hours of waiting she caught an animal in her net");
        else
            ToText(" After several hours of hide-and-seek she found an animal");
    } else {
        if (girl.isRace('dryad') || ["leaves", "fur-leaves"].indexOf(girl.body.skin.coverage.value) != -1) {
            ToText("Her plant-like skin let her hide from a wild animal and continue on her way.");
            return false;
        }
        // hightened hearing gets another chance to escape
        if (girl.body.ears.acuity >= 200 && Math.tossCoin(chance)) {
            ToText("Her hightened hearing helped her to narrowly escape a dangerous encounter.");
            return false;
        }
        ToText(" She was attacked by a wild animal");
    }
    var animal = Math.tossDice(20, 40);

    var damage = animal;
    if (girl.isRace('arachna'))
        damage = Math.round(damage / 2); //nets are awesome

    if (girl.body.hasAugmentation('skin')) {
        damage = Math.round(damage / 2);
        if (damage+1 < girl.health) {
            ToText(". She fought, and her ");
            if (girl.body.hasAugmentation('fur'))
                ToText("thick fur");
            else
                ToText("tough scales");
            ToText(" kept her safe,");
        }
    }

    // bloody encounter
    girl.healthChange(Math.min(-damage + girl.skill.combat*(hunt ? 3 : 2) + girl.skill.bodycontrol*3, 0));
    if (girl.health < 1) {
        ToText(" and died in the encounter due to her poor health. Her body was found the next morning.");
        return true;
    }

    // still not dead, try to finish it
    var chance = Math.withChance(30 - girl.skill.bodycontrol*10);
    if (Math.tossCoin(chance) || girl.health < 20) {
        if (hunt)
            ToText(" but could not capture it.");
        else
            ToText(" and she drove it off.");

        girl.experienceChange(Math.tossDice(5, 10)); //learn from failure
    } else {
        var meat = animal + girl.skill.survival * 20;
        if (girl.isRace('arachna'))
            meat = Math.round(meat * 1.3);
        Game.food += meat;
        if (hunt)
            ToText(" and managed");
        else
            ToText(" but she managed");
        ToText(" to capture it for " + meat + " units of food (meat).");
    }

    if (girl.health < 10) {
        ToText(" She had a near-death experience.");
        girl.experienceChange(20);
        // TODO add SM traits?
    }

    girl.experienceChange(animal);
    girl.loyaltyChange(-1);
    return true;
}

j = new Job('Forage', 'will look around for edible berries and fungi.');
j.reqs( { reluctance: 60 });
j.nightly = function(girl) {
    ToText(girl.name + " went to the forest in search of wild edibles.");
    var hadEncounter = animalEncounter(girl, false);

    if (girl.health < 1)
        return;

    var found = Math.tossDice(30, 40) + girl.skill.survival * 6;
    if (girl.isRace('dryad'))
        found += 50;

    if (hadEncounter) {
        found = Math.round(found / 3);
        ToText(" The encounter made her very tired,");
        ToText(" but she still managed");
    } else {
        ToText(" She managed");
    }
    ToText(" to collect " + found + " units of food (berries, mushrooms and fungy).");

    Game.food += found;
    girl.experienceChange(Math.round(found / 3));
    girl.loyaltyChange(-1);
}

j = new Job('Hunt', 'will hunt for wild animals.');
j.reqs({
    survival: 1,
    courage: 30,
    reluctance: 60,
});
j.nightly = function(girl) {
    ToText(girl.name + " went to the forest in search of wild animals.");
    animalEncounter(girl, true);

    if (girl.health < 1)
        return;

    girl.loyaltyChange(-1);
}

j = new Job('Library', 'will manage the library and study.');
j.reqs( { reluctance: 50, });
j.nightly = function(girl) {
    ToText(girl.name + " spends her time studying in the library.");
    girl.experienceChange(Math.max((25 + Game.library_level * 5) - girl.level*2, 0));
}

j = new Job('Chef', "will cook for the other residents and buy food from market when it runs short (is below 200). Service improves efficiency.");
j.reqs( { reluctance: 50, });
j.quitJob = function(girl) {
    Game.roles.cook = -1;
}
j.takeJob = function(girl) {
    if (Game.roles.cook != -1)
        Game.Girls[Game.roles.cook].setJob("rest");
    Game.roles.cook = girl.getIndex();
}
j.nightly = function(girl) {
    ToText(girl.name + " spent the day preparing meals for the residents.");
    girl.experienceChange(Game.Girls.length * 3);

    if (Game.food < 200) {
        if (Game.gold >= 100) {
            ToText(" She went to the market and bought 200 units of food.");
            Game.food += 200;
            Game.gold -= 100;
        } else {
            ToText(" She complained about lack of food and no money to supply the kitchen on her own.");
        }
    } else if (Game.food > 500) {
        var extra = Game.food - 500;
        Game.food -= extra;
        var gold = Math.round(extra * (0.4 + girl.skill.management*0.04));
        Game.gold += gold;
        ToText(" She went to the market and sold " + extra + " units of food for " + gold + " gold.");
    }
}

j = new Job('Nurse','will help residents maintain and recover their health. Higher Service has a stronger effect.');
j.quitJob = function(girl) {
    Game.roles.nurse = -1;
}
j.takeJob = function(girl) {
    if (Game.roles.nurse != -1)
        Game.Girls[Game.roles.nurse].setJob("rest");
    Game.roles.nurse = girl.getIndex();
}
j.reqs({ reluctance: 60, });
j.nightly = function(girl) {
    ToText(girl.name + " is taking care of resident's health.");
    var patients = 0;
    for (var i = 0; i < Game.Girls.length; i++) {
        var res = Game.Girls[i];
        if (res.busy == 0) {
            patients++;
            res.healthChange(5 + girl.skill.service*3);
            res.toxicityChange(Math.tossDice(-girl.skill.service-1, 0));
        }
    }
    girl.experienceChange(patients * 3);
}

j = new Job('Prostitution', "will be assigned to town's brothel as a common whore.");
j.display = function(girl, returnto) {
    if (Game.mission.brothel < 1)
        return "";
    if (Game.mission.brothel < 2)
        return "You need to get permission from the brothel first.

";
    if (girl.body.shape.type.index >= 4)
        return "This girl's body shape is too uncommon to be accepted into brothel.

";

    return Job.prototype.display.call(this, girl, returnto);
}
j.reqs({
    reluctance: 100,
    corruption: 30,
});
j.nightly = function(girl) {
    ToText(girl.name + " went to the brothel to work as a whore.");
    if (girl.body.pussy.virgin.value) {
        ToText(" Her virginity was taken by one of the customers.");
        girl.body.pussy.virgin = false;
        girl.loyaltyChange(-5);
    }
    // FIXME come up with some customers.
    /* sexAct(risky) may trigger pregnancy */
    if (girl.sexAct(['oral', 'pussy', 'risky']))
        ToText(" She became pregnant from one of the customers.");

    var earnings = Math.round(girl.value() * 0.25  + Math.tossDice(0, 15));
    earnings += girl.skill.sex * 10 + girl.skill.allure * 5 + Math.round(girl.attr.charm / 2);

    if (girl.body.chest.nipples.elastic == true)
        earnings += 25;

    if (girl.body.tongue.size >= 200)
        earnings += 20;

    girl.healthChange(-10);

    if (!girl.isRace(['Bunny', 'Halfkin Bunny']))
        girl.stressChange(Math.round(10 - girl.corruption/20));

    if (girl.hasEffect("entranced")) {
        if (!girl.hasSexTrait("frivolous")) {
            ToText(girl.name + " became sexually frivolous.");
            girl.addSexTrait('frivolous');
        }
        girl.removeEffect("entranced");
    }

    if (girl.corruption < 50 && !girl.hasSexTrait('frivolous')) {
        girl.obedienceChange(0 - Math.round(girl.attr.confidence / 5));
        girl.loyaltyChange(-3);
        girl.corruptionChange(Math.tossDice(3, 6));
    }

    ToText(" By the end of the day, she earned " + earnings + " gold.");
    Game.gold += earnings;
    girl.experienceChange(Math.round(earnings / 10));
}

j = new Job('Escort', "will be assigned to the town's brothel as a high class whore for rich men.");
j.reqs({
    health: 80, // rich people don't fuck diseased whores
    sex: 2,
    allure: 2,
    corruption: 30,
    reluctance: 100,
});
j.display = function(girl, returnto) {
    if (girl == undefined)
        girl = new Girl();
    if (Game.mission.brothel < 2 || girl.body.shape.type.index >= 4)
        return "";

    return Job.prototype.display.call(this, girl, returnto);
}
j.nightly = function(girl) {
    ToText(girl.name + " went providing escort service to rich clients of the brothel.");

    if (girl.body.pussy.virgin.value) {
        ToText(" Her virginity was taken by one of the customers.");
        girl.body.pussy.virgin = false;
        girl.loyaltyChange(-5);
    }
    // FIXME come up with some customers.
    /* sexAct(risky) may trigger pregnancy */
    if (girl.sexAct(['oral', 'petting', 'pussy', 'risky']))
        ToText(" She became pregnant from one of the customers.");

    var earnings = Math.round(girl.value() * 0.25 + Math.tossDice(0, 25));
    earnings += girl.skill.sex * 10 + girl.skill.allure * 15 + girl.attr.charm; // ~70 from reqs, up to 225

    girl.healthChange(-5);

    if (!girl.isRace(['Bunny', 'Halfkin Bunny']))
        girl.stressChange(Math.round(10 - girl.attr.confidence/10));

    if (girl.hasEffect("entranced")) {
        if (!girl.hasSexTrait("frivolous")) {
            ToText(girl.name + " became sexually frivolous.");
            girl.addSexTrait('frivolous');
        }
        girl.removeEffect("entranced");
    }

    if (girl.corruption < 50 && !girl.hasSexTrait('frivolous')) {
        girl.obedienceChange(0 - Math.round(girl.attr.confidence / 5));
        girl.loyaltyChange(-4);
        girl.corruptionChange(Math.tossDice(3, 6));
    }

    ToText(" By the end of the day, she earned " + earnings + " gold.");
    Game.gold += earnings;
    girl.experienceChange(Math.round(earnings / 12));
}

j = new Job('Fucktoy', "will be used by the most deviant men in brothel.");
j.reqs({
    sex: 2,
    reluctance: 125,
    corruption: 50,
});
j.display = Data.job_byname.escort.display;
j.nightly = function(girl) {
    ToText(girl.name + " departed to work as an exclusive fucktoy.");

    if (girl.body.pussy.virgin.value) {
        ToText(" Her virginity was taken by one of the customers.");
        girl.body.pussy.virgin = false;
        girl.loyaltyChange(-5);
        girl.stressChange(10);
    }
    // FIXME come up with some customers.
    /* sexAct(risky) may trigger pregnancy */
    if (girl.sexAct(['oral', 'petting', 'pussy', 'risky']))
        ToText(" She became pregnant from one of the customers.");

    var earnings = Math.round(girl.value() * 0.15 + Math.tossDice(0, 40));
    earnings += girl.skill.sex * 20 +  girl.corruption; // 40(+75) from reqs, up to 200

    if (girl.body.chest.nipples.elastic == true)
        earnings += 25;

    if (girl.body.tongue.size >= 200)
        earnings += 20;

    girl.healthChange(-15);

    if (!girl.isRace(['Bunny', 'Halfkin Bunny']))
        girl.stressChange(Math.round(20 - girl.corruption/10));

    if (girl.hasEffect("entranced")) {
        if (!girl.hasSexTrait("deviant")) {
            ToText(girl.name + " became sexually deviant.");
            girl.addSexTrait('deviant');
        }
        girl.removeEffect("entranced");
    }

    if (girl.corruption < 75 && !girl.hasSexTrait('deviant')) {
        girl.obedienceChange(0 - Math.round(girl.attr.confidence / 3));
        girl.loyaltyChange(-10);
        girl.corruptionChange(Math.tossDice(8, 13));

        /* damage her personality */
        girl.attr.confidence = Math.max(girl.attr.confidence - Math.tossDice(3, 8), 1);
        ToText(" Her unaccustomed personality was damaged.");
        girl.WillRecounter();
    }

    ToText(" By the end of the day, she earned " + earnings + " gold.");
    Game.gold += earnings;
    girl.experienceChange(Math.round(earnings / 12));
}

j = new Job('Store', 'will do manual labor at the town store to earn money.');
j.reqs({
    reluctance: 50,
});
j.nightly = function(girl) {
    ToText(girl.name + " worked at a town store.");
    var earnings = Math.tossDice(10, 20) + 10 * girl.skill.management + 5 * girl.skill.service;

    if (girl.isRace(['Tanuki', 'Halfkin Tanuki']))
        earnings = Math.round(earnings * 1.3);

    ToText(" She earned " + earnings + " gold.");

    girl.healthChange(-5);
    girl.loyaltyChange(-1);
    girl.stressChange(10);

    var extra = 0;

    if (girl.attr.charm >= 40 && Math.tossCoin(0.333)) {
        extra = Math.tossDice(10, 25);
        ToText(" She was also rewarded with "+extra+" provisions for her effort.");
        Game.food += extra;
    }

    Game.gold += earnings;
    girl.experienceChange(Math.round(earnings / 4 + extra / 2));
}

j = new Job('Order Assistant', "will work as a staff member on various guild assignments.");
j.reqs({
    magicarts: 3,
    management: 2,
    reluctance: 70,
});
j.display = function(girl, returnto) {
    if (Game.guildrank < 1)
        return "";

    return Job.prototype.display.call(this, girl, returnto);
}
j.nightly = function(girl) {
    ToText(girl.name + " spent the day working at the Mage's Order.");
    var earnings = Math.tossDice(40, 50) + 12 * girl.skill.magicarts + 7 * girl.skill.management;

    if (girl.isRace('Demon'))
        earnings = Math.round(earnings * 1.2);
    else
        girl.stressChange(12);

    ToText(" She earned " + earnings + " gold.");
    Game.gold += earnings;

    girl.healthChange(-5);
    girl.loyaltyChange(-1);
    girl.experienceChange(Math.round(earnings / 12));
}

j = new Job('Public Entertainer', 'will earn money by doing dances, shows and other stage performances.');
j.reqs({
    allure: 3,
    bodycontrol: 2,
    reluctance: 70,
    confidence: 40,
});

j.nightly = function(girl) {
    ToText(girl.name + " goes to the town square and works as an entertainer.");

    var earnings = Math.tossDice(30, 40) + 10*girl.skill.allure + 6*girl.skill.bodycontrol + Math.round(girl.attr.charm/4);

    if (girl.isRace('Nereid'))
        earnings = Math.round(earnings * 1.2);

    var earnings = Math.round(earnings * girl.attr.confidence / 80);

    ToText(" She earned " + earnings + " gold.");

    Game.gold += earnings;

    girl.healthChange(-5);
    girl.loyaltyChange(-2);
    girl.stressChange(Math.round(15 - girl.attr.confidence/10));

    girl.experienceChange(Math.round(earnings/12));
}

j = new Job('Jailer', 'will be taking care of prisoners, needs to be loyal.');
j.quitJob = function(girl) {
    Game.roles.jailer = -1;
}
j.takeJob = function(girl) {
    if (Game.roles.jailer != -1)
        Game.Girls[Game.roles.jailer].setJob("rest");
    Game.roles.jailer = girl.getIndex();
}
j.reqs({
    confidence: 40,
    loyalty: 25,
    reluctance: 100,
});
j.nightly = function(girl) {
    var xp = 0;
    for (var i = 0; i < Game.Girls.length; i++) {
        var prisoner = Game.Girls[i];
        if (prisoner.where_sleep != 5)
            continue;

        prisoner.healthChange(2 + girl.skill.service * 2);
        prisoner.stressChange(0 - (4 + girl.skill.management*4));
        prisoner.obedienceChange(2 + girl.skill.service*3 + girl.skill.management*3);
        xp += 5;
    }
    if (xp > 0)
        ToText(girl.name + " spends time taking care of the prisoners.");
    else
        ToText(girl.name + " has no prisoners to care for.");

    girl.experienceChange(xp);
}

j = new Job('Lab Assistant', 'will help to manage your laboratory experiments and apiary.');
j.quitJob = function(girl) {
    Game.roles.labassistant = -1;
}
j.takeJob = function(girl) {
    if (Game.roles.labassistant != -1)
        Game.Girls[Game.roles.labassistant].setJob("rest");
    Game.roles.labassistant = girl.getIndex();
}
j.reqs({
    magicarts: 3,
    management: 2,
    reluctance: 50,
});
j.display = function(girl, returnto) {
    if (Game.lab_level < 1)
        return "";

    return Job.prototype.display.call(this, girl, returnto);
}
j.nightly = function(girl) {
    /* first, do we have anything to do */
    var total_mana = 0;
    var modifications = 0;

    for (var i = 0; i < Game.Girls.length; i++) {
        var res = Game.Girls[i];
        if (res.mission == 5) {
            res.busy--;
            if (modifications++ == 0)
                ToText(girl.name + " works on the girls in the lab.");

            if (res.busy < 1) {
                res.busy = 0;
                res.mission = 0;
                res.setJob("Rest");
                ToText(" The modifications on " + Game.Girls[i].name + " are complete.");
            }
        }
    }

    for (var i = 0; i < Game.Girls.length; i++) {
        var res = Game.Girls[i];
        if (res.hasJob("Apiary")) { // apiary production
            if (total_mana == 0) {
                if (modifications)
                    ToText("
She also");
                else
                    ToText(girl.name);
                ToText(" tends to the residents in the apiary.");
            }

            var mana = Math.tossDice(1, 2);
            if (res.isRace('Drow'))
                mana += 1;

            if (res.attr.willpower > 0 && !res.hasSexTrait('deviant'))
                res.stressChange(15);

            mana = mana + Math.round(girl.skill.magicarts / 2) + Math.floor(girl.skill.service / 2.5);
            total_mana += mana;
        }
    }

    if (total_mana > 0) {
        ToText(" they have produced " + total_mana + " mana.");
        Game.mana += total_mana;
    }

    if (modifications == 0 && total_mana == 0)
        ToText(girl.name + " has nothing to do in the laboratory.");
    else {
        // stress per operation being performed, and a little more if she's
        // also tending the apiary.
        girl.stressChange(modifications * (6 - girl.skill.management) +
                total_mana > 0 ? 5 : 0);
        girl.experienceChange(modifications * 2 + Math.round(total_mana / 10));

        // get hurt some during surgury.
        girl.healthChange(0 - modifications);
    }

    girl.loyaltyChange(-1);
}

j = new Job('Head Girl', 'will watch over other servants improving their behavior.');
j.quitJob = function(girl) {
    Game.roles.headgirl = -1;
}
j.takeJob = function(girl) {
    if (Game.roles.headgirl != -1)
        Game.Girls[Game.roles.headgirl].setJob("rest");
    Game.roles.headgirl = girl.getIndex();
}
j.reqs({
    health: 0,
    management: 3,
    loyalty: 65,
});
j.reqs({
    health: 0,
    allure: 3,
    loyalty: 65,
}, 1);
j.display = function(girl, returnto) {
    if (Game.max_residents < 8)
        return "";

    return Job.prototype.display.call(this, girl, returnto);
}
j.nightly = function(girl) {
    if (Game.headgirl_orders > 1)
        Game.headgirl_orders = 0;

    /* check if it's the wrong one. */
    if (Game.headgirl_orders == 0 && girl.skill.management < 3) {
        /* can we fix it? */
        if (girl.skill.allure >= 3) {
            ToText(girl.name + " isn't skilled enough for strict behavior, so she begins patronage.
");
            Game.headgirl_orders = 1;
        } else {
            ToText(girl.name + " cannot carry out the duties of Head Girl.

");
            girl.setJob("Rest");
            return;
        }
    }

    if (Game.headgirl_orders == 1 && girl.skill.allure < 3) {
        /* try to fix this one too? */
        if (girl.skill.management >= 3) {
            ToText(girl.name + " isn't skilled enough for patronage behavior, so she begins strict.
");
            Game.headgirl_orders = 0;
        } else {
            ToText(girl.name + " cannot carry out the duties of Head Girl.

");
            girl.setJob("Rest");
            return;
        }
    }

    if (Game.headgirl_orders == 0)
        this.strict_behavior(girl);
    else
        this.patronage_behavior(girl);

    var xp = 0;
    for (var i = 0; i < Game.Girls.length; i++) {
        var res = Game.Girls[i];
        if (res.busy > 0 || res.where_sleep > 3)
            continue;
        xp += 2;
    }

    girl.experienceChange(xp);
}

j.strict_behavior = function(girl) {
    var corrected = 0;
    ToText(girl.name + " patrols the halls of your mansion, looking for any servants who get out of line.");

    var count = 0;

    for (var i = 0; i < Game.Girls.length; i++) {
        var res = Game.Girls[i];
        if (res.busy > 0 || res.where_sleep > 3 || res == girl)
            continue;
        if (res.obedience < girl.obedience) {
            res.obedienceChange(girl.skill.management * 5 -5);
            res.stressChange(Math.tossDice(5, 10));
            count++;
        }
    }
    girl.stressChange(Math.round(Math.sqrt(count)));
    girl.obedienceChange(1);
    girl.loyaltyChange(-1);
}

j.patronage_behavior = function(girl) {
    ToText(girl.name + " keeps her door open to anyone feeling stressed.");
    var count = 0;
    for (var i = 0; i < Game.Girls.length; i++) {
        var res = Game.Girls[i];
        if (res.busy > 0 || res.where_sleep > 3 || res == girl)
            continue;
        res.loyaltyChange(girl.skill.allure);
        if (res.stress > 25) {
            res.stressChange(Math.tossDice(5, 10));
            count++;
        }
    }
    girl.loyaltyChange(1);
    girl.stressChange(Math.round(Math.sqrt(count)));
}

j = new Job('Farm Manager', 'will watch over your "cattle" and oversee daily Farm operation.');
j.quitJob = function(girl) {
    Game.roles.manager = -1;
}
j.takeJob = function(girl) {
    if (Game.roles.manager != -1)
        Game.Girls[Game.roles.manager].setJob("rest");
    Game.roles.manager = girl.getIndex();
}
j.display = function(girl, returnto) {
    if (Game.mission.farm < 2)
        return "";

    return Job.prototype.display.call(this, girl, returnto);
}
j.reqs({
    confidence: 60,
    management: 3,
});

/* nightly is handled in a different way */

j = new Job('Companion', 'will accompany you on your journey.');
j.quitJob = function(girl) {
    Game.companion = -1;
}
j.takeJob = function(girl) {
    if (Game.companion != -1)
        Game.Girls[Game.companion].setJob("rest");
    Game.companion = girl.getIndex();
}
j.requirements[0] = {}

/* no nightly job for companions */

/* stub out the "passive" jobs. */

j = new Job('Farm Cow', 'Is being milked at your farm');
j.takeJob = function(girl) {
    girl.mission = 1;
    girl.where_sleep = 1;
}
j.quitJob = function(girl) {
    girl.mission = 0;
}
j.display = function() { return "";}
j = new Job('Farm Hen', 'Is used for egg collection at your farm');
j.takeJob = function(girl) {
    girl.mission = 1;
    girl.where_sleep = 1;
}
j.quitJob = function(girl) {
    girl.mission = 0;
}
j.display = function() { return "";}
j = new Job('Apiary', 'Used for mana production in your apiary');
j.takeJob = function(girl) {
    girl.mission = 1;
    girl.where_sleep = 4;
}
j.quitJob = function(girl) {
    girl.mission = 0;
    girl.where_sleep = 1;
}
j.display = function() { return "";}

j = new Job('', 'NULL JOB');
j.display = function() { return "";}