code:
/* eslint-disable sonarjs/no-identical-expressions */
// cSpell:ignore unaccelerated

App.RA.Activation.Context = class {
	/**
	 * @param {FC.SlaveState} slave
	 */
	constructor(slave) {
		this._slave = slave;
	}

	get slave() {
		return this._slave;
	}
};

/**
 * @template {boolean|number|string} T
 * @typedef {object} Getter
 * @property {string} name
 * @property {string} description Should include possible values if applicable.
 * @property {string} [requirements] Plaintext description of requirements to use this getter.
 * @property {()=>boolean} [enabled] Whether the getter can be used.
 * @property {()=>boolean} [visible] Whether the getter should be shown. Mainly intended for disabled mods.
 * @property {(s: App.RA.Activation.Context) =>T} val
 */

App.RA.Activation.getterManager = (function() {
	class GetterManager {
		constructor() {
			/**
			 * @private
			 * @type {Map<string, Getter<boolean>>}
			 */
			this._booleanGetters = new Map();
			/**
			 * @private
			 * @type {Map<string, Getter<boolean>>}
			 */
			this._assignmentGetters = new Map();
			/**
			 * @private
			 * @type {Map<string, Getter<number>>}
			 */
			this._numberGetters = new Map();
			/**
			 * @private
			 * @type {Map<string, Getter<string>>}
			 */
			this._stringGetters = new Map();
		}

		/**
		 * @param {string} key
		 * @private
		 */
		_validateKey(key) {
			if (!/[a-zA-Z]/.test(key.first())) {
				throw new Error(`Invalid Key: ${key}; The first character of a getter key has to be alphabetic`);
			}
		}

		/**
		 * @param {string} key
		 * @param {Getter<boolean>} getter
		 */
		addBoolean(key, getter) {
			this._validateKey(key);
			this._booleanGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @param {Getter<boolean>} getter
		 */
		addAssignment(key, getter) {
			this._validateKey(key);
			this._assignmentGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @param {Getter<number>} getter
		 */
		addNumber(key, getter) {
			this._validateKey(key);
			this._numberGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @param {Getter<string>} getter
		 */
		addString(key, getter) {
			this._validateKey(key);
			this._stringGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		has(key) {
			return this._booleanGetters.has(key) || this._assignmentGetters.has(key) ||
				this._numberGetters.has(key) || this._stringGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isBoolean(key) {
			return this._booleanGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isAssignment(key) {
			return this._assignmentGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isNumber(key) {
			return this._numberGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isString(key) {
			return this._stringGetters.has(key);
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<boolean>>}
		 */
		get booleanGetters() {
			return this._booleanGetters;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<boolean>>}
		 */
		get assignmentGetters() {
			return this._assignmentGetters;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<number>>}
		 */
		get numberGetters() {
			return this._numberGetters;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<string>>}
		 */
		get stringGetters() {
			return this._stringGetters;
		}

		/**
		 * @returns {string}
		 */
		get booleanDefault() {
			return this._booleanGetters.keys().next().value;
		}

		/**
		 * @returns {string}
		 */
		get assignmentDefault() {
			return this._assignmentGetters.keys().next().value;
		}

		/**
		 * @returns {string}
		 */
		get numberDefault() {
			return this._numberGetters.keys().next().value;
		}

		/**
		 * @returns {string}
		 */
		get stringDefault() {
			return this._stringGetters.keys().next().value;
		}

		/**
		 * @param {App.RA.Activation.Stack} stack
		 * @param {App.RA.Activation.Context} context
		 * @param {string} key
		 * @returns {boolean} True, if a getter exists for the given key
		 */
		read(stack, context, key) {
			let getterB = this._booleanGetters.get(key);
			if (getterB !== undefined) {
				stack.pushBoolean(getterB.val(context));
				return true;
			}
			getterB = this._assignmentGetters.get(key);
			if (getterB !== undefined) {
				stack.pushBoolean(getterB.val(context));
				return true;
			}
			const getterN = this._numberGetters.get(key);
			if (getterN !== undefined) {
				stack.pushNumber(getterN.val(context));
				return true;
			}
			const getterS = this._stringGetters.get(key);
			if (getterS !== undefined) {
				stack.pushString(getterS.val(context));
				return true;
			}
			return false;
		}
	}

	return new GetterManager();
})();

App.RA.Activation.populateGetters = function() {
	const gm = App.RA.Activation.getterManager;
	// Note: The first value of each type being added is taken as the default.

	// Booleans
	gm.addBoolean("isfertile", {
		name: "Is Fertile?", description: "Whether or not the slave is fertile.",
		val: c => isFertile(c.slave)
	});
	gm.addBoolean("isvirile", {
		name: "Is Virile?", description: "Whether or not the slave is virile, has fertile sperm.",
		val: c => isVirile(c.slave)
	});
	gm.addBoolean("isamputee", {
		name: "Is Amputee?", description: "Whether or not the slave has no limbs.",
		val: c => isAmputee(c.slave)
	});
	gm.addBoolean("ispregnant", {
		name: "Is Pregnant?", description: "Whether or not the slave is pregnant.",
		val: c => c.slave.preg > 0
	});
	gm.addBoolean("isslim", {
		name: "Is Slim?", description: "If the slave is considered slim or not by arcology standards.",
		val: c => isSlim(c.slave)
	});
	gm.addBoolean("isstacked", {
		name: "Is Stacked?", description: "If the slave is considered stacked (big T&A) or not.",
		val: c => isStacked(c.slave)
	});
	gm.addBoolean("ismodded", {
		name: "Is Modded?", description: "If the slave is considered heavily modded or not.",
		val: c => SlaveStatsChecker.isModded(c.slave)
	});
	gm.addBoolean("isunmodded", {
		name: "Is Unmodded?", description: "If the slave is (relatively) unmodded.",
		val: c => SlaveStatsChecker.isUnmodded(c.slave)
	});
	gm.addBoolean("canmove", {
		name: "Can Move?", description: "Can the slave move at all?",
		val: c => canMove(c.slave)
	});
	gm.addBoolean("canstand", {
		name: "Can Stand?", description: "Can the slave stand independently?",
		val: c => canStand(c.slave)
	});
	gm.addBoolean("canwalk", {
		name: "Can Walk?", description: "Can the slave walk independently; if they can, they can move and stand too.",
		val: c => canWalk(c.slave)
	});
	gm.addBoolean("hasinternalballs", {
		name: "Has Internal Balls?", description: "If the slave has internal balls. False, if the slave has no balls",
		val: c => c.slave.balls > 0 && c.slave.scrotum === 0
	});
	gm.addBoolean("ismindbroken", {
		name: "Is Mindbroken?", description: "If the slave is mindbroken",
		val: c => isMindbroken(c.slave)
	});
	gm.addBoolean("hasimplants", {
		name: "Has implants?", description: "Does the slave have any cosmetic implants?",
		val: c => SlaveStatsChecker.hasImplants(c.slave)
	});
	gm.addBoolean("hasallimplants", {
		name: "Has all Implants?", description: "Does the slave have the full set of cosmetic implants (boobs, butt, lips)?",
		val: c => SlaveStatsChecker.hasAllImplants(c.slave)
	});
	gm.addBoolean("issurgicallyimproved", {
		name: "Is Surgically Improved?", description: "Does the slave undergone breast, butt, lips, and waist enhancement surgeries?",
		val: c => SlaveStatsChecker.isSurgicallyImproved(c.slave)
	});

	// Assignments
	// Groups
	gm.addAssignment("any_penthouse", {
		name: "Penthouse", description: "Serving in your penthouse.",
		val: c =>
			[
				Job.REST, Job.FUCKTOY, Job.HOUSE, Job.WHORE, Job.PUBLIC, Job.SUBORDINATE, Job.MILKED, Job.GLORYHOLE,
				Job.CONFINEMENT, Job.CHOICE
			].includes(c.slave.assignment)
	});
	gm.addAssignment("any_leader", {
		name: "Leader", description: "Serving in any leadership position.",
		val: c =>
			[
				Job.BODYGUARD, Job.HEADGIRL, Job.RECRUITER, Job.AGENT, Job.AGENTPARTNER, Job.MADAM, Job.WARDEN, Job.DJ,
				Job.NURSE, Job.MILKMAID, Job.FARMER, Job.CONCUBINE, Job.MATRON, Job.TEACHER, Job.STEWARD, Job.ATTENDANT
			].includes(c.slave.assignment)
	});
	gm.addAssignment("any_facility", {
		name: "Facility", description: "Serving in any facility.",
		val: c =>
			[
				Job.ARCADE, Job.BROTHEL, Job.CELLBLOCK, Job.CLUB, Job.CLINIC, Job.DAIRY, Job.FARMYARD,
				Job.HEADGIRLSUITE, Job.MASTERSUITE, Job.NURSERY, Job.SCHOOL, Job.QUARTER, Job.SPA
			].includes(c.slave.assignment)
	});
	// Penthouse Assignments
	gm.addAssignment("rest", {
		name: "Resting", description: "Resting in the penthouse.",
		val: c => c.slave.assignment === Job.REST
	});
	gm.addAssignment("fucktoy", {
		name: "Fucktoy", description: "Pleasing the master.",
		val: c => c.slave.assignment === Job.FUCKTOY
	});
	gm.addAssignment("classes", {
		name: "Taking classes", description: "Taking classes to better serve.",
		val: c => c.slave.assignment === Job.CLASSES
	});
	gm.addAssignment("house", {
		name: "Cleaning", description: "Cleaning the penthouse.",
		val: c => c.slave.assignment === Job.HOUSE
	});
	gm.addAssignment("whore", {
		name: "Whoring", description: "Whoring themself out.",
		val: c => c.slave.assignment === Job.WHORE
	});
	gm.addAssignment("public", {
		name: "Serving public", description: "Serving the public.",
		val: c => c.slave.assignment === Job.PUBLIC
	});
	gm.addAssignment("subordinate", {
		name: "Subordinate", description: "Subordinate to other slaves.",
		val: c => c.slave.assignment === Job.SUBORDINATE
	});
	gm.addAssignment("milked", {
		name: "Milked", description: "Getting milked.",
		val: c => c.slave.assignment === Job.MILKED
	});
	gm.addAssignment("gloryhole", {
		name: "Glory hole", description: "Working as a glory hole.",
		val: c => c.slave.assignment === Job.GLORYHOLE
	});
	gm.addAssignment("confinement", {
		name: "Confined", description: "Confined at the penthouse.",
		val: c => c.slave.assignment === Job.CONFINEMENT
	});
	gm.addAssignment("choice", {
		name: "Choose own", description: "Allowed to choose their own job.",
		val: c => c.slave.assignment === Job.CHOICE
	});
	// Leadership Assignments
	gm.addAssignment("bodyguard", {
		name: "Bodyguard", description: "Serving as Bodyguard.",
		requirements: "Armory is built.", enabled: () => App.Entity.facilities.armory.established,
		val: c => c.slave.assignment === Job.BODYGUARD
	});
	gm.addAssignment("headgirl", {
		name: "Head Girl", description: "Serving as Head Girl",
		val: c => c.slave.assignment === Job.HEADGIRL
	});
	gm.addAssignment("recruiter", {
		name: "Recruiter", description: "Recruiting new slaves.",
		val: c => c.slave.assignment === Job.RECRUITER
	});
	gm.addAssignment("agent", {
		name: "Agent", description: "Serving as an Agent in another arcology.",
		val: c => c.slave.assignment === Job.AGENT
	});
	gm.addAssignment("agentpartner", {
		name: "Agent partner", description: "Serving an agent living in another arcology.",
		val: c => c.slave.assignment === Job.AGENTPARTNER
	});
	// Facility Assignments
	gm.addAssignment("arcade", {
		name: "Confined in arcade", description: "Confined in the arcade.",
		requirements: "Arcade is built.", enabled: ()=>App.Entity.facilities.arcade.established,
		val: c => c.slave.assignment === Job.ARCADE
	});
	gm.addAssignment("madam", {
		name: "Madam", description: "Serving as Madam.",
		requirements: "Brothel is built.", enabled: ()=>App.Entity.facilities.brothel.established,
		val: c => c.slave.assignment === Job.MADAM
	});
	gm.addAssignment("brothel", {
		name: "Brothel whoring", description: "Working in the brothel.",
		requirements: "Brothel is built.", enabled: ()=>App.Entity.facilities.brothel.established,
		val: c => c.slave.assignment === Job.BROTHEL
	});
	gm.addAssignment("warden", {
		name: "Wardeness", description: "Serving as Wardeness.",
		requirements: "Cellblock is built.", enabled: ()=>App.Entity.facilities.cellblock.established,
		val: c => c.slave.assignment === Job.WARDEN
	});
	gm.addAssignment("cellblock", {
		name: "Confined in cellblock", description: "Confined in the cellblock.",
		requirements: "Cellblock is built.", enabled: ()=>App.Entity.facilities.cellblock.established,
		val: c => c.slave.assignment === Job.CELLBLOCK
	});
	gm.addAssignment("dj", {
		name: "DJ", description: "Serving as DJ.",
		requirements: "Club is built.", enabled: ()=>App.Entity.facilities.club.established,
		val: c => c.slave.assignment === Job.DJ
	});
	gm.addAssignment("club", {
		name: "Serving club", description: "Serving in the club.",
		requirements: "Club is built.", enabled: ()=>App.Entity.facilities.club.established,
		val: c => c.slave.assignment === Job.CLUB
	});
	gm.addAssignment("nurse", {
		name: "Nurse", description: "Serving as Nurse.",
		requirements: "Clinic is built.", enabled: ()=>App.Entity.facilities.clinic.established,
		val: c => c.slave.assignment === Job.NURSE
	});
	gm.addAssignment("clinic", {
		name: "Getting treatment", description: "Getting treatment in the clinic.",
		requirements: "Clinic is built.", enabled: ()=>App.Entity.facilities.clinic.established,
		val: c => c.slave.assignment === Job.CLINIC
	});
	gm.addAssignment("milkmaid", {
		name: "Milkmaid", description: "Serving as Milkmaid",
		requirements: "Dairy is built.", enabled: ()=>App.Entity.facilities.dairy.established,
		val: c => c.slave.assignment === Job.MILKMAID
	});
	gm.addAssignment("dairy", {
		name: "Work dairy", description: "Working in the dairy",
		requirements: "Dairy is built.", enabled: ()=>App.Entity.facilities.dairy.established,
		val: c => c.slave.assignment === Job.DAIRY
	});
	gm.addAssignment("farmer", {
		name: "Farmer", description: "Serving as Farmer",
		requirements: "Farmyard is built.", enabled: ()=>App.Entity.facilities.farmyard.established,
		val: c => c.slave.assignment === Job.FARMER
	});
	gm.addAssignment("farmyard", {
		name: "Farmhand", description: "Working as a farmhand.",
		requirements: "Farmyard is built.", enabled: ()=>App.Entity.facilities.farmyard.established,
		val: c => c.slave.assignment === Job.FARMYARD
	});
	gm.addAssignment("headgirlsuite", {
		name: "Head Girl Servant", description: "Living with the Head Girl.",
		requirements: "Head Girl Suite is built.", enabled: ()=>App.Entity.facilities.headGirlSuite.established,
		val: c => c.slave.assignment === Job.HEADGIRLSUITE
	});
	gm.addAssignment("concubine", {
		name: "Concubine", description: "Serving as Concubine.",
		requirements: "Master suite is built.", enabled: ()=>App.Entity.facilities.masterSuite.established,
		val: c => c.slave.assignment === Job.CONCUBINE
	});
	gm.addAssignment("mastersuite", {
		name: "Master suite servant", description: "Serving in the master suite.",
		requirements: "Master suite is built.", enabled: ()=>App.Entity.facilities.masterSuite.established,
		val: c => c.slave.assignment === Job.MASTERSUITE
	});
	gm.addAssignment("matron", {
		name: "Matron", description: "Serving as Matron.",
		requirements: "Nursery is built.", enabled: ()=>App.Entity.facilities.nursery.established,
		visible: () => V.experimental.nursery > 0,
		val: c => c.slave.assignment === Job.MATRON
	});
	gm.addAssignment("nursery", {
		name: "Nanny", description: "Working as a nanny.",
		requirements: "Nursery is built.", enabled: ()=>App.Entity.facilities.nursery.established,
		visible: () => V.experimental.nursery > 0,
		val: c => c.slave.assignment === Job.NURSERY
	});
	gm.addAssignment("teacher", {
		name: "Schoolteacher", description: "Serving as Schoolteacher.",
		requirements: "Schoolroom is built.", enabled: ()=>App.Entity.facilities.schoolroom.established,
		val: c => c.slave.assignment === Job.TEACHER
	});
	gm.addAssignment("school", {
		name: "Learning", description: "Learning in the schoolroom.",
		requirements: "Schoolroom is built.", enabled: ()=>App.Entity.facilities.schoolroom.established,
		val: c => c.slave.assignment === Job.SCHOOL
	});
	gm.addAssignment("steward", {
		name: "Stewardess", description: "Serving as Stewardess.",
		requirements: "Servants Quarters are built.",
		enabled: ()=>App.Entity.facilities.servantsQuarters.established,
		val: c => c.slave.assignment === Job.STEWARD
	});
	gm.addAssignment("quarter", {
		name: "Servant", description: "Working as a servant in the Servants Quarters.",
		requirements: "Servants Quarters are built.",
		enabled: ()=>App.Entity.facilities.servantsQuarters.established,
		val: c => c.slave.assignment === Job.QUARTER
	});
	gm.addAssignment("attendant", {
		name: "Attendant", description: "Serving as Attendant.",
		requirements: "Spa is built.", enabled: ()=>App.Entity.facilities.spa.established,
		val: c => c.slave.assignment === Job.ATTENDANT
	});
	gm.addAssignment("spa", {
		name: "Spa resting", description: "Resting in the spa.",
		requirements: "Spa is built.", enabled: () => App.Entity.facilities.spa.established,
		val: c => c.slave.assignment === Job.SPA
	});

	// Numbers
	gm.addNumber("devotion", {
		name: "Devotion",
		description: "Very Hateful: (-∞, -95), Hateful: [-95, -50), Resistant: [-50, -20), Ambivalent: [-20, 20], " +
			"Accepting: (20, 50], Devoted: (50, 95], Worshipful: (95, ∞)",
		val: c => c.slave.devotion
	});
	gm.addNumber("trust", {
		name: "Trust",
		description: "Extremely terrified: (-∞, -95), Terrified: [-95, -50), Frightened: [-50, -20), " +
			"Fearful: [-20, 20], Careful: (20, 50], Trusting: (50, 95], Total trust: (95, ∞)",
		val: c => c.slave.trust
	});
	gm.addNumber("health", {
		name: "Health",
		description: "Death: (-∞, -100), Near Death: [-100, -90), Extremely Unhealthy: [-90, -50), " +
			"Unhealthy: [-50, -20), Healthy: [-20, 20], Very Healthy: (20, 50], Extremely Healthy: (50, 90], " +
			"Unnaturally Healthy: (90, ∞)",
		val: c => c.slave.health.condition
	});
	gm.addNumber("fatigue", {
		name: "Fatigue",
		description: "Energetic: (-∞, 0], Rested: (0, 30], Tired: (30, 60], Fatigued: (60, 90], Exhausted: (90, ∞)",
		val: c => c.slave.health.tired
	});
	gm.addNumber("illness", {
		name: "Illness",
		description: "0: Not ill, 1: A little under the weather, 2: Slightly ill, can be treated at the clinic, " +
			"3: Ill, can be treated at the clinic, 4: Very ill, can be treated at the clinic, " +
			"5: Terribly ill, can be treated at the clinic",
		val: c => c.slave.health.illness
	});
	gm.addNumber("energy", {
		name: "Sex drive",
		description: "Frigid: (-∞, 20], Poor: (20, 40], Average: (40, 60], Powerful: (60, 80], " +
			"Sex Addict: (80, 100), Nympho: 100",
		val: c => c.slave.energy
	});
	gm.addNumber("weight", {
		name: "Weight",
		description: "Emaciated: (-∞, -95), Skinny: [-95, -30), Thin: [-30, -10), Average: [-10, 10], " +
			"Plush: (10, 30], Overweight: (30, 95], Fat: (95, 130], Obese: (130, 160], Super Obese: (160, 190], " +
			"Dangerously Obese: (190, ∞)",
		val: c => c.slave.weight
	});
	gm.addNumber("height", {name: "Height", description: "Slave height in cm.", val: c => c.slave.height});
	gm.addNumber("age", {name: "Age", description: "Real slave age", val: c => c.slave.actualAge});
	gm.addNumber("physicalAge", {
		name: "Body Age", description: "Age of the slave's body.",
		val: c => c.slave.physicalAge
	});
	gm.addNumber("visualAge", {
		name: "Visible Age", description: "How old the slave looks.",
		val: c => c.slave.visualAge
	});
	gm.addNumber("muscles", {
		name: "Muscles",
		description: "Frail: (-∞, -96), Very weak: [-96, -31], Weak: [-31, -6), Soft: [-6, 5), Toned: [5, 30), " +
			"Fit: [30, 50), Muscular: [50, 95), Hugely muscular: [95, ∞)",
		val: c => c.slave.muscles
	});
	gm.addNumber("lactation", {
		name: "Lactation", description: "0: None, 1: Natural, 2: Lactation implant",
		val: c => c.slave.lactation
	});
	gm.addNumber("pregType", {
		name: "Pregnancy Multiples", description: "Fetus count, known only after the 10th week of pregnancy",
		val: c => c.slave.pregType
	});
	gm.addNumber("pregWeeks", {
		name: "Pregnancy Progress", description: "Fetal development status, in weeks equivalent to unaccelerated pregnancy. Negative is postpartum.",
		val: c => c.slave.pregWeek
	});
	gm.addNumber("bellyImplant", {
		name: "Belly Implant", description: "Volume in CCs. None: -1",
		val: c => c.slave.bellyImplant
	});
	gm.addNumber("belly", {
		name: "Belly Size", description: "Volume in CCs, any source",
		val: c => c.slave.belly
	});
	gm.addNumber("intelligenceImplant", {
		name: "Education",
		description: "Education level. 0: uneducated, 15: educated, 30: advanced education, " +
			"(0, 15): incomplete education.",
		val: c => c.slave.intelligenceImplant
	});
	gm.addNumber("intelligence", {
		name: "Intelligence", description: "From moronic to brilliant: [-100, 100]",
		val: c => c.slave.intelligence
	});
	gm.addNumber("face", {
		name: "Face", description: "Very Ugly: (-∞, -96), Ugly: [-96, -40), Unattractive: [-40, -10), " +
			"Average: [-10, 10), Attractive: [10, 40), Beautiful: [40, 95), Very beautiful: [95, ∞)",
		val: c => c.slave.face
	});
	gm.addNumber("beautyscore", {
		name: "Beauty Score", description: "As seen in the slave description",
		val: c => Beauty(c.slave)
	});
	gm.addNumber("sexscore", {
		name: "Sexual Score", description: "As seen in the slave description",
		val: c => FResult(c.slave)
	});
	gm.addNumber("accent", {
		name: "Accent", description: "No accent: 0, Nice accent: 1, Bad accent: 2, Can't speak language: 3 and above",
		val: c => c.slave.accent
	});
	gm.addNumber("waist", {
		name: "Waist",
		description: "Absurdly narrow: (-∞, -95), Wasp waist: [-95, -40), Feminine waist: [-40, -10), " +
			"Average waist: [-10, 10], Unattractive waist: (10, 40], Ugly waist: (40, 95], Masculine waist: (95, ∞)",
		val: c => c.slave.waist
	});
	gm.addNumber("chem", {
		name: "Carcinogen Buildup",
		description: "Side effects from drug use. If greater than 10 will have negative consequences.",
		val: c => c.slave.chem
	});
	gm.addNumber("addict", {
		name: "Aphrodisiac Addiction",
		description: "No addiction: 0, Minor addiction: 1-2, Serious addiction: 3 and above",
		val: c => c.slave.addict
	});
	gm.addNumber("boobs", {
		name: "Breasts",
		description: "0-299: Flat, 300-399: A-cup, 400-499: B-cup, 500-649: C-cup, 650-799: D-cup, 00-999: DD-cup, " +
			"1000-1199: F-cup, 1200-1399: G-cup, 1400-1599: H-cup, 1600-1799: I-cup, 1800-2049: J-cup, " +
			"2050-2299: K-cup, 2300-2599: L-cup, 2600-2899: M-cup, 2900-3249: N-cup, 3250-3599: O-cup, " +
			"3600-3949: P-cup, 3950-4299: Q-cup, 4300-4699: R-cup, 4700-5099: S-cup, 5100-10499: Massive",
		val: c => c.slave.boobs
	});
	gm.addNumber("dick", {
		name: "Dick",
		description: "None: 0, Tiny: 1, Little: 2, Normal: 3, Big: 4, Huge: 5, Gigantic: 6, Massive: 7, Titanic: 8, " +
			"Monstrous: 9, Inhuman: 10, Hypertrophied: 11+",
		val: c => c.slave.dick
	});
	gm.addNumber("balls", {
		name: "Balls",
		description: "None: 0, Vestigial: 1, Small: 2, Average: 3, Large: 4, Massive: 5, Huge: 6, Gigantic: 7, " +
			"Enormous: 8, Monstrous: 9, Hypertrophied: 11+",
		val: c => c.slave.balls
	});
	gm.addNumber("clit", {
		name: "Clit",
		description: "Normal: 0, Large: 1, Huge: 2, Enormous: 3, Gigantic: 4, Massive: 5",
		val: c => c.slave.clit
	});
	gm.addNumber("labia", {
		name: "Labia",
		description: "Minimal: 0, Normal: 1, Large: 2, Huge: 3",
		val: c => c.slave.labia
	});
	gm.addNumber("lips", {
		name: "Lips",
		description: "Thin: (-∞, 10), Normal: [10, 20), Pretty: [20, 40), Plush: [40, 70), Huge (lisping): [70, 95), " +
			"Facepussy (mute): [95, ∞)",
		val: c => c.slave.lips
	});
	gm.addNumber("butt", {
		name: "Butt",
		description: "Flat: 0, Less flat: 1, Small: 2, Big: 3, Large: 4, Huge: 5, Enormous: 6, Gigantic: 7, " +
			"Ridiculous: 8, Immense: 9-10, Inhuman: 11-20",
		val: c => c.slave.butt
	});
	gm.addNumber("hips", {
		name: "Hips",
		description: "Very narrow: -2, Narrow: -1, Normal: 0, Wide hips: 1, Very wide hips: 2, Inhumanly wide hips: 3",
		val: c => c.slave.hips
	});
	gm.addNumber("shoulders", {
		name: "Shoulders",
		description: "Very narrow: -2, Narrow: -1, Feminine: 0, Broad: 1, Very broad: 2",
		val: c => c.slave.shoulders
	});
	gm.addNumber("vagina", {
		name: "Vagina Size",
		description: "No Vagina: -1, Virgin: 0, Tight: 1, higher numbers are looser.",
		val: c => c.slave.vagina
	});
	gm.addNumber("anus", {
		name: "Anus Size",
		description: "Virgin: 0, Tight: 1, higher numbers are looser.",
		val: c => c.slave.anus
	});
	gm.addNumber("wetness", {
		name: "Vaginal Wetness",
		description: "How easily the slave gets wet: dry: 0, wet: 1, soaking wet: 2",
		val: c => c.slave.vaginaLube
	});
	gm.addNumber("hormonebalance", {
		name: "Hormone Balance",
		description: "Absolutely masculine: (-∞, -500], Overwhelmingly masculine: (-500, -400], " +
			"Extremely masculine: (-400, -300], Heavily masculine: (-300, -200], Very masculine: (-200, -100], " +
			"Masculine: (-100, -20), Neutral: [-20, 20], Feminine: (20, 100), Very feminine: [100, 200), " +
			"Heavily feminine: [200, 300), Extremely feminine: [300, 400), Overwhelmingly feminine: [400, 500), " +
			"Absolutely feminine: [500, ∞)",
		val: c => c.slave.hormoneBalance
	});
	gm.addNumber("oralskill", {
		name: "Oral Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.oral
	});
	gm.addNumber("analskill", {
		name: "Anal Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.anal
	});
	gm.addNumber("vaginalskill", {
		name: "Vaginal Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.vaginal
	});
	gm.addNumber("penetrativeskill", {
		name: "Penetrative Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.penetrative
	});
	gm.addNumber("whoringskill", {
		name: "Whoring Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.whoring
	});
	gm.addNumber("entertainmentskill", {
		name: "Entertainment Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.entertainment
	});
	gm.addNumber("combatskill", {
		name: "Combat Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.combat
	});

	// Strings
	gm.addString("label", {name: "Label", description: "Assigned Label", val: c => c.slave.custom.label});
	gm.addString("genes", {
		name: "Genetic sex", description: "Genetic sex: Female: XX, Male: XY",
		val: c => c.slave.genes
	});
	gm.addString("phenotypesex", {
		name: "Phenotype sex",
		description: "A three-character string that encodes state of sexual organs" +
			" as female('X'), male('Y'), both ('H'), or absent ('-'): vagina/dick, ovaries/balls, tits",
		val: c => phenotypeSex(c.slave)
	});
	gm.addString("fetish", {
		name: "Fetish",
		description: "One of 'buttslut', 'cumslut', 'masochist', 'sadist', 'dom', 'submissive', 'boobs', " +
			"'pregnancy', 'none' (AKA vanilla)",
		val: c => c.slave.fetish
	});
	gm.addString("teeth", {
		name: "Teeth",
		description: "One of 'normal', 'crooked', 'gapped', 'straightening braces', 'cosmetic braces', 'removable', 'pointy', 'fangs', 'fang', 'baby', 'mixed'",
		val: c => c.slave.teeth
	});
	gm.addString("faceshape", {
		name: "Face Shape",
		description: "One of 'normal', 'masculine', 'androgynous', 'cute', 'sensual', 'exotic', 'feline' (catmod only)",
		val: c => c.slave.faceShape
	});
	gm.addString("nippleshape", {
		name: "Nipple Shape",
		description: "One of 'huge', 'puffy', 'inverted', 'tiny', 'cute', 'partially inverted', 'fuckable', 'flat'",
		val: c => c.slave.nipples
	});
	gm.addString("title", {
		name: "Title",
		description: "Slave title (class) without adjectives (slavegirl, MILF, bimbo, shemale, herm, etc.). ",
		requirements: "The alternate titles game option is enabled",
		enabled: () => V.newDescriptions === 1,
		val: c => SlaveTitle(c.slave, false, false)
	});
};

/**
 * @param {FC.SlaveState} slave
 * @param {FC.RA.PostFixRule} rule
 * @returns {boolean} If the rule should be applied to the given slave
 */
App.RA.Activation.evaluate = function(slave, rule) {
	const gm = App.RA.Activation.getterManager;
	const context = new App.RA.Activation.Context(slave);
	const stack = new App.RA.Activation.Stack();

	/**
	 * @type {Map<string, function(): void>}
	 */
	const operators = new Map([
		// and, or, +, * can take arbitrarily many arguments, so the first one describes the argument count
		["and", () => {
			const length = stack.popNumber();
			let value = 1;
			for (let i = 0; i < length; i++) {
				value &= stack.popAsBoolean();
			}
			stack.pushNumber(value);
		}],
		["or", () => {
			const length = stack.popNumber();
			let value = 0;
			for (let i = 0; i < length; i++) {
				value |= stack.popAsBoolean();
			}
			stack.pushNumber(value);
		}],
		["add", () => {
			const length = stack.popNumber();
			let value = 0;
			for (let i = 0; i < length; i++) {
				value += stack.popNumber();
			}
			stack.pushNumber(value);
		}],
		["mul", () => {
			const length = stack.popNumber();
			let value = 1;
			for (let i = 0; i < length; i++) {
				value *= stack.popNumber();
			}
			stack.pushNumber(value);
		}],
		["max", () => {
			const length = stack.popNumber();
			let value = stack.popNumber();
			for (let i = 1; i < length; i++) {
				value = Math.max(value, stack.popNumber());
			}
			stack.pushNumber(value);
		}],
		["min", () => {
			const length = stack.popNumber();
			let value = stack.popNumber();
			for (let i = 1; i < length; i++) {
				value = Math.min(value, stack.popNumber());
			}
			stack.pushNumber(value);
		}],
		["sub", () => {
			const subtract = stack.popNumber();
			stack.pushNumber(stack.popNumber() - subtract);
		}],
		["div", () => {
			const divisor = stack.popNumber();
			stack.pushNumber(stack.popNumber() / divisor);
		}],
		["eqstr", () => stack.pushBoolean(stack.popString() === stack.popString())],
		["neqstr", () => stack.pushBoolean(stack.popString() !== stack.popString())],
		["eqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())],
		["neqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())],
		["gt", () => stack.pushBoolean(stack.popNumber() < stack.popNumber())],
		["gte", () => stack.pushBoolean(stack.popNumber() <= stack.popNumber())],
		["lt", () => stack.pushBoolean(stack.popNumber() > stack.popNumber())],
		["lte", () => stack.pushBoolean(stack.popNumber() >= stack.popNumber())],
		["substr", () => {
			const value = stack.popString();
			stack.pushBoolean(stack.popString().includes(value));
		}],
		["match", () => {
			const value = stack.popString();
			stack.pushBoolean(stack.popString().match(value) !== null);
		}],
		["not", () => stack.pushBoolean(stack.popNumber() === 0)],
		["ternarystr", () => {
			const ifFalse = stack.popString();
			const ifTrue = stack.popString();
			stack.pushString(stack.popNumber() ? ifTrue : ifFalse);
		}],
		["ternarynum", () => {
			const ifFalse = stack.popNumber();
			const ifTrue = stack.popNumber();
			stack.pushNumber(stack.popNumber() ? ifTrue : ifFalse);
		}],
	]);

	/**
	 * Custom getters start with "?" and then "b", "n" or "s" depending on return type
	 * @param {string} rulePart
	 */
	function evalCustom(rulePart) {
		const expectedType = rulePart.charAt(1) === "b" ? "boolean"
			: rulePart.charAt(1) === "n" ? "number"
				: "string";
		try {
			// TODO: Can we cache the function (and is that useful)?
			const value = (new Function("c", `return (${rulePart.slice(2)})(c)`))(context);
			if (expectedType === "boolean") {
				stack.pushNumber(value ? 1 : 0);
			} else if (expectedType === "number") {
				stack.pushNumber(value);
			} else {
				// no guarantee we are getting a string, and it's easy to ensure it is one.
				stack.pushString(value.toString());
			}
		} catch (e) {
			throw new Error(`Custom condition '${rulePart.slice(2)}' failed: '${e.message}'`);
		}
	}

	for (const rulePart of rule) {
		if (typeof rulePart === "string") {
			const operation = operators.get(rulePart);
			if (operation !== undefined) {
				operation();
			} else {
				const result = gm.read(stack, context, rulePart);
				if (!result) {
					if (rulePart.startsWith("?")) {
						evalCustom(rulePart);
					} else {
						stack.pushString(rulePart.slice(1));
					}
				}
			}
		} else if (typeof rulePart === "number") {
			stack.pushNumber(rulePart);
		} else {
			stack.pushBoolean(rulePart);
		}
	}
	return !!stack.popNumber();
};


code:
App.RA.Activation.Editor = (function() {
	/**
	 * Should the advanced mode (tree editor) be used?
	 * @type {boolean}
	 */
	let advanced = false;
	/**
	 * Keep a reference to the outermost node, so we can refresh it when needed.
	 * @type {HTMLDivElement}
	 */
	let outerNode = null;

	/**
	 * @param {FC.RA.RuleConditionEditorArguments} args
	 * @returns {HTMLDivElement}
	 */
	function editor(args) {
		outerNode = document.createElement("div");
		fillOuterNode(args);
		return outerNode;
	}

	/**
	 * @param {FC.RA.RuleConditionEditorArguments} args
	 */
	function fillOuterNode(args) {
		advanced = args.advancedMode;
		let editorNode = document.createElement("div");
		let switchNode = document.createElement("div");
		switchNode.classList.add("condition-switch");
		outerNode.append(switchNode);
		if (advanced) {
			switchNode.append(App.UI.DOM.link("Reset to simple mode", () => {
				if (SugarCube.Dialog.isOpen()) {
					SugarCube.Dialog.close();
				}
				SugarCube.Dialog.create("Reset RA to simple mode");
				$(SugarCube.Dialog.body()).empty().append(
					"
Resetting will delete your current conditions. Do you want to continue?
",
					App.UI.DOM.link("Yes, delete conditions.", () => {
						args.advancedMode = false;
						args.activation = App.RA.newRule.conditions().activation;
						SugarCube.Dialog.close();
						$(outerNode).empty();
						fillOuterNode(args);
					}), " ",
					App.UI.DOM.makeElement("p", App.UI.DOM.link("Abort.", () => {
						SugarCube.Dialog.close();
					})));
				SugarCube.Dialog.open();
			}));
			App.RA.Activation.TreeEditor.build(args.activation, editorNode);
		} else {
			switchNode.append(App.UI.DOM.link("Switch to advanced mode", () => {
				args.advancedMode = true;
				$(outerNode).empty();
				fillOuterNode(args);
			}));
			App.RA.Activation.SimpleEditor.build(args.activation, editorNode);
		}
		switchNode.append(" | ", App.Encyclopedia.link("Help", "RA Condition Editor"));
		outerNode.append(editorNode);
	}

	/**
	 * Save the rule, if it is valid.
	 *
	 * @param {FC.RA.RuleConditionEditorArguments} args
	 */
	function saveEditor(args) {
		if (advanced) {
			let rule = App.RA.Activation.TreeEditor.save();
			if (rule == null) {
				return;
			}
			args.advancedMode = advanced;
			args.activation = rule;
		} else {
			let rule = App.RA.Activation.SimpleEditor.save();
			if (rule == null) {
				return;
			}
			args.advancedMode = advanced;
			args.activation = rule;
		}
	}

	function resetEditor() {
		App.RA.Activation.TreeEditor.reset();
		App.RA.Activation.SimpleEditor.reset();
		outerNode = null;
		advanced = false;
	}

	return {
		build: editor,
		save: saveEditor,
		reset: resetEditor,
		// Because of this reference we need to load after conditionEditorTree.js
		validateRule: App.RA.Activation.TreeEditor.validateRule,
	};
})();

repo_name: Nithhogg/fc

license: mit


code:
/**
 * @typedef {object} selectOption
 * @property {string} key
 * @property {string} name
 * @property {boolean} [enabled]
 */

/**
 * Creates a new dropdown with available and unavailable options.
 * @param {Array<selectOption>} options A list of options to display
 * @param {string} selected
 * @param {function(string):void} onchange
 *
 * @returns {HTMLSelectElement}
 */
App.UI.DOM.makeSelect = function(options, selected, onchange) {
	const select = document.createElement("select");
	let matchFound = false;

	for (const choice of options) {
		const option = document.createElement("option");

		option.text = choice.name;
		option.value = choice.key;

		if (selected === choice.key) {
			option.selected = true;
			matchFound = true;
		}

		if ("enabled" in choice && !choice.enabled) {
			option.disabled = true;
		}

		select.append(option);
	}

	if (!matchFound) {
		select.selectedIndex = -1;
	}

	select.onchange = () => onchange(select.value);

	return select;
};


code:
/* eslint no-unused-vars: [2, { "vars": "local" }] */
// cSpell:ignore yxxx, ØSTER

/* This file contains only JS functions without dependencies on FC specific variables/conventions and do not rely on
 * custom functions outside this file
 */

/**
 * Returns whether x is defined. Port of SugarCube's def.
 * @param {any} x
 * @returns {boolean}
 */
function jsDef(x) {
	return !(typeof x === "undefined" || x === null || x === undefined);
}

/**
 * @template {PropertyKey} T
 * @param {Object.<T, number>} obj
 * @param {number|string} [seed=undefined]
 * @returns {T}
 */
function hashChoice(obj, seed) {
	let randint = Math.floor(seededRandom(seed) * hashSum(obj));
	let ret;
	Object.keys(obj).some((key) => {
		if (randint < obj[key]) {
			ret = key;
			return true;
		} else {
			randint -= obj[key];
			return false;
		}
	});
	return ret;
}

/**
 * @param {Object.<PropertyKey, number>} obj
 * @returns {number}
 */
function hashSum(obj) {
	let sum = 0;
	Object.keys(obj).forEach((key) => {
		sum += obj[key];
	});
	return sum;
}

/**
 * @template {PropertyKey} T
 * @param {T[]} arr
 * @returns {Object.<T, number>}
 */
function arr2obj(arr) {
	/** @type {Object.<T, number>} */
	const obj = {};
	arr.forEach((item) => {
		obj[item] = 1;
	});
	return obj;
}

/**
 * @param {object} object
 * @param {PropertyKey[]} rest
 */
function hashPush(object, ...rest) {
	rest.forEach((item) => {
		if (object[item] === undefined) {
			object[item] = 1;
		} else {
			object[item] += 1;
		}
	});
}

/**
 * @param {Object.<PropertyKey, number>} obj
 * @param {Object.<PropertyKey, number>} other
 */
function hashMerge(obj, other) {
	Object.keys(other).forEach((key) => {
		if (obj[key] === undefined) {
			obj[key] = other[key];
		} else {
			obj[key] += other[key];
		}
	});
}

/**
 * @template {PropertyKey} T
 * @param {T[]} array
 * @returns {Object.<T, number>}
 */
function weightedArray2HashMap(array) {
	/** @type {Object.<T, number>} */
	const obj = {};
	array.forEach((item) => {
		if (obj[item] === undefined) {
			obj[item] = 1;
		} else {
			obj[item] += 1;
		}
	});
	return obj;
}

/**
 * generate a random, almost unique ID that is compliant (possibly) with RFC 4122
 *
 * @returns {string}
 */
function generateNewID() {
	// let date = Date.now(); // high-precision timer
	// let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
	// 	let r = (date + Math.random() * 16) % 16 | 0;
	// 	date = Math.floor(date / 16);
	// 	return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
	// });
	// return uuid;
	return generateUUID();
}

/**
 * Generate a random unique ID
 * Taken from https://stackoverflow.com/a/8809472
 * Compliant with https://www.ietf.org/rfc/rfc4122.txt
 * @returns {string} a RFC4122 V4 compliant UUID
 */
function generateUUID() { // Public Domain/MIT
	let d = Date.now(); // Timestamp
	let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0; // Time in microseconds since page-load or 0 if unsupported
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
		let r = Math.random() * 16; // random number between 0 and 16
		if (d > 0){ // Use timestamp until depleted
			r = (d + r)%16 | 0;
			d = Math.floor(d/16);
		} else { // Use microseconds since page-load if supported
			r = (d2 + r)%16 | 0;
			d2 = Math.floor(d2/16);
		}
		return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
	});
}

/**
 * Swaps two values in an array.
 * @param {Array} array
 * @param {number} indexA
 * @param {number} indexB
 */
function arraySwap(array, indexA, indexB) {
	const tmp = array[indexA];
	array[indexA] = array[indexB];
	array[indexB] = tmp;
}

/**
 * @param {string} string
 * @returns {string}
 */
function capFirstChar(string) {
	return string.charAt(0).toUpperCase() + string.substr(1);
}

/**
 * @param {string} string
 * @returns {string}
 */
function uncapFirstChar(string) {
	return string.charAt(0).toLowerCase() + string.substr(1);
}

/**
 * @param {string} word
 * @returns {string}
 */
function addA(word) {
	let vocal = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
	if (vocal.includes(word.charAt(0))) {
		return `an ${word}`;
	}
	return `a ${word}`;
}

/**
 * Takes a number and appends the appropriate suffix.
 * Example: `ordinalSuffix(1)` gives `1st`.
 * @param {number} i
 * @returns {string}
 */
function ordinalSuffix(i) {
	let j = i % 10;
	let k = i % 100;
	if (j === 1 && k !== 11) {
		return `${i}st`;
	}
	if (j === 2 && k !== 12) {
		return `${i}nd`;
	}
	if (j === 3 && k !== 13) {
		return `${i}rd`;
	}
	return `${i}th`;
}

/**
 * @param {number} i
 * @returns {string}
 * Takes a number and returns the appropriate ordinal.
 * Example: `ordinalSuffix(1)` gives `first`.
 * For number greater than 19 identical to `ordinalSuffix(i)`
 */
function ordinalSuffixWords(i) {
	const text = ["zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth"];
	if (i < text.length) {
		return text[i];
	}
	return ordinalSuffix(i);
}

/**
 * Takes an array and returns a new array without duplicate entries.
 * @template T
 * @param {Iterable<T>} array
 * @returns {T[]}
 */
function removeDuplicates(array) {
	return [...new Set(array)];
}

/**
 * Maps an index from one list onto a matching index on the other.
 * The first and last indexes will be matched to the first and last indexes of the other list,
 * while indexes in between will go to the nearest index.
 * @param {number} index The index in original list to map to new list.
 * @param {*} originalList The original list the index refers into.
 * @param {*} newList The new list which we want an index for
 * @returns {number} The new index into newList
 */
App.Utils.mapIndexBetweenLists = function(index, originalList, newList) {
	if (index === 0) { return 0; }
	if (index === originalList.length - 1) { return newList.length - 1; }
	index--;
	const originalLimitedLength = originalList.length - 2;
	const newLimitedLength = newList.length - 2;
	return Math.round((index / originalLimitedLength) * newLimitedLength) + 1;
};

/**
 * replaces special HTML characters with their '&xxx' forms
 * @param {string} text
 * @returns {string}
 */
App.Utils.escapeHtml = function(text) {
	const map = {
		'&': '&',
		'<': '<',
		'>': '>',
		'"': '"',
		"'": '''
	};
	return text.replace(/[&<>"']/g, m => map[m]);
};

App.Utils.expandHTML = (function() {
	const element = document.createElement("div");

	/**
	 * @param {string} s
	 * @returns {string}
	 */
	function expand(s) {
		element.innerHTML = s;
		return element.textContent;
	}

	return expand;
})();

/**
 * Creates an object where the items are accessible via their ids.
 *
 * @param {Iterable} list
 * @returns {{}}
 */
function mapIdList(list) {
	let mappedList = {};
	for (const item of list) {
		mappedList[item.id] = item;
	}
	return mappedList;
}

/**
 * Topological sorting algorithm
 * https://gist.github.com/shinout/1232505
 * Added keys parameter since it better suits our needs and updated to project code style.
 *
 * @template {PropertyKey} T
 * @param {T[]} keys
 * @param {T[][]} edges list of edges. each edge forms Array<ID,ID> e.g. [12, 3]
 * @returns {T[]} Array: topological sorted list of IDs
 * @throws Error: in case there is a closed chain.
 **/
App.Utils.topologicalSort = function(keys, edges) {
	class Node {
		/** @param {T} id */
		constructor(id) {
			this.id = id;
			/** @type {T[]} */
			this.afters = [];
		}
	}

	/** @type {Object.<T, Node>} */
	let nodes = {}; // hash: stringified id of the node => { id: id, afters: list of ids }
	let sorted = []; // sorted list of IDs ( returned value )
	/** @type {Object.<T, boolean>} */
	let visited = {}; // hash: id of already visited node => true

	// 1. build data structures
	keys.forEach(key => {
		nodes[key] = new Node(key);
	});

	edges.forEach(edge => {
		const from = edge[0];
		const to = edge[1];
		if (!nodes[from]) { nodes[from] = new Node(from); }
		if (!nodes[to]) { nodes[to] = new Node(to); }
		nodes[from].afters.push(to);
	});

	/** 2. topological sort with visitor
	 * @param {string} idStr
	 * @param {number[]} [ancestors]
	 */
	function visit(idStr, ancestors = []) {
		let node = nodes[idStr];
		let id = node.id;

		// if already exists, do nothing
		if (visited[idStr]) { return; }

		ancestors.push(id);

		visited[idStr] = true;

		node.afters.forEach((afterID) => {
			if (ancestors.indexOf(afterID) >= 0) { // if already in ancestors, a closed chain exists.
				throw new Error('closed chain : ' + afterID + ' is in ' + id);
			}

			visit(afterID.toString(), ancestors.map(v => v)); // recursive call
		});

		sorted.unshift(id);
	}
	Object.keys(nodes).forEach(node => visit(node));

	return sorted;
};

/**
 * Sorts an array of objects based on the order of the first array.
 * Sorts by values accessible by unsorted[key]
 * Values of the second array must be a subset of sorted
 *
 * @param {Array<any>} sorted
 * @param {Array<object>} unsorted
 * @param {string} key
 * @returns {Array<object>}
 */
function sortArrayByArray(sorted, unsorted, key) {
	const map = {};
	sorted.forEach((value, index) => {
		map[value] = index;
	});

	return unsorted.sort((a, b) => {
		return map[a[key]] - map[b[key]];
	});
}

/**
 * @param {object} target
 * @param {object} source
 */
function deepAssign(target, source) {
	_.merge(target, _.cloneDeep(source));
}

/**
 * Returns the median value for an array
 *
 * For more information about mean vs. median see
 * https://www.clinfo.eu/mean-median/
 * @param {number[]} arr Does not need to be sorted
 * @returns {number}
 */
function median(arr = []) {
	const mid = Math.floor(arr.length / 2);
	const numbers = [...arr].sort((a, b) => a - b);

	return arr.length % 2 === 0 ? (numbers[mid] + numbers[mid - 1]) / 2 : numbers[mid];
}

/**
 * Returns value for point x, computed assuming linear function defined
 * via two points (x0,y0) and (x1,y1)
 * @param {number} x
 * @param {number} x0
 * @param {number} y0
 * @param {number} x1
 * @param {number} y1
 * @returns {number}
 */
function linearInterpolation(x, x0, y0, x1, y1) {
	return y0 + (x - x0) * ((y1 - y0) / (x1 - x0));
}

/**
 * Convert a Blob to a base64 string
 * @param {Blob} blob
 */
async function blobToBase64(blob) {
	return new Promise((resolve, reject) => {
		const reader = new FileReader();
		reader.onerror = (e) => reject(reader.error);
		reader.onloadend = (e) => {
			const dataUrl = /** @type {string} */ (reader.result);
			// remove mime type prefix from data url
			const base64 = dataUrl.substring(dataUrl.indexOf(',') + 1);
			resolve(base64);
		};
		reader.readAsDataURL(blob);
	});
}

/**
 * Takes a given string and converts it to title case.
 * "hello world" -> "Hello World".
 * "7th time's the charm for jim-bob" -> "7th Time's The Charm For Jim-Bob".
 * "ØSTER HUS AS" -> "Øster Hus As".
 * "f" -> "F".
 * Taken from https://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript#comment138081811_196991 and converted into a function
 * @param {string} str
 * @returns {string}
 */
globalThis.toTitleCase = (str) => {
	return str.replace(/[^-\s]+/g, s => s.charAt(0).toUpperCase() + s.substring(1).toLowerCase());
};


code:
@echo off

:: Set working directory
pushd %~dp0

:: See if we can find a git installation
set GITFOUND=no
for %%k in (HKCU HKLM) do (
	for %%w in (\ \Wow6432Node\) do (
		for /f "skip=2 delims=: tokens=1*" %%a in ('reg query "%%k\SOFTWARE%%wMicrosoft\Windows\CurrentVersion\Uninstall\Git_is1" /v InstallLocation 2^> nul') do (
			for /f "tokens=3" %%z in ("%%a") do (
				set GIT=%%z:%%b
				set GITFOUND=yes
				goto FOUND
			)
		)
	)
)
:FOUND
if %GITFOUND% == yes (
	set "PATH=%GIT%bin;%PATH%"
	echo Running legacySanityCheck.sh using git bash...
	bash --login -c "./legacySanityCheck.sh"
) else (
	echo Could not find git installation
)

:: keep window open instead of closing it
<nul set /p "=Press any key to exit"
pause >nul
popd


code:
#!/bin/bash
if [ ! -d ".git" ]; then
	#not running in git repo, so can't use git commands :-)
	echo "No .git repo found - skipping sanity checks"
	exit 0
fi

WARNING='\033[93m'

myprint() {
	while read -r data; do
		echo -n -e "[$1]$WARNING"
		echo "$data"
	done
}

GREP="git grep -n --color"

# Check for accidental assignment.  e.g.:   <<if $foo = "hello">>
$GREP "<<[ ]*if[^>=]*[^><\!=]=[^=][^>]*>>" -- 'src/*' | myprint "AccidentalAssignmentInIf"
# Check for accidental assignment.  e.g.:   <<elseif $foo = "hello">>
$GREP "<<[ ]*elseif[^>=]*[^><\!=]=[^=][^>]*>>" -- 'src/*' | myprint "AccidentalAssignmentInElseIf"
# Check for missing ".  e.g.:   <<if $foo == "hello>>
$GREP "<<[^\"<>]*\"[^\"<>]*>>" -- 'src/*' | myprint "MissingSpeechMark"
# Check for missing ".  e.g.:   <<if $foo = "hello)
$GREP -e "<<[^\"<>]*\([^\"<>]*\"[^><\"]*\"\| [<>] \)*\"\([^\"<>]*\"[^><\"]*\"\| [<>] \)*\([^\"<>]\| [<>] \)*>>" --and --not -e "*[^']*" -- 'src/*' | myprint "MissingSpeechMark2"
# Check for colors like: @@color:red   - should be @@.red
$GREP -e "@@color:" --and --not -e "@@color:rgb([0-9 ]\+,[0-9 ]\+,[0-9 ]\+)" -- "src/*" | myprint "UseCSSColors"
# Check for missing $ in activeSlave or PC
$GREP "<<[ ]*[^\$><_\[]*\(activeSlave\|PC\)[.]" -- "src/*" | myprint "MissingDollar"
# Check for closing bracket without opening bracket.  e.g.:  <<if foo)>>	  (but  <<case "foo")>>   is valid, so ignore those
$GREP -e "<<[ a-zA-Z]\+\([^()<>]\|[^()<>][<>][^()<>]\)*)" --and --not -e "<< *case" -- "src/**/*.tw" | myprint "MissingOpeningBracket"
# Check for opening bracket without closing bracket.  e.g.:  <<if (foo>>
$GREP -e "<<[ a-zA-Z]\([^<>]\|[^<>][<>][^<>]\)\+(\([^()<>]\|[^<>()][<>][^<>()]\|([^<>()]*])\)*>>" -- "src/**/*.tw" | myprint "MissingClosingBracket"
# Check for two closing brackets but one opening bracket.  e.g.:  <<if (foo))>>
$GREP -e "<<[ a-zA-Z]\+[^()<>]*([^()]*)[^()]*)[^()<>]*>>" -- "src/**/*.tw" | myprint "MissingOpeningBracket2"
# Check for one closing bracket but two opening brackets.  e.g.:  <<if ((foo)>>
$GREP -e "<<[ a-zA-Z]\+[^()<>]*([^()]*([^()]*)[^()<>]*>>" -- "src/**/*.tw" | myprint "MissingClosingBracket2"
$GREP -e "<<.*[(][^<>)]*[(][^<>)]*)\?[^<>)]*>>" -- "src/**/*.tw" | myprint "MissingClosingBracket3"
# Check for too many >>>.  e.g.: <</if>>>
$GREP "<<[^<>]*[<>]\?[^<>]*>>>" -- "src/**/*.tw" | myprint "TooManyAngleBrackets"
# Check for wrong capitalization on 'activeslave' and other common typos
$GREP -e "\$act" --and --not -e "\$\(activeSlave\|activeChild\|activeArcology\|activeOrgan\|activeLimbs\|activeLurcher\|active.\)" -- "src/*" | myprint "WrongCapitalization"
# Check for strange spaces e.g.  $slaves[$i]. lips
$GREP "\$slaves\[\$i\]\. " -- 'src/*' | myprint "MissingPropertyAfterSlaves"
# Check using refreshmentType instead of refreshment
$GREP "\$PC.refreshmentType[^ =]" -- 'src/*' | grep -v src/events/intro/introSummary.tw | myprint "ShouldBeRefreshment"
# Check, e.g.  <<else $foo==4
$GREP "<<else >\?[^>]" -- 'src/*' | myprint "ShouldBeElseIf"
# Check, e.g., =to
$GREP "=to" -- 'src/*' | myprint "EqualAndTo"
# Check doing  $slaves.foo instead of $slaves[i].foo
$GREP -e "[$]slaves[.]" --and --not -e '[$]slaves[.]\(length\|random\|map\|filter\|deleteAt\|push\|find\|includes\|delete\|forEach\|some\|reduce\|splice\)' -- 'src/*' | myprint "MissingSlavesIndex"
# Try to check for accidentally mixing slaves[] and activeSlave.  This can have a lot of false matches, but has caught a lot of bugs so it's worth the pain
$GREP -e "activeSlave[.]" --and -e "slaves\[..\?\][.]" --and --not -e '[.]ID' --and --not -e 'slaves\[..\?\][.]\(slaveName\|slaveSurname\|actualAge\|relation\|assignment\|age\|devotion\|trust\|vagina\|mother\|father\|training\)' -- 'src/*' | myprint "MaybeAccidentalMixingOfSlavesAndActiveSlave"
# Check, e.g.  <<set foo == 4>>
$GREP "<<set[^{>=]*==" -- 'src/*' | myprint "DoubleEqualsInSet"
# Check for, e.g   <<if slaves[foo]>>
$GREP "<<\([^>]\|[^>]>[^>]\)*[^$]slaves\[" -- 'src/*' | myprint "MissingDollar"
# Check for missing $ or _ in variable name:
$GREP -e "<<[a-zA-Z]\([^>\"]\|[^>]>[^>]\|\"[^\"]*\"\)* [a-zA-Z]\+ * =" -- "src/**/*.tw" | myprint "MissingDollar2"
# Check for missing command, e.g.  <<foo =
$GREP -e "<<[a-zA-Z]* = *" -- "src/**/*.tw" | myprint "BadCommand"
# Check for duplicate words, e.g. with with
$GREP -e " \(\b[a-zA-Z][a-zA-Z]\+\) \1\b " --and --not -e " her her " --and --not -e " you you " --and --not -e " New New " --and --not -e "Slave Slave " --and --not -e " that that " --and --not -e " in in " --and --not -e " is is " -- 'src/*' ':!*.css' | myprint "Duplicate words"
# Check for obsolete SugarCube macros
$GREP -E "<<display |<<click|<<.*\.contains" -- "src/**/*.tw" | myprint "ObsoleteMacro"
# Check for double articles
$GREP -E "\Wa an\W" -- "src/**/*.tw" | myprint "DoubleArticle"
# Check for incorrect articles
$GREP -i -E "\Wa (a|e|i|o|u)." -- "src/**/*.tw" | grep -a -i -vE "\Wa (un|eu|us|ut|on|ur|in)." | grep -a -i -vE "(&|<<s>>|UM)." | myprint "IncorrectArticle"
$GREP -i -E "\Wan (b|c|d|f|g|j|k|l|m|n|p|q|r|s|t|v|w|x|y|z)\w." -- "src/**/*.tw" | grep -a -i -vE "[A-Z]{3}" | myprint "IncorrectArticle"
# Check for $ sign mid-word
$GREP -i "\w$\w" -- "src/**/*.tw" | myprint "VarSignMidWord"
# check for $ sign at beginning of macro
$GREP '<<\s*\$' -- 'src/*' | myprint "VarSignAtMacroStart"
# check our custom option macro is either options,option,optionlt,optionlte,optiongt,optiongte,optiondefault,optionstyle,optionif
$GREP -e '<<option' --and --not -e "<<option\(\|s\|lt\|lte\|gt\|gte\|default\|style\|if\)[ >]" -- 'src/*' | myprint "OptionUnrecognized"
# check our custom option macro is: <<option variable "somestring"
$GREP -e "<<option[lg]te\? " --and --not -e "<<option[lg]te\?\s\+-\?[0-9]\+\s\+-\?[0-9]\+\s\+[\'\"].*[\'\"]" -- 'src/*' | grep -v src/js | myprint "OptionBadArguments1"
$GREP -e "<<optiondefault " --and --not -e "<<optiondefault\s\+\(-\?[0-9]\+\|[\'\"].*[\'\"]\|false\|true\)\s\+[\'\"].*[\'\"]" -- 'src/*' | grep -v src/js | myprint "OptionBadArguments2"
$GREP -e "<<option\([lg]t\?\|default\) *>" -- 'src/*' | grep -v src/js | myprint "OptionBadArguments3"
#$GREP -e "<<option " --and --not -e "<<option\s\+\(-\?[0-9]\+\|[\'\"].*[\'\"]\|false\|true\)\s\+[\`\'\"].*[\'\"\`]" -- 'src/*' | grep -v src/js | myprint "OptionBadArguments4" # too many false positives
$GREP -e "<<if def [^(>]*[&|]" -- 'src/*' | myprint "AddBracketsAroundDef2"
# check for missing ; before statement
$GREP 'if $ ' -- 'src/*' | myprint "Missing ; before statement"
$GREP 'elseif $ ' -- 'src/*' | myprint "Missing ; before statement"
# Check for an unrecognized letter before >>
$GREP "[^]a-zA-Z0-9 \")}'+-\*\`] *>>" -- 'src/*' | myprint "StrangeCharacterAtEndOfCommand"
# Check for a . inside a <<>>
$GREP "<<[a-zA-Z]\([^\"'>]\|[^\"'>]>[^\"'>]\)*[a-zA-Z][.][^a-zA-Z]" -- "src/**/*.tw"| myprint "StrangeCharacterAfterDot"
# Check for @@.  instead of .@@
$GREP -E "@@(\.|,|;|:)\s" -- "src/**/*.tw" | myprint "WrongSelectorPunctuation"
$GREP "@@[a-z]\+;" -- 'src/*' | myprint "SelectorMissingDot"
# Check for .  instead of .
$GREP -E "(\.|,|;|:)\s" -- src/*.js | myprint "WrongSelectorPunctuation"
# Check for missing whitespace between operators
#$GREP -E "\(.(\+|\-|\*|\/|\=)." -- src/*.js :^src/001-lib/Jquery.js,^src/js/dTree.min.js,^devTools/tweeGo/storyFormats/sugarcube-2/format.js,^src/001-lib/mousetrap.js | myprint "MissingWhitespace"		disabled until I can figure out how to exclude files
# Check for @@ selector instead of  selector
$GREP "@@\." -- src/*.js | myprint "WrongSelectorUsed"
# Check for JSdoc inside function declaration
#$GREP -e ".\=.\/\*\*" --or -e "slave\s\*\/" -- src/*.js --exclude 'src/interaction/main/mainLinks.js' | myprint "WrongJSdocFormat"	# this --exclude doesn't work # too many false positives
# Check for missing whitespace at end of /**/ style comments
#$GREP "\S\*\/" -- src/* --exclude 'src/001-lib/jquery/Jquery.js' | myprint "MissingWhitespace"		disabled until I can figure out to exclude files
# Check for var instead of let or const
#$GREP "var\s" -- src/*.js | myprint "UseLetOrConst"	disabled until I can figure out how to exclude certain files
# Check for non lethal or non-lethal instead of nonlethal
$GREP -i "non.lethal" -- 'src/*' | myprint "ShouldBeNonlethal"
# Check for missing quotation marks around selectors
$GREP "span class=[^\"\']" -- src/*.js ':!src/001-lib/Jquery/Jquery.js' | myprint "MissingQuotes"

(
	cd src/ || exit
	$GREP "\$\(PC\|activeSlave\|slaves\|tanks\)[.][^a-zA-Z]" | myprint "UnexpectedCharAfterDot"
)
# Check for lines with only "let"
$GREP '[^A-Za-z]let(\r|\n)' | myprint "WrongLetStyle"
# Check for lines with only "const"
$GREP 'const(\r|\n)' | myprint "WrongConstStyle"
# Check for events with mismatched names
$GREP -P 'App\.Events\.(.*)\s*=\s*class\s+(?!\1)(.*)(?=\s+extends\s+App\.Events\.BaseEvent)' -- src/*.js | myprint "MismatchedEventName"

# Check that all the tags are properly opened and closed & a lot of other stuff
java -jar devTools/javaSanityCheck/SanityCheck.jar


code:
# Custom Drugs

To add custom drugs through modding, pass object(s) with the following properties to the `App.Mods.Drug.add` function. More than one drug can be added at a time by using multiple arguments.

## Custom Drug Object Properties

- `name` (String): The name of the drug, should be unique(however a PC and a slave drug under the same name will be fine) and not conflict with existing drugs. This is displayed in menus.
- `text` (String): The text used in the link to select the drug. Typically the first letter is in upper case.
- `type` (String): The category of the drug, used in the slave interaction menu. Can be an existing vanilla category(`Lips`, `Breasts`, `Nipples`, `Butt`, `Dick`/`Clit`(Will change automatically depending on slave genital type), `Fertility`, `Hormones`, `Psych`, `Misc`), or entirely new. Not required for PC drugs.
- `isPCDrug` (Boolean, optional): If the drug is for PC instead of slaves, default to `false`.
- `isConsumerGrade` (Boolean, optional): Lists the drug as consumer grade in manage personal affairs if true, other wise listed as slave grade, default to `false`. Only used is the drug is for PC.
- `available` (Function → *boolean*): If the drug should be displayed at all. The slave object will be passed as the first parameter.
- `enable` (Function → true or *string*): If the drug link should be enabled. The slave object will be passed as the first parameter. If the drug is allowed, return `true`. If not, return a string, which will be displayed to the player as a tool tip.
- `effect` (Function → *string* or *object*): The effect of the drug. The slave object will be passed as the first parameter. The returned string will be displayed in end week report.

## Example:
```js
App.Mods.Drugs.add(
	{
		name: "hip enhancer",
		text: "Hip enhancer",
		type: "Structural",
		available: function (slave) {
			return slave.indentureRestrictions < 2;
		},
		enable: function (slave) {
			//Disable if the slave's hip is already very wide or wider.
			if (slave.hips < 2) return true;
			else return "Hips are already too wide";
		},
		effect: function (slave) {
			//Get the pronouns of the slaves to be used in end week report.
			const { he, him, his, himself, He, His } = getPronouns(slave);
			if (slave.hips >= 2) {
				//If the slave's hips is already very wide, stop the drug regimen, notify in end week report.
				slave.drugs = "no drugs";
				return ` ${His} hips is already as wide is possible, <span class="yellow">${his} drug regimen has been ended.</span>`;
			} else {
				//If not, widen hips and notify in end week report.
				slave.hips++;
				return ` The hip enhancer <span class="lime">widens ${his} hip</span>.`;
			}
		}
	},
	/* More than one drug can be added at a time */
	{
		//Drug name should be unique, but two drug with identical name is okay if one is for PC and one is for slaves.
		name: "hip enhancer",
		text: "Hip enhancer",
		//PC Drugs do not require type
		isPCDrug: true,
		isConsumerGrade: true,
		available: () => true,
		enable: function (slave) {
			if (slave.hips < 2) return true;
			else return "Your hips are already too wide";
		},
		effect: function (slave) {
			if (slave.hips >= 2) {
				slave.drugs = "no drugs";
				return "Your hips is already as wide is possible, <span class=\"yellow\">your drug regimen has ended.</span>";
			} else {
				slave.hips++;
				return "The hip enhancer <span class=\"lime\">widens your hip</span>.";
			}
		}
	}
);
```

path: mods/custom drugs.md


code:
# Custom Events

To add a custom event(s) through modding, add `new App.Events.EVENT_NAME()` (following the file structure laid out within `devNotes\jsEventCreationGuide.md` is strongly recommended) to the applicable array.

## Example

`EVENT_NAME` is bigTitsPlease.
To add this as a new `random` `individual` event place `App.Mods.events.random.individual.push(new App.Events.bigTitsPlease())` within the `index.js` outlined as apart of `mods\mods.md`.

## Array list and notes

- `App.Mods.events.nonRandom` - hooks into the end of `App.Events.getNonrandomEvents` (`src\events\nonRandomEvent.js`) which has the following warning `// ORDER MATTERS - if multiple events from this list trigger in a single week, they are executed in this order`.

- `App.Mods.events.random.individual` - hooks into the end of `App.Events.getIndividualEvents` (`src\events\randomEvent.js`), `// instantiate all possible random individual events here`.

- `App.Mods.events.random.nonIndividual` - hooks into the end of `App.Events.getNonindividualEvents` (`src\events\randomEvent.js`), `// instantiate all possible random nonindividual events here` and `Note: recruitment events should NOT be added to this list; they go in getNonindividualRecruitmentEvents instead.`.

- `App.Mods.events.random.recruitment` - hooks into the end of `App.Events.getNonindividualRecruitmentEvents` (`src\events\randomEvent.js`).

- `App.Mods.events.recruit` - hooks into the end of `this.eventList` (`App.Events.RERecruit` - `src\events\reRecruit.js`).


code:
// Always scope your code to not pollute the global namespace.
{
	// Get the mod so we can modify it:
	const mod = App.Modding.currentMod;

	// Set the name and version:
	mod.name = "Example Mod";
	mod.version = "1.0";
	mod.description = "This is an example mod showing off the modding API and gives examples for moddable systems.";

	// For bigger mods it can be useful to split the mod in multiple parts.
	// These parts can then be loaded as subscripts.
	// Subscripts will be loaded in series and only once this script is done.
	mod.addSubscript("subscript");

	// Usage examples for moddable systems
	mod.addSubscript("rulesAssistantGetters");

	// Write to console, so we can easily see that the mod has loaded:
	console.log("Index Loading: Success!");

	// Tell the modding system this file is done. Do this at the end of EVERY script file.
	App.Modding.scriptDone();
}


code:
// Add a new boolean getter for use in the RA condition editor.
// The first argument is a unique name for the getter. To ensure uniqueness with other mods it is recommended to
// prefix the name with the mod name.
// The second argument is an object describing the getter.
// 'name' is the string shown in the RA condition editor when selecting.
// 'description' should be a short description of the value the getter returns, with possible values if applicable.
//      It will be displayed in the encyclopedia.
// 'val' is the actual getter reading out the value and returning it. It operates on a 'context' object with the current
//      slave as an attribute.
// It is important that the getter always returns the data type that it is being added as, in this case 'string'.
// 'addNumber()' and 'addBoolean()' also exist. The usage is the same.
App.RA.Activation.getterManager.addString("example_slavename",
	{name: "Slave Name", description: "The slave's name.", val: context => context.slave.slaveName}
);

console.log("Rule Assistant Getters loaded.");

App.Modding.scriptDone();


code:
// Show the subscript loaded successfully:
console.log("Subscript loaded!");

// Tell the modding system this file is done. Do this at the end of EVERY script file.
App.Modding.scriptDone();


code:
# Player Mods

Player mods are scripts that are loaded after loading the game itself.

There is currently very limited support for these mods with few systems exposing an API for modding.

To use the mods in this directory copy the `mods` directory directly next to the game HTML file like this:

* `FC_pregmod.html`
* `mods/`
    * `example/`
        * `index.js`
        * ...
    * ...

The example mod explains the modding API itself and has examples for all moddable systems.


free-cities

description: The Free Cities game

type: module

type: git

serve: npx http-server --port 6969 -c-1

watch: node devTools/scripts/watcher.js

build: npx gulp all

build:debug: npx gulp all --debug

build:release: npx gulp all --minify --release

extra:all: node devTools/scripts/customChecks.js

spell:all: npx cspell --show-context --show-suggestions --gitignore --color "**/*.{js,md,d.ts}"

lint: node devTools/scripts/eslintChecks.js --changed && node devTools/scripts/typescriptChecks.js --changed

lint:all: node devTools/scripts/eslintChecks.js && node devTools/scripts/typescriptChecks.js

sanity: node devTools/scripts/sanityCheck.js

compile: sh compile.sh


code:
There are actually two places to add art for FreeCities.  The folder you are currently in, ./resources, is mostly for the git.  Once you compile an HTML, the HTML won't check here for art.

If you are interested in adding the old renders or your own custom art to your local version of FreeCities, then you actually want to have a resources folder in the same folder FC_pregmod.html is in.  By default, that will be ./bin/resources.  If you want any of the art in this folder, make sure to copy it over there.

Two different resources folders, and both connected to art.  But they work very differently.

P.S. make sure you don't stack resources folders, like resources/resources.  That would just be silly.


path: resources/vector/body/addon/boob 0 areola piercing.svg


path: resources/vector/body/addon/boob 0 piercing heavy.svg


path: resources/vector/body/addon/boob 1 areola piercing.svg


path: resources/vector/body/addon/boob 1 piercing heavy.svg


path: resources/vector/body/addon/boob 2 areola piercing.svg


path: resources/vector/body/addon/boob 2 piercing heavy.svg


path: resources/vector/body/addon/boob 3 areola piercing.svg


path: resources/vector/body/addon/boob 3 piercing heavy.svg


path: resources/vector/body/addon/boob 4 areola piercing.svg


path: resources/vector/body/addon/boob 4 piercing heavy.svg


path: resources/vector/body/addon/boob 5 areola piercing.svg


path: resources/vector/body/addon/boob 5 piercing heavy.svg


path: resources/vector/body/addon/boob 6 areola piercing.svg


path: resources/vector/body/addon/boob 6 piercing heavy.svg


path: resources/vector/body/addon/boob 7 areola piercing.svg


path: resources/vector/body/addon/boob 7 piercing heavy.svg


code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
	.st0{fill:#787878;}
</style>
<g id="Clit_Piercing">
	<circle id="XMLID_257_" class="st0" cx="488.3" cy="435.6" r="1.2"/>
	<circle id="XMLID_256_" class="st0" cx="483.6" cy="436.1" r="1.2"/>
</g>
</svg>


path: resources/vector/body/addon/hair aft neat auburn.svg


path: resources/vector/body/addon/hair aft neat black.svg


path: resources/vector/body/addon/hair aft neat blonde.svg


path: resources/vector/body/addon/hair aft neat blue.svg


path: resources/vector/body/addon/hair aft neat brown.svg


path: resources/vector/body/addon/hair aft neat burgundy.svg


path: resources/vector/body/addon/hair aft neat chestnut.svg


path: resources/vector/body/addon/hair aft neat chocolate.svg


path: resources/vector/body/addon/hair aft neat copper.svg


path: resources/vector/body/addon/hair aft neat dark brown.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair aft neat darkbrown.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1920"
	 inkscape:window-height="1001"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-1508.4746"
	 inkscape:cy="500"
	 inkscape:window-x="-9"
	 inkscape:window-y="-9"
	 inkscape:window-maximized="1"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Aft_7_"
	 class="st0"
	 d="m 568.83729,121.38644 c 13.5,72.4 34.5,66.5 14,103.3 -43.3,47.7 -90.1,7 -98.1,-9.8 -41.1,-23.4 -0.3,-140.799999 48.3,-130.299999 33.3,7.2 33.3,19.499999 35.8,36.799999"
	 inkscape:connector-curvature="0"
	 style="fill:#463325;fill-opacity:1;" /></svg>

path: resources/vector/body/addon/hair aft neat darkbrown.svg


path: resources/vector/body/addon/hair aft neat dirty blonde.svg


path: resources/vector/body/addon/hair aft neat ginger.svg


path: resources/vector/body/addon/hair aft neat golden.svg


path: resources/vector/body/addon/hair aft neat green.svg


path: resources/vector/body/addon/hair aft neat grey.svg


path: resources/vector/body/addon/hair aft neat hazel.svg


path: resources/vector/body/addon/hair aft neat pink.svg


path: resources/vector/body/addon/hair aft neat platinum blonde.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair aft neat platinumblonde.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1920"
	 inkscape:window-height="1001"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-1527.5424"
	 inkscape:cy="500"
	 inkscape:window-x="-9"
	 inkscape:window-y="-9"
	 inkscape:window-maximized="1"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Aft_7_"
	 class="st0"
	 d="m 568.83729,121.38644 c 13.5,72.4 34.5,66.5 14,103.3 -43.3,47.7 -90.1,7 -98.1,-9.8 -41.1,-23.4 -0.3,-140.799999 48.3,-130.299999 33.3,7.2 33.3,19.499999 35.8,36.799999"
	 inkscape:connector-curvature="0"
	 style="fill:#fcf3c1;fill-opacity:1" /></svg>

path: resources/vector/body/addon/hair aft neat platinumblonde.svg


path: resources/vector/body/addon/hair aft neat purple.svg


path: resources/vector/body/addon/hair aft neat red.svg


path: resources/vector/body/addon/hair aft neat silver.svg


path: resources/vector/body/addon/hair aft neat strawberry-blonde.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair aft neat strawberry.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1920"
	 inkscape:window-height="1001"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-1656.7797"
	 inkscape:cy="500"
	 inkscape:window-x="-9"
	 inkscape:window-y="-9"
	 inkscape:window-maximized="1"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Aft_7_"
	 class="st0"
	 d="m 568.83729,121.38644 c 13.5,72.4 34.5,66.5 14,103.3 -43.3,47.7 -90.1,7 -98.1,-9.8 -41.1,-23.4 -0.3,-140.799999 48.3,-130.299999 33.3,7.2 33.3,19.499999 35.8,36.799999"
	 inkscape:connector-curvature="0"
	 style="fill:#e6c78c;fill-opacity:1" /></svg>

path: resources/vector/body/addon/hair aft neat strawberry.svg


path: resources/vector/body/addon/hair aft neat white.svg


path: resources/vector/body/addon/hair fore neat auburn.svg


path: resources/vector/body/addon/hair fore neat black.svg


path: resources/vector/body/addon/hair fore neat blonde.svg


path: resources/vector/body/addon/hair fore neat blue.svg


path: resources/vector/body/addon/hair fore neat brown.svg


path: resources/vector/body/addon/hair fore neat burgundy.svg


path: resources/vector/body/addon/hair fore neat chestnut.svg


path: resources/vector/body/addon/hair fore neat chocolate.svg


path: resources/vector/body/addon/hair fore neat copper.svg


path: resources/vector/body/addon/hair fore neat dark brown.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair fore neat darkbrown.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1166"
	 inkscape:window-height="991"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-16.949153"
	 inkscape:cy="500"
	 inkscape:window-x="-8"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Black"
	 class="st0"
	 d="M484.6,101.6c5.1,7.6,15,14,52.5,7.3c18.7,5.2,2.6,37.9,1.1,61c-1.3,19.3-7.8,36.4-18.9,51.5  c17.1-12.2,36.2-10.4,50.8-41.2c41.1-23.4-1.2-125.9-49.9-115.4C487.2,71.9,487.1,84.2,484.6,101.6z"
	 style="fill:#463325;fill-opacity:1" /></svg>

path: resources/vector/body/addon/hair fore neat darkbrown.svg


path: resources/vector/body/addon/hair fore neat dirty blonde.svg


path: resources/vector/body/addon/hair fore neat ginger.svg


path: resources/vector/body/addon/hair fore neat golden.svg


path: resources/vector/body/addon/hair fore neat green.svg


path: resources/vector/body/addon/hair fore neat grey.svg


path: resources/vector/body/addon/hair fore neat hazel.svg


path: resources/vector/body/addon/hair fore neat pink.svg


path: resources/vector/body/addon/hair fore neat platinum blonde.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair fore neat platinum.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1166"
	 inkscape:window-height="991"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-777.54237"
	 inkscape:cy="500"
	 inkscape:window-x="-8"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Black"
	 class="st0"
	 d="M484.6,101.6c5.1,7.6,15,14,52.5,7.3c18.7,5.2,2.6,37.9,1.1,61c-1.3,19.3-7.8,36.4-18.9,51.5  c17.1-12.2,36.2-10.4,50.8-41.2c41.1-23.4-1.2-125.9-49.9-115.4C487.2,71.9,487.1,84.2,484.6,101.6z"
	 style="fill:#fcf3c1;fill-opacity:1" /></svg>

path: resources/vector/body/addon/hair fore neat platinum.svg


path: resources/vector/body/addon/hair fore neat purple.svg


path: resources/vector/body/addon/hair fore neat red.svg


path: resources/vector/body/addon/hair fore neat silver.svg


path: resources/vector/body/addon/hair fore neat strawberry-blonde.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair fore neat strawberry.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1166"
	 inkscape:window-height="991"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-777.54237"
	 inkscape:cy="500"
	 inkscape:window-x="-8"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Black"
	 class="st0"
	 d="M484.6,101.6c5.1,7.6,15,14,52.5,7.3c18.7,5.2,2.6,37.9,1.1,61c-1.3,19.3-7.8,36.4-18.9,51.5  c17.1-12.2,36.2-10.4,50.8-41.2c41.1-23.4-1.2-125.9-49.9-115.4C487.2,71.9,487.1,84.2,484.6,101.6z"
	 style="fill:#e6c78c;fill-opacity:1" /></svg>

path: resources/vector/body/addon/hair fore neat strawberry.svg


path: resources/vector/body/addon/hair fore neat white.svg


code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
	.st0{fill:#787878;}
</style>
<g id="Navel_Piercing">
	<circle id="XMLID_279_" class="st0" cx="482.3" cy="364.4" r="1.2"/>
	<circle id="XMLID_278_" class="st0" cx="482.3" cy="368.6" r="1.2"/>
</g>
</svg>


path: resources/vector/body/addon/preg navel piercing heavy.svg


code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
	.st0{fill:#787878;}
</style>
<g id="Belly_Piercing">
	<circle id="XMLID_249_" class="st0" cx="466.9" cy="390.9" r="1.2"/>
	<circle id="XMLID_248_" class="st0" cx="466.9" cy="395" r="1.2"/>
</g>
</svg>


path: resources/vector/body/white/ball 0.svg


path: resources/vector/body/white/ball 1.svg


path: resources/vector/body/white/ball 2.svg


path: resources/vector/body/white/ball 3.svg


path: resources/vector/body/white/ball 4.svg


path: resources/vector/body/white/boob 0.svg


path: resources/vector/body/white/boob 1.svg


path: resources/vector/body/white/boob 2.svg


path: resources/vector/body/white/boob 3.svg


path: resources/vector/body/white/boob 4.svg


path: resources/vector/body/white/boob 5.svg


path: resources/vector/body/white/boob 6.svg


path: resources/vector/body/white/boob 7.svg


path: resources/vector/body/white/butt 0.svg


path: resources/vector/body/white/butt 1.svg


path: resources/vector/body/white/butt 2.svg


path: resources/vector/body/white/butt 3.svg


path: resources/vector/body/white/penis 0.svg


path: resources/vector/body/white/penis 1.svg


path: resources/vector/body/white/penis 2.svg


path: resources/vector/body/white/penis 3.svg


path: resources/vector/body/white/penis 4.svg


path: resources/vector/body/white/penis 5.svg


path: resources/vector/body/white/penis 6.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="preg belly 100.svg"
   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"><metadata
	 id="metadata45"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
	 id="defs43" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1680"
	 inkscape:window-height="987"
	 id="namedview41"
	 showgrid="false"
	 inkscape:snap-global="false"
	 inkscape:measure-start="0,0"
	 inkscape:measure-end="0,0"
	 inkscape:zoom="2"
	 inkscape:cx="545.96361"
	 inkscape:cy="600.87271"
	 inkscape:window-x="-8"
	 inkscape:window-y="-8"
	 inkscape:window-maximized="1"
	 inkscape:current-layer="Leg_Normal" /><style
	 type="text/css"
	 id="style34">
	.st0{fill:#010101;}
	.st1{fill:#F6E0E8;}
	.st2{fill:#D76B93;}
</style><style
	 id="style59"
	 type="text/css">
	.st0{fill:#010101;}
	.st1{fill:#F6E0E8;}
</style><g
	 inkscape:groupmode="layer"
	 id="layer1"
	 inkscape:label="Layer 1"><g
	   id="Leg_Normal"
	   transform="translate(-39.648802,-11.229384)" /></g><g
	 inkscape:groupmode="layer"
	 id="layer3"
	 inkscape:label="Layer 3" /><g
	 inkscape:groupmode="layer"
	 id="layer2"
	 inkscape:label="Layer 2"><g
	   id="Preg_Belly-1"
	   transform="matrix(0.42363273,-0.00346638,0.00338687,0.40450922,249.32988,238.00252)"><path
		 d="m 494.22473,431.94097 c 8.65119,-1.26911 13.12608,0.72344 21.53315,-2.31516 21.34938,-7.7407 29.9012,-15.4146 37.48963,-37.90478 3.62677,-23.97982 -5.77124,-58.05381 -19.25926,-75.96694 -8.11213,-10.77353 -10.00026,-13.1249 -33.11002,-11.17175 -17.36777,2.41057 -33.49073,5.07262 -43.71025,19.13363 -6.88573,11.58807 -12.34848,37.1166 -11.5462,64.5651 3.51979,31.82667 16.11551,40.14327 48.60295,43.6599 z"
		 class="st0"
		 id="XMLID_252_-4"
		 inkscape:connector-curvature="0"
		 style="fill:#010101"
		 sodipodi:nodetypes="cccccccc" /><path
		 d="m 494.81397,432.05206 c 8.81587,-0.65772 10.61717,0.0233 10.61717,0.0233 3.10959,-1.20696 3.98998,-0.81048 10.52995,-3.3228 5.42497,-2.12433 5.53012,-1.71289 8.79788,-3.24116 11.13609,-5.20814 24.94568,-12.22864 22.47898,-10.88102 14.46331,-13.05465 3.66243,-18.56552 10.22666,-23.26448 4.2539,-9.61017 10.82714,-38.48748 -13.02389,-56.08498 -6.15516,-13.76321 -7.45105,-16.85922 -12.5039,-24.39355 -1.50944,-1.52039 -3.0003,-4.05234 -6.27655,-6.82304 -2.15645,-2.24905 -5.3111,-2.95866 -6.82838,-3.74157 -6.37506,-3.28948 -13.90562,0.49207 -17.60425,-0.21084 -17.46891,3.26711 -18.27045,4.82805 -26.69869,7.24636 -8.15903,4.98466 -7.24946,-7.07732 -12.96677,8.62769 -5.61144,2.95192 -12.87468,34.92008 -15.32685,41.40491 -16.6494,22.05412 -12.19763,47.36429 3.1016,55.675 6.64051,6.09663 15.52422,11.46163 23.55923,14.33337 1.89991,1.29062 16.95902,2.88103 21.91781,4.65281 z"
		 class="st1"
		 id="XMLID_251_-6"
		 inkscape:connector-curvature="0"
		 style="fill:#f6e0e8"
		 sodipodi:nodetypes="ccccccccccccccccc" /><ellipse
		 ry="6.5197196"
		 rx="6.0236797"
		 cy="472.41959"
		 cx="396.2402"
		 class="st2"
		 transform="matrix(0.98089865,-0.19451952,0.20438027,0.97889157,0,0)"
		 id="XMLID_250_-9"
		 style="fill:#d76b93;stroke-width:2.12708855" /></g></g><style
	 id="style93"
	 type="text/css">
	.st0{fill:#F6E0E8;}
	.st1{fill:#010101;}
</style><style
	 id="style3832"
	 type="text/css">
	.st0{fill:#010101;}
	.st1{fill:#F6E0E8;}
	.st2{fill:#D76B93;}
</style><style
	 id="style3871"
	 type="text/css">
	.st0{fill:#010101;}
	.st1{fill:#F6E0E8;}
	.st2{fill:#D76B93;}
</style><style
	 id="style3910"
	 type="text/css">
	.st0{fill:#010101;}
	.st1{fill:#F6E0E8;}
	.st2{fill:#D76B93;}
</style><style
	 id="style4807"
	 type="text/css">
	.st0{fill:#010101;}
	.st1{fill:#F6E0E8;}
	.st2{fill:#D76B93;}
</style></svg>


code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
	.st0{fill:#D76B93;}
</style>
<path id="Vagina" class="st0" d="M486.1,434.3c3,13.5,5.9,25.9,9.3,33C487.8,458.8,484.6,448.4,486.1,434.3z"/>
</svg>


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="afro front.svg"
   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="2066"
	 inkscape:window-height="1295"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="3.776"
	 inkscape:cx="513.77669"
	 inkscape:cy="866.69981"
	 inkscape:window-x="0"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#5FBA46;}
</style><path
	 id="Hair_Green"
	 class="st0"
	 d="M 471.88814,103.71864 C 477.5178,117.14492 499.6,115.6 537.1,108.9 c 4.13432,2.02203 12.39873,7.17966 12.22288,25.24788 -24.86991,51.07966 52.58136,19.71569 57.10635,-48.076267 C 606.05042,61.689405 604.75847,36.211864 578.04492,18.123727 560.35254,0.54999657 506.4,3.7173729 487.09619,14.747034 457.80381,27.408475 453.20169,83.935169 471.88814,103.71864 Z"
	 style="fill:#b4b3b7;fill-opacity:1"
	 inkscape:connector-curvature="0"
	 sodipodi:nodetypes="ccccccc" /></svg>

path: resources/vector/hair/afro front.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="bun front.svg"
   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1935"
	 inkscape:window-height="1331"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="3.776"
	 inkscape:cx="502.72508"
	 inkscape:cy="834.78887"
	 inkscape:window-x="0"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#5FBA46;}
</style><path
	 id="Hair_Green"
	 class="st0"
	 d="m 484.6,101.6 c 5.1,7.6 15,14 52.5,7.3 8.37161,3.34619 11.60424,19.36186 10.10424,29.75 -11.09873,28.03941 11.53263,14.41906 14.73347,-4.64407 0.94534,-4.78474 10.77627,-41.649999 -13.02415,-57.61949 C 600.87161,65.963135 504.81101,18.01822 520.2,64.8 c -33,7.1 -33.1,19.4 -35.6,36.8 z"
	 style="fill:#b4b3b7;fill-opacity:1"
	 inkscape:connector-curvature="0"
	 sodipodi:nodetypes="ccccccc" /></svg>

path: resources/vector/hair/bun front.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="trimmed front.svg"
   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1691"
	 inkscape:window-height="1100"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="3.776"
	 inkscape:cx="493.13268"
	 inkscape:cy="878.68152"
	 inkscape:window-x="51"
	 inkscape:window-y="112"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="NewBody" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#5FBA46;}
</style><g
	 transform="matrix(0.11855749,0,0,0.11836127,42.924405,-240.28811)"
	 style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
	 id="NewBody"><metadata
	   id="CorelCorpID_0Corel-Layer" /><g
	   id="HairShort"
	   style="fill:#b4b3b7;fill-opacity:1"
	   transform="matrix(0.96090345,0,0,0.81429225,136.51865,590.3991)"><path
		 inkscape:connector-curvature="0"
		 id="Hair"
		 class="fil0"
		 d="m 3731,2912.312 c 0,0 23,27.2601 105,27.2601 82,0 254,-16.7078 317,22.8633 44.974,42.9769 56.3376,16.5435 88.3376,6.2128 37.974,24.5105 60.2595,50.9564 66.6362,93.1656 10.026,75.1822 -4.4674,121.6816 12.5326,149.8211 17,28.1394 24.1169,-144.4482 24.4936,-209.6284 7,-92.4366 12.974,-142.125 -9.026,-236.2162 -41.2553,-191.0494 -221.327,-266.7287 -405.974,-188.5134 -97.974,34.6239 -159.052,114.6455 -194,188.1824 -10.3767,81.7803 -18,136.3004 -5,146.8527 z"
		 style="fill:#b4b3b7;fill-opacity:1;stroke-width:0.93774056"
		 sodipodi:nodetypes="csccccccccc" /></g></g></svg>

path: resources/vector/hair/buzzcut front.svg


path: resources/vector/hair/eary back.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="luxurious back.svg"
   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1916"
	 inkscape:window-height="1237"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="2.6700352"
	 inkscape:cx="499.80468"
	 inkscape:cy="857.13839"
	 inkscape:window-x="71"
	 inkscape:window-y="53"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#8D4F21;}
</style><path
	 id="Hair_Aft_1_"
	 class="st0"
	 d="M564.6,100.2c13.5,72.4,34.5,66.5,14,103.3c-43.3,47.7-90.1,7-98.1-9.8  c-41.1-23.4-0.3-140.8,48.3-130.3C562.1,70.6,562.1,82.9,564.6,100.2"
	 style="fill:#474e62;fill-opacity:1" /></svg>