
/// <reference path="../_system/wardrobe/wardrobe.ts" />


declare let rand: Window["rand"];

const timed_stat_changes ={ //Hourly
	energy: {
		sleep: 2,
		default: 6
	},
	hydra: {
		sleep: 4,
		default: 12
	},
	sleep:{
		sleep: -8,
		default: 4
	}
}

const dieRisks = {
	'hunger':{
		variable: 'pcs_energy',
		durations: [720,720,14400]
	},
	'thirst':{
		variable: 'pcs_hydra',
		durations: [720,720,1440]
	}
}


const fameAlwaysLocale = ['sex','prostitute'];

class PlayerCharacter implements Character{

	get INSTANCE():PlayerCharacter{return State.variables.pc;}

	gameover:string;
	_death = {};

	//#region Name
		name_first  = 'Svetlana';
		name_last = 'Lebedev';
		name_nick = 'Sveta';
		get name_full(){return `${this.name_first} ${this.name_last}`;}
	//#endregion

	//#region Description
		get desc():BodyDescription{
			return new setup.BodyDescription(this);
		}
	//#endregion

	//#region Images
		/**
		 * A custom path to an image-file for the portrait of the character.
		 * @type {string}
		 */
		avatar:string = '';
		
		/**
		 * Path of an image-file that is used as the portrait of the character.
		 * Uses `avatar` if set, otherwise a combination of hair length and color as specified by `:: $face_image`.
		 * @readonly
		 * @type {string}
		 */
		get image():string{
			return this.avatar || this.bodyImage('hair');
		}

		bodyImage(part:string|undefined=undefined):string{
			const wardrobe = State.variables.wardrobe;
			switch(part){
				case undefined:
				case 'body':
					/*
						<<if ($pc.knowpreg == 1 or ($pc.preg == 1 and $pc.thinkpreg == 1) or ($pc.preg == 1 and $pc.PregChem > 3600)) and $pc.bodset == 3>>
							<<if $pc.PregChem > 6216>>
								<<set $result = 'pc/body/shape/'+$bodimgsets[(($pc.bodset * 10) + 9)]+'/8.jpg'>>
							<<elseif $pc.PregChem < 2688>>
								<<set $result = 'pc/body/shape/'+$bodimgsets[(($pc.bodset * 10) + 9)]+'/0.jpg'>>
							<<else>>
								<<set $result = 'pc/body/shape/'+$bodimgsets[(($pc.bodset * 10) + 9)]+'/(($pc.PregChem - 2184) / 504).jpg'>>
							<</if>>
						<<elseif $pc.salocatnow >= 1 and $pc.salocatnow <= 5>>
							<<set $result = 'pc/body/shape/'+$bodimgsets[(($pc.bodset * 10) + 9)]+'/'+$pc.salocatnow+'.jpg'>>
						<<elseif $pc.salocatnow <= 0>>
							<<set $result = 'pc/body/shape/0.jpg'>>
						<<elseif $pc.salocatnow == 6>>
							<<if getvar("$imgset6ovr["+$pc.bodset+"]") == 1>>
								<<set $result = 'pc/body/shape/'+$bodimgsets[(($pc.bodset * 10) + 9)]+'/6.jpg'>>
							<<else>>
								<<set $result = 'pc/body/shape/6.jpg'>>
							<</if>>
						<<else>>
							<<if getvar("$imgset7ovr["+$pc.bodset+"]") == 1>>
								<<set $result = 'pc/body/shape/'+$bodimgsets[(($pc.bodset * 10) + 9)]+'/7.jpg'>>
							<<else>>
								<<set $result = 'pc/body/shape/7.jpg'>>
							<</if>>
						<</if>>
					*/
					let imageIndex = Math.clamp(this.bmiCategory+2,1,5);
					return `pc/body/shape/default_low/${imageIndex}.jpg`;
				case 'bra':
					if(wardrobe.bra.isValidItem)
						return wardrobe.bra.image;
					return this.bodyImage('breasts');
				case 'clothes':
					/*
						<<if $wardrobe.clothingworntype == 'nude' and getvar("$towel") == 1 and !$wardrobe.isWearingPanties>>
							<<set $result = 'pc/body/towel.jpg'>>
							
						<<elseif $wardrobe.clothingworntype == 'nude' and getvar("$robe") == 1>>
							<<set $result = 'pc/body/robe.jpg'>>
							
						<<elseif $wardrobe.clothingworntype == 'nude' and $wardrobe.isWearingPanties>>
							<<set $result = 'pc/body/nude.jpg'>>
							
						<<elseif $wardrobe.clothingworntype == 'nude' and !$wardrobe.isWearingPanties>>
							<<set $result = 'pc/body/nude1.jpg'>>
							
						<<elseif $wardrobe.clothingworntype == 'misc_outfits' and $wardrobe.clothingwornnumber == 1>>
							<<set $result = setup.func('$clothing_image', $wardrobe.clothingworntype, $wardrobe.clothingwornnumber)>>
							
						<<else>>
							<<set $result = $wardrobe.clothes.image>>
							<!--
							<<set $result = setup.func('$clothing_image', $wardrobe.clothingworntype, $wardrobe.clothingwornnumber)>>
							
							<<if getvar("$PClobimbo") == 1>>
								

							<</if>> -->
							<!--<<if $wardrobe.clothingworntype != 'coat' and !$wardrobe.isWearingSwimwear>>
								<<gs 'clothing_attributes' $wardrobe.clothingworntype $wardrobe.clothingwornnumber>>
								<<gs 'clothing_descriptions'>>
								
							<<else>>
								<<if $wardrobe.clothingworntype == 'danilovich_swimsuit'>>
									setup.func('$attributes_danilovich_swim_one', $wardrobe.clothingworntype, clothingwornnumber)
									
								<<elseif $wardrobe.clothingworntype == 'scandalicious_swimsuit'>>
									setup.func('$attributes_scandalicious_swim_one', $wardrobe.clothingworntype, clothingwornnumber)
									
								<<elseif $wardrobe.clothingworntype == 'scandalicious_bikinis'>>
									setup.func('$attributes_scandalicious_swim_two', $wardrobe.clothingworntype, clothingwornnumber)
									
								<<elseif $wardrobe.clothingworntype == 'allure_swimsuit'>>
									setup.func('$attributes_allure_swim_one', $wardrobe.clothingworntype, clothingwornnumber)
									
								<<elseif $wardrobe.clothingworntype == 'allure_bikinis'>>
									setup.func('$attributes_allure_swim_two', $wardrobe.clothingworntype, clothingwornnumber)
									
								<<elseif $wardrobe.clothingworntype == 'nerdvana_swimsuit'>>
									setup.func('$attributes_nerdvana_swim_one', $wardrobe.clothingworntype, clothingwornnumber)
									
								<<elseif $wardrobe.clothingworntype == 'nerdvana_bikinis'>>
									setup.func('$attributes_nerdvana_swim_two', $wardrobe.clothingworntype, clothingwornnumber)
									
								<</if>>
							<</if>>
							<<if $wardrobe.clothingworntype == 'gm_maid' or $wardrobe.PCloStyle2 == 1>>
								
							<<elseif $wardrobe.clothingworntype == 'gm_server' or $wardrobe.PCloStyle2 == 2>>
								
							<<elseif $wardrobe.clothingworntype == 'eroto_strip' or $wardrobe.PCloStyle2 == 3>>
								
							<</if>>-->
						<</if>>
					*/
					if(wardrobe.clothes.isValidItem)
						return wardrobe.clothes.image;
					if(wardrobe.panties.isValidItem)
						return 'pc/body/nude.jpg';
					return 'pc/body/nude1.jpg';
				case 'hair':
					return setup.func("$face_image");
				case 'coat':
					if(wardrobe.coat.isValidItem)
						return wardrobe.coat.image;
					return '';
				case 'panties':
					if(wardrobe.panties.isValidItem)
						return wardrobe.panties.image;
					return setup.func('$pube_image');
				case 'shoes':
					if(wardrobe.shoes.isValidItem)
						return wardrobe.shoes.image;
					return 'pc/body/feet.jpg';
				case 'tits':
				case 'breasts':
					/*
					<<if $pc.nipples >= 40 and $pc.nipples < 60 and $pc.tits == 2>>
						<<set $result = 'pc/body/tits/t'+$pc.tits+'_p.jpg'>>
					<<else>>
						<<set $result = 'pc/body/tits/t'+$pc.tits+'.jpg'>>
					<</if>>
					*/
					return 'pc/body/tits/t'+this.tits+'.jpg';
			}
			return '';
		}

			/*<<case 'bodysuit'>>
				<<if !$wardrobe.isWearingBra>>
					<<set $result = 'pc/body/tits/ttits.jpg'>>
					
				<<else>>
					<<set $result = setup.func('$pcs_outfit_image', $pc.bodysuitworntype+'_bodysuits', bodysuitwornnumber)>>
					
				<</if>>*/
				/*
			<<case 'teeth'>>
				<<if getvar("$pcs_brace") == 1>>
					<<set $result ='pc/body/teeth/brace.jpg'>>
				<<elseif $pc.pcs_teeth == -1>>
					<<set $result ='pc/body/teeth/goodteeth.jpg'>>
				<<elseif $pc.pcs_teeth == 1>>
					<<set $result ='pc/body/teeth/badteeth1.jpg'>>
				<<elseif $pc.pcs_teeth == 2>>
					<<set $result ='pc/body/teeth/badteeth2.jpg'>>
				<<else>>
					<<set $result ='pc/body/teeth/averageteeth.jpg'>>
				<</if>>*/


	//#endregion

	dailyUpdate(){
		this.bodyDailyUpdate();
	}

	get outfit(){
		return State.variables.wardrobe.wornItems;
	}

	/**
	 * Is the character currently wearing shoes? They could be taken manually or autoatically, because the character is inside.
	 * @type {boolean}
	 */
	get isWearingShoes():boolean{
		return State.variables.wardrobe.isWearingShoes;
	}

	//#region Birthday & Age
		birthday	= 1;
		birthmonth  = 4;
		birthyear   = 1999;

		get birthdayDate(){
			return new Date(Date.UTC(this.birthyear,this.birthmonth-1,this.birthday));
		}

		get age(){
			let age = State.variables.time.ageOfDate(this.birthday,this.birthmonth,this.birthyear);
			if(age < 16)
			{
				console.error("Critical Error: Playercharacter too young");
				Engine.restart();
			}
			return age;
		}
		set age(v){
			v = Math.max(v,16);
			let currentAge = this.age;
			let difference = v - currentAge;
			this.birthyear -= difference;
			console.log("AGE set to "+v);
		}
	//#endregion

	//#region Visual Age
		_visualAgeDaysOffset = 0;
		get visualAgeDays(){return this._visualAgeDaysOffset}
		set visualAgeDays(v){this._visualAgeDaysOffset = v;}
		get visualAgeDaysInverse(){
			return this._visualAgeDaysOffset * -1;
		}
		set visualAgeDaysInverse(v){
			this._visualAgeDaysOffset = v * -1;
		}
		get visualAge(){
			return State.variables.time.ageOfDate(this.visualBirthday);
		}
		set visualAge(v){
			let currentVisualAge = this.visualAge;
			let offset = v - currentVisualAge;
			this._visualAgeDaysOffset += Math.floor(offset * 365.25);
		}
		get visualBirthday(){
			let visualBirthday = this.birthdayDate;
			visualBirthday.setUTCDate(visualBirthday.getUTCDate() - this._visualAgeDaysOffset);
			return visualBirthday;
		}
	//#endregion

	//#region Mood
		_mood = new setup.Mood();

		get mood(){return this._mood.mood;}
		set mood(v){this._mood.mood = v}
		//get moodlets(){return this._mood.moodlets}
		get moodletsActive(){return this._mood.moodletsActive}
		get moodletsActiveByGroup(){return this._mood.moodletsActiveByGroup}
		get moodletsActiveEffect(){return this._mood.moodletsActiveEffect}
		get moodletsActiveByGroupAccumulationApplied(){return this._mood.moodletsActiveByGroupAccumulationApplied}
		moodletApplyById(moodletId,minutes=0){return this._mood.moodletApplyById(moodletId,minutes)}
		//moodletCombinedData(moodletId){return this._mood.moodletCombinedData(moodletId)}
		moodletDeactivateById(moodletId){return this._mood.moodletDeactivateById(moodletId)}
		moodletIncTime(moodletId,minutes,skipIncludedMoodlets=false){return this._mood.moodletIncTime(moodletId,minutes,skipIncludedMoodlets)}
		//moodletIsActive(moodletId){return this._mood.moodletIsActive(moodletId)}
		moodletUpdate(moodletId,updateObject){return this._mood.moodletUpdate(moodletId,updateObject)}
		#moodletsClean(){return this._mood._moodletsClean();}
		
		get moodletsSpecial():{[key: string]: ActiveMoodlet}{
			//return Object.assign({},this.moodletPain,this.activeEffectsMoodlets);
			return {pain:this.moodletPain};
		}

		get moodletPain():ActiveMoodlet{
			return PainMoodlet.createPainMoodlet(this.painTotal);
		}

	//#endregion

	//#region Personality
		//#region Deprecated
			// ----- Personality -----
			_pcs_dom = 0
			
			/**
			 * Dominance
			 * @deprecated
			 * @type {number}
			 */
			get pcs_dom(){return this._pcs_dom;}
			set pcs_dom(v){
				if(v < 0){
					this.pcs_sub -= v;
					this._pcs_dom = 0;
				}else{
					this._pcs_dom = Math.min(100,v);
					this._balanceDomSub();
				}
			}

			_pcs_sub = 0

			
			/**
			 * Submissiveness
			 * @deprecated
			 * @type {number}
			 */
			get pcs_sub(){return this._pcs_sub;}
			set pcs_sub(v){
				if(v < 0){
					this.pcs_dom -= v;
					this._pcs_sub = 0;
				}else{
					this._pcs_sub = Math.min(100,v);
					this._balanceDomSub();
				}
			}

			_balanceDomSub(){
				if(this._pcs_dom > 0 && this._pcs_sub > this._pcs_dom){
					this._pcs_sub -= this._pcs_dom;
					this._pcs_dom = 0;
				}

				if(this._pcs_sub > 0 && this._pcs_dom > this._pcs_sub){
					this._pcs_dom -= this._pcs_sub;
					this._pcs_sub = 0;
				}
			}
		//#endregion

		_personalityValues:{
			[key:string]:{				
				/**
				 * Index 0: DaystartS, Index 1: Current at that day
				 * @type {number[][]}
				 */
				history:number[][]
			}
		} = {};

		personalityScale(scaleId:string):PersonalityScale{
			if(scaleId in this._personalityValues)
				return PersonalityScale.get(scaleId,this);
			return this.personalityScaleInitialize(scaleId);
		}

		get personalityScales():{[key:string]:PersonalityScale}{
			let result:{[key:string]:PersonalityScale} = {};
			for(const scaleId of Object.keys(this._personalityValues))
				result[scaleId] = this.personalityScale(scaleId);
			return result;
		}

		personalityScaleInitialize(scaleId:string,v:number=undefined){
			let start:number;
			if(v === undefined){
				const scaleData = PersonalityScale.get(scaleId,this);
				start = scaleData.start;
			}else
				start = v;

			const daystart = State.variables.time.daystart;

			this._personalityValues[scaleId] = {history:[[daystart,start],[daystart-1,start]]};

			return PersonalityScale.get(scaleId,this);
		}


	//#endregion

	//#region Mental Capacity
		get consciousness(){
			return this.#activeEffectValueByKey('consciousness','*');
		}

		_mentalHealth = 1000;
		get mentalHealth(){return this._mentalHealth;}
		set mentalHealth(v){
			if(v <= 0)
				this.gameover = 'mentalHealth';
			this._mentalHealth = Math.clamp(v,0,1000);
		}

		get mentalHealthRegenerationRate(){
			// Regenerate 10 willpower in 1 day
			const regenRate =10 / (24 * 60); 
			return regenRate;
		}

		#mentalHealthRegenerate(minutes:number){
			this.mentalHealth += minutes * this.mentalHealthRegenerationRate;
		}

		_pcs_willpwr = 70;
		
		get willpower(){return this._pcs_willpwr;}
		set willpower(v){
			if(v < 0){
				this._pcs_willpwr = 0;
				this.mentalHealth += v;
			}else{
				this._pcs_willpwr = Math.min(v,this.willpowermax);
			}
		}

		get willpowermax(){return 100}

		
		/**
		 * Willpower regeneration per minute
		 * @readonly
		 * @type {void}
		 */
		get willpowerRegenerationRate(){
			// Regenerate 100 willpower in 2 days
			const regenRate =100 / (2 * 24 * 60); 
			return regenRate;
		}

		#willpowerRegenerate(minutes:number){
			this.willpower += minutes * this.willpowerRegenerationRate;
		}

		/*get pcs_willpwr(){return this._pcs_willpwr;}
		set pcs_willpwr(v){
			this._pcs_willpwr = Math.clamp(v,0,this.willpowermax);
			console.log("Willpower set to "+this._pcs_willpwr);
		}
		will_counter = 0;

		_willpowermax = 70;
		get willpowermax(){return this._willpowermax}
		set willpowermax(v){this._willpowermax = Math.max(50,v);}
		pcs_willpower_feeder = 0;*/

	//#endregion

	//#region Main Stats (Hunger, Thirst & Sleep)

		consume(consumable:string|Consumable,percentage=100){
			if(typeof consumable == 'string')
				consumable = Consumable.get(consumable);
			consumable.consume(this,percentage);
		}

		//#region Hunger and Eating
			_energyBalance = 0; // A value between -6 and 6 that determines how fast you gain or lose bmi.
			_energy = 0;		// The energy you consumed today.
			_energyDemand = 0;	// Todays energy demand. Get updated every 15 minutes along witht the update of the hunger bar. Can also be increased by doing sports.

			_pcs_energy = 100;
			/**
			 * This is effecitvely the hunger-bar.
			 * @date 7/23/2023 - 5:56:29 PM
			 *
			 * @type {number}
			 */
			get pcs_energy(){return this._pcs_energy;}
			set pcs_energy(v){
				this._pcs_energy = Math.clamp(v,0,100);
				
				if(v > 0 && this._death['hunger']?.stage){
					this._death['hunger'] = {stage:0};
					for(let i=1;i<=dieRisks.hunger.durations.length;i++)
						this.moodletDeactivateById('hunger_'+i);
				}

			}
			_dieHungerStage = 0;
			_dieHungerNextStageDate = undefined;

			
			/**
			 * Intake in calories. Influences BMI in the long run.
			 * @type {number}
			 */
			get energy(){return this._energy;}
			set energy(v){this._energy = v;}
			get energyBalance(){return this._energyBalance;}
			set energyBalance(v){this._energyBalance = v;}
			get energyDemand(){return this._energyDemand;}
			set energyDemand(v){this._energyDemand = v;}

			
			/**
			 * Energy Demand adjusted for the current BMI.
			 * @date 7/27/2023 - 11:47:05 AM
			 *
			 * @readonly
			 * @type {number}
			 */
			get energyRequirement(){
				let baseRequirement = this.energyDemand; //timed_stat_changes.energy.sleep * 8 + timed_stat_changes.energy.default * 16;
				let requirementFactor = baseRequirement / 100;

				let bmi = this.bmi;
				// The following calculations assume that the base requirement is 100, therefore we need to multiplicate them with requirementFactor
				if(bmi < 19)
					return requirementFactor * (100 - Math.sqrt(19-bmi) * 10);
				if (bmi > 25)
					return requirementFactor * (100 + 5 * (bmi - 25));
				return baseRequirement;
			}

			_energyHistory = [];
			get energyHistory(){return this._energyHistory;}
			_energyHistoryLengthTarget = 7;
			get energyHistoryLengthTarget(){return this._energyHistoryLengthTarget;}
			set energyHistoryLengthTarget(v){
				this._energyHistoryLengthTarget = v;
				while(this._energyHistory.length > v)
					this._energyHistory.shift();
			}

			dailyEnergyUpdate(){
				let energyRequirement = this.energyRequirement;
				let energy = this.energy; 
				let energyQuota = energy / energyRequirement;
				let balanceTarget = 0;
				if(energyQuota < 0.5)
					balanceTarget = -6;
				else if(energyQuota < 0.8)
					balanceTarget = -3;
				else if(energyQuota > 1.5)
					balanceTarget = 6;
				else if(energyQuota > 1.2)
					balanceTarget = 3;
				
				let balanceCurrent = this.energyBalance;
				let balanceDifference = balanceTarget - balanceCurrent;

				// Gaining weight is way easier than losing it.
				if(balanceDifference < 0)
					balanceDifference = balanceDifference / 3;
				else
					balanceDifference = balanceDifference * 2 / 3;

				this.energyBalance += balanceDifference;

				// Change the BMI
				let bmi_change = Math.pow(this.energyBalance,2) * 0.005 * Math.sign(this.energyBalance);
				this.bmi += bmi_change;

				console.log("$pc.dailyEnergyUpdate(): energyRequirement,energy,balanceOld, balanceNew, bmiChange, bmiNew",energyRequirement,energy,balanceCurrent,this.energyBalance,bmi_change,this.bmi);

				this._energyHistory.push({
					energyRequirement: energyRequirement,
					energy: energy,
					balanceTarget: balanceTarget,
					balanceChange: balanceDifference,
					balanceNew: this.energyBalance,
					bmiChange: bmi_change,
					bmiNew: this.bmi
				});

				this.energy = 0;
				this.energyDemand = 0;
			}

			get pcs_weight(){				   
				return Math.pow(this.height / 100,2) * this.bmi;
			}

			_bmi = 20;

			get bmi(){
				return this._bmi;
			}
			set bmi(v){
				this._bmi = v;
			}

			
			/**
			 * Returns the bmi-category
			 * 
			 * @readonly
			 * @type {(-2|-1|0|1|2|3|4|5)} -2: severely underweight, -1: underweight, 0: normal, 1: overweight, 2-5: increasingly obese
			 */
			get bmiCategory():-2|-1|0|1|2|3|4|5{
				let bmi = this.bmi;
				if(bmi<16)
					return -2;
				if(bmi<18.5)
					return -1;
				if(bmi<25)
					return 0;
				if(bmi<30)
					return 1;
				if(bmi<35)
					return 2;
				if(bmi<40)
					return 3;
				if(bmi<45)
					return 4;
				return 5;
			}
		//#endregion

		//#region Drinking
			_pcs_hydra = 100;
			_dieThirstStage = 0;
			_dieThirstNextStageDate = undefined;
			get pcs_hydra(){return this._pcs_hydra;}
			set pcs_hydra(v){
				
				/*if(v < 0){
					v = 0;
					this.pcs_health -= 5;
					setup.msgPredefined("warn_hydra_low");
				}

				*/
				this._pcs_hydra = Math.clamp(v,0,100);

				if(v > 0 && this._death['thirst']?.stage){
					this._death['thirst'] = {stage:0};
					for(let i=1;i<=dieRisks.thirst.durations.length;i++)
						this.moodletDeactivateById('thirst_'+i);
				}
			}
		//#endregion

		//#region Sleep
			_pcs_sleep = 100;
			get pcs_sleep(){return this._pcs_sleep;}
			set pcs_sleep(v){

				/*if(v < 0){
					v = 0;
					this.mood -= 5;
					setup.msgPredefined("warn_sleep_low");
				}*/



				this._pcs_sleep = Math.clamp(v,0,100);
				console.log("Sleep set to "+this._pcs_sleep);
			}

			isSleeping = 0;

			
		//#endregion
		minutesTilStat(stat,target=0,isAsleep=undefined){
			const time = State.variables.time;
			isAsleep ??= (this.isSleeping == 1);
			
			let current = 0;
			let configIndex;

			switch(stat){
				case 'energy':
				case 'hunger':
					configIndex = 'energy';
					current = this.pcs_energy;
					break;
				case 'hydra':
				case 'thirst':
					configIndex = 'hydra';
					current = this.pcs_hydra;
					break;
			}

			const timeMod = 0.25 * this.timeFactor;
			let change = 0;
			if(isAsleep)
				change = timed_stat_changes[configIndex]['sleep']  * timeMod;
			else
				change = timed_stat_changes[configIndex]['default']  * timeMod;

			let requiredUpdates = Math.ceil((current - target) / (change || 1));
			
			const minutesToNext15MinutesInterval = 15 - time.now.getUTCMinutes() % 15;
			return Math.max(0,(requiredUpdates - 1)*15 + minutesToNext15MinutesInterval);
		}
	//#endregion
	
	//#region Body Odor
		deodorant_on = 0;
		deodorant_time = 0;

		_pcs_sweat = 0;
		get pcs_sweat(){return this._pcs_sweat;}
		set pcs_sweat(v){
			this._pcs_sweat = v;
			console.log("Sweat set to "+this._pcs_sweat);
		}
		
		sweatAdd(v:number){
			this.pcs_sweat += v;
		}
	//#endregion

	//#region Arousal
		get hornyMin(){
			const cycleArousalModificator = this.cycleArousalModificator;
			return cycleArousalModificator.target;
		}
		_pcs_horny = 0;
		get horny(){return Math.max(this._pcs_horny,this.hornyMin);}
		set horny(v){
			if(typeof v != "number" || isNaN(v)){
				console.error("Trying to set pcs_horny to non-number",v);
				return;
			}
			this._pcs_horny = Math.clamp(v,0,100);
			console.log("Horny set to "+this._pcs_horny);
		}
	
		/**
		 * How much horny is supposed to decrease each minute.
		 * @readonly
		 * @type {number}
		 */
		get hornyDeteriorationRate(){
			const cycleArousalModificator = this.cycleArousalModificator;
			return (1 / cycleArousalModificator.factor);
		}

	//#endregion

	//#region Inhibition
		
		/**
		 * @deprecated
		 * @type {number}
		 */
		get pcs_inhib(){
			return this.inhibition;
		}
		set pcs_inhib(v){
			this.inhibition = v;
		}

		get inhibition(){
			return this.personalityScale('exhibitionism').current;
		}
		set inhibition(v){
			this.personalityScale('exhibitionism').current = v;
		}
	//#endregion
	
	//#region Frost
		_frost = 0;
		get frost(){
			if(this.alko > 0)
				return 0;
			return this._frost;
		}
		set frost(v){
			this._frost = v;
		}
	//#endregion

	//#region Drugs
		_drugs = new setup.Drugs();

		get drugsActiveEffects():{[key: string]:Effect}{return this._drugs.activeEffects}
		get drugsActiveEffectIds(){return this._drugs.activeEffectIds}
		drugsDeteriorate(minutes){return this._drugs.deteriorate(minutes)}
		drugVolInc(drugId:string, inc:number){return this._drugs.volInc(drugId, inc)}
		drugVolSet(drugId,v){return this._drugs.volSet(drugId, v)}
		drugVol(drugId){return this._drugs.vol(drugId)}

		get alko(){return this.drugVol('alcohol')}
		set alko(v){this.drugVolSet('alcohol',v)}
	//#endregion
	
	//#region Appearance History
		/*_appearanceHistory:Array<{time:Date,varname:string,val:string|number}> = [];
		a*/

		_appearanceHistory:AppearanceChange[] = [];

		get appearanceHistory():AppearanceChange[]{
			return this._appearanceHistory;
		}

		appearanceHistoryFiltered(time:Date):{[key:string]:AppearanceChange}{
			const appearanceHisotryRaw = this.appearanceHistory.
				filter(
					(entry)=>entry.time.getTime() > time.getTime()
				).
				sort(
					(a,b)=>a.time.getTime()-b.time.getTime()
				);
			
			let result:{[key:string]:AppearanceChange} = {};
			for(const historyEntry of appearanceHisotryRaw){
				if(result[historyEntry.key]){
					result[historyEntry.key].to = historyEntry.to;
				}else{
					result[historyEntry.key] = historyEntry;
				}
			}
			return result;
		}

		appearanceHistoryPush(key:string,from:any,to:any){
			this._appearanceHistory.push({
				time: State.variables.time.now,
				key: key,
				from: from,
				to: to,
			});
		}
	//#endregion

	//#region Face

		//#region Eyes
			
			eyecolor:EEyeColor = EEyeColor.BLUE;
			eyesize:EEyeSize = EEyeSize.MEDIUM;
		//#endregion

		faceGeneticAttractiveness = 0;
		faceSurgeries = 0;
		get faceAttractiveness(){
			return Math.clamp(this.faceGeneticAttractiveness+this.faceSurgeries,-3,3);
		}
	//#endregion

	// ----- Body ------

	 

	_pcs_vag = 0
	get pcs_vag(){return this._pcs_vag;}
	set pcs_vag(v){this._pcs_vag = Math.min(36,v);}

	_pcs_ass = 0
	get pcs_ass(){return this._pcs_ass;}
	set pcs_ass(v){this._pcs_ass = Math.min(36,v);}

	_pcs_throat = 0
	get pcs_throat(){return this._pcs_throat;}
	set pcs_throat(v){this._pcs_throat = Math.min(36,v);}

	_pcs_health = 0;
	get pcs_health(){return Math.min(this._pcs_health, this.healthmax)}
	set pcs_health(v){
		/*if(v < 0)
			this.gameover = 1;*/
		this._pcs_health = Math.min(v, this.healthmax);
	}
	get healthmax(){
		let healthmax_calc = Math.max(1,(this.vitality * 10 + this.strength * 5));
		let mult_by_pain = 1;
		let pain_total = this.painTotal;
		if(pain_total > 80)
			mult_by_pain = 0.20;
		else if(pain_total > 60)
			mult_by_pain = 0.40;
		else if(pain_total > 40)
			mult_by_pain = 0.60;
		else if(pain_total > 20)
			mult_by_pain = 0.80;
		else if(pain_total > 0)
			mult_by_pain = 0.90;
		healthmax_calc = Math.ceil(healthmax_calc * mult_by_pain);
		return healthmax_calc;
	}

	_pcs_stam = 0;
	get pcs_stam(){return this._pcs_stam}
	set pcs_stam(v){this._pcs_stam = Math.min(v, this.stammax)}
	get stammax(){return Math.max(1,5 * (2 * this.vitality + this.agility + this.strength) / 2)}

	get speed(){return (2 * (this.strength + this.agility) + this.vitality) / 5}

	genbsize	= 12;		   // the set genetic bust size
	nbsize	  = 12;		   // starts at a set genetic bust size, but can be adjusted down if salo drops too low


	silicone = 0;
	silicone_butt = 0;
	butt_cheat = 0;

	magicf2b = 0;   //magicf2b = set in body_shape for the fat moved to bust

	/*get pcs_hips(){return (this.pcs_hgt * this.hratio) / 100 + this.vhips;}
	get pcs_waist(){return (this.pcs_hips * this.wratio) / 100 + this.vofat;}
	get pcs_band(){return (this.pcs_waist * this.bratio) / 100 + this.vofat;}
	get pcs_bust(){return (this.pcs_waist * this.bratio) / 100 + this.nbsize + this.magicf2b + this.silicone;}
	get pcs_butt(){return (this.pcs_hips / 10) + this.silicone_butt + this.butt_cheat;}*/
	//get pcs_cupsize(){return (this.pcs_bust - this.pcs_band);}

	_cupsize = 15;
	get pcs_cupsize(){return this._cupsize;}
	set pcs_cupsize(v){this._cupsize = v;}

	get tits(){
		if(this.pcs_cupsize <= 0)	  return -1;
		if(this.pcs_cupsize <= 5)	  return 0;
		if(this.pcs_cupsize <= 10)	 return 1;
		if(this.pcs_cupsize <= 15)	 return 2;
		if(this.pcs_cupsize <= 20)	 return 3;
		if(this.pcs_cupsize <= 25)	 return 4;
		if(this.pcs_cupsize <= 30)	 return 5;
		if(this.pcs_cupsize <= 35)	 return 6;
		if(this.pcs_cupsize <= 40)	 return 7;
		if(this.pcs_cupsize <= 45)	 return 8;
		if(this.pcs_cupsize <= 50)	 return 9;
		if(this.pcs_cupsize <= 55)	 return 10;
		return 11;
	}

	set tits(v){
		this.pcs_cupsize = Math.ceil((v+1)*5);
	}

	
	/**
	 * Cup size of the breasts.
	 * @date 1/21/2024 - 12:20:29 PM
	 *
	 * @readonly
	 * @type {("AA cup" | "A cup" | "B cup" | "C cup" | "D cup" | "E cup" | "F cup" | "G cup" | "H cup" | "I cup" | "J cup" | "K cup" | "??? cup")}
	 */
	get titsize(){
		switch(this.tits){
			case 0:	 return 'AA cup';
			case 1:	 return 'A cup';
			case 2:	 return 'B cup';
			case 3:	 return 'C cup';
			case 4:	 return 'D cup';
			case 5:	 return 'E cup';
			case 6:	 return 'F cup';
			case 7:	 return 'G cup';
			case 8:	 return 'H cup';
			case 9:	 return 'I cup';
			case 10:	return 'J cup';
			case 11:	return 'K cup';
			default:	return '??? cup';
		}
	}

	//#region Height
		pcs_hgt = 170;
		/**
		 * The characters height in cm.
		 * @type {number}
		 */
		get height(){
			return this.pcs_hgt;
		}
		set height(v){
			this.pcs_hgt = v;
		}
	//#endregion

	//#region Hair
		//#region Head
			pcs_hairbsh = 0;
			pcs_hairlng = 300;
			
			/**
			 * Hair Length in mm
			 * @type {number}
			 */
			get hairLength(){return this.pcs_hairlng;}
			set hairLength(v){this.pcs_hairlng = v;}

			get hairLengthCategory():EHairLength{
				const categories = [
					30,80,160,260,400,600,800
				];
				for(let index = 0; index < categories.length; index++){
					let maxLengthOfCategory = categories[index];
					if(this.hairLength <= maxLengthOfCategory)
						return index;
				}
				return categories.length;
			}

			hairColor:EHairColor = 0;
			hairColorNatural:EHairColor = 0;
			hairDyeFade = 0;

			hairDye(newColor:EHairColor,fadeDuration=undefined){
				this.appearanceHistoryPush('hairColor',this.hairColor,newColor);
				this.hairColor = newColor;
				this.hairDyeFade = fadeDuration ?? 30;
			}
		//#endregion

		//#region Legs
			_leghair = 0;
			/**
			 * The length of the hair on the legs in mm.
			 * @type {number}
			 */
			get legHair(){return this._leghair;}
			set legHair(v){this._leghair = v;}

			_legHairState:ELegHairState = ELegHairState.DEFAULT;
			_legHairWaxExpiration:Date;

			get legHairState():ELegHairState{
				if(this._legHairState == ELegHairState.WAXED && !this._legHairWaxExpiration.isFuture)
					this._legHairState = ELegHairState.DEFAULT;
				return this._legHairState;
			}
			set legHairState(v:ELegHairState){
				if(v != ELegHairState.DEFAULT)
					this.legHair = 0;
				if(v == ELegHairState.WAXED)
					this._legHairWaxExpiration = State.variables.time.dayWithOffsetS(28);
				this._legHairState = v;
			}

			get legHairGrowth(){
				switch(this.legHairState){
					case ELegHairState.DEFAULT: return (this.age < 18 ? 0.14 : 0.21);
					case ELegHairState.WAXED: return 0;
					case ELegHairState.LASERED: return 0;
				}
			}


			get legHairVisibility():ELegHairVisibility{
				if(this.legHair <= 0.25)
					return ELegHairVisibility.SMOOTH;
				else if(this.legHair <= 0.5) // ~3 Days
					return ELegHairVisibility.INVISIBLE;
				else if(this.legHair <= 1.5) // ~1 Week
					return ELegHairVisibility.NOTICABLE;
				else if(this.legHair <= 6) // ~1 Month
					return ELegHairVisibility.LONG;
				return ELegHairVisibility.MANLY;
			}
			set legHairVisibility(v){
				switch (v) {
					case ELegHairVisibility.SMOOTH: 	this._leghair = 0; return;
					case ELegHairVisibility.INVISIBLE: 	this._leghair = 0.2; return;
					case ELegHairVisibility.NOTICABLE: 	this._leghair = 1; return;
					case ELegHairVisibility.LONG: 		this._leghair = 4; return;
					default:							this._leghair = 20; return;
				}
			}
		//#endregion

		//#region Pubes
			pubesLength = 0;					// Pubes hair length in mm
			pubestyle = 0;				  		// The style the pubes get shaved into. 0 is not shaving.
			pubesState = 0;						// 0: Default, 1: lasered
			get pubesGrowth(){
				switch(this.pubesState){
					case 0: return 0.5;
					case 1: return 0;
				}
			}
			get pubesAreLasered(){return (this.pubesState == 1);}
			set pubesAreLasered(v){
				if(v){
					this.pubesLength = 0;
					this.pubesState = 1;
				}else{
					this.pubesState = 0;
				}
			}
		//#endregion
	//#endregion

	preg = 0;						   // 1: is pregnant, 0: is not pregnant
	knowpreg = 0;					   // 1: Is pregnant and knows it, 0: doesn't know is pregnant but could be
	thinkpreg = 0;					  // 1: thinks she is pregnant (doesn't have to be true), 0: doesn't think she is pregnant (doesn't have to be true either)
	PregChem = 0;					   // Size of the pregnancy

	clit_size = 0;

	get isPregnancyAware(){			 // Either knows she is pregnant, correctly assumes to be pregnant or is big enough to no longer be in denial.
		if (this.knowpreg == 1) return 1;
		if (this.preg == 1 && this.thinkpreg == 1) return 1;
		if (this.preg == 1 && this.PregChem > 3600) return 1;
		return 0;
	}

	get bodset(){					   //body image and descriptor control variable, used to indicate which image and descriptor set is in use
		/*if(this.isPregnancyAware == 1) return 3;
		if (this.muscularity >= 70) return 2;
		if (this.muscularity <= 40) return 0;*/
		return 1;
	}

	get body(){
		/*let bodimgsets = State.variables.bodimgsets;
		if(this.isPregnancyAware){
			if(this.PregChem > 6216)
				return bodimgsets[(this.bodset * 10) + 8];
			if(this.PregChem < 2688)
				return bodimgsets[this.bodset * 10];
			return bodimgsets[Math.floor(this.bodset * 10 + ((this.PregChem - 2184) / 504))];
		}
		if(this.salocatnow <= 7)
			return bodimgsets[((this.bodset * 10) + this.salocatnow)];
		return bodimgsets[(this.bodset * 10) + 7];*/
		return null;
	}

	//#region Teeth
		/*pcs_teeth = 0;  //-1: perfectly white
		teeth = {
			brushed: 0,
			caffe_or_tea: 0,
			degradation:0 ,
			smoked: 0
		}

		get teethQuality(){return this.pcs_teeth}*/

		_teeth = 1;
		_teeethMissing = [];
		get teeth(){return this._teeth;}
		set teeth(v){this._teeth=Math.max(0,v);}

		get teethQuality(){
			let t = this.teeth;
			if(t < 100)
				return 0;
			else if(t < 1000)
				return 1;
			return 2;
		}

		set teethQuality(v){
			switch(v){
				case 0: this.teeth = 50; break;
				case 1: this.teeth = 500; break;
				case 2: this.teeth = 1500; break;
			}
		}

		get teethMissingCount(){
			return this._teeethMissing.length;
		}
	//#endregion

	//pcs_breath = 0;


	//#region Lipbalm
		_pcs_lipbalm = 0;
		get pcs_lipbalm(){return this._pcs_lipbalm;}
		set pcs_lipbalm(v){this._pcs_lipbalm = Math.max(0,v);}
	//#endregion
	
	//#region Skin Quality
		//moisturizerDailyCount = 0;
		//skinDailyGain = 0;
		//skinDailyPenalty = 0;
		_pcs_skin = 500;
		get pcs_skin(){
			return this._pcs_skin;
		}
		set pcs_skin(v){
			this._pcs_skin = Math.clamp(v,0,1000);
		}
		get skinAppearance(){
			return Math.round(this.pcs_skin / 200 - 2.5);
		}
		set skinAppearance(v){
			switch(v){
				case -3: this.pcs_skin = 0; return;
				case -2: this.pcs_skin = 100; return;
				case -1: this.pcs_skin = 300; return;
				case 0: this.pcs_skin = 500; return;
				case 1: this.pcs_skin = 700; return;
				case 2: this.pcs_skin = 900; return;
				case 3: this.pcs_skin = 1000; return;
			}
		}

		tan = 0;
	//#endregion

	bodyDailyUpdate(){
		this.dailyEnergyUpdate();

		/*if(this.muscularity > this.strength)
			this.muscularity -= 1;
		else if(this.muscularity < this.strength)
			this.muscularity += 1;

		if(this.healthiness > this.vitality)
			this.healthiness -= 1;
		else if(this.healthiness < this.vitality)
			this.healthiness += 1;

		if(this.dexterity > this.agility)
			this.dexterity -= 1;
		else if(this.dexterity < this.agility)
			this.dexterity += 1;*/


		/*if(this.fat > (17 + this.healthiness / 25)){
			this.salo += 1;
			this.fat = 0;
		}
		else if(this.fat < (-2 - (this.healthiness / 10))){
			this.salo -= 1;
			this.fat = 0;
		}
		this.fat = this.fat / 4;

		this.bodySaloCalc();*/
		this.hairLength += 1;

		/*if(this.pcs_lashes > 2){
			if(this.lashextensionstyle >= 1){
				this.lashextensionduration -= 1;
				if(this.lashextensionduration >= 1 && this.lashextensionduration <= 4)
					message("warn","It's time for you to do your maintenance on your lash extensions; you should go to the salon or you risk growing them all out.");
				if(this.lashextensionduration <= 0){
					message("bad","You waited too long to do maintenance on your lash extensions; there's too little there to notice or work with at this point.");
					this.pcs_lashes = this.pcs_naturallashes;
					this.lashextensionstyle = 0;
					this.lashextensionduration = 0;
					this.lashextensionnew = 0;
				}
			}
			if(this.false_lashes > 0){
				this.false_lashes -= 1;
				if (this.false_lashes == 0){
					message("bad","Your false lashes came off in the night; there's no recovering them now.");
					this.pcs_lashes = this.pcs_naturallashes;
				}
				message("info","Somehow, your lashes managed to stay attached throughout the night. You might be able to get away with wearing them another day straight.");
			}
		}*/

		// Hair colour change
		//TODO: Dasdas
		/*if(this.pcs_haircol != this.nathcol){
			this.hairDyeFade = Math.max(this.hairDyeFade - 1 , 0);
		}*/
		// Leg and pubes hair growth
		this.legHair += this.legHairGrowth;
		//Pubic hair growth at 1/2 per night
		this.pubesLength += this.pubesGrowth;

		this.teeth += 1;

		console.info("Deactivated: degradation of pube hair coloring");
		/*<!-- !!pubic hair colouring-->
	<!-- !! pcs_pubecol = natural colour-->
	<!-- !! pcs_pubecol_num[1] = flag for saveupdate-->
	<!-- !! pcs_pubecol_num[2] = actual colour-->
	<!-- !! pcs_pubecol_num[3] = countdown timer for dye-->
	<<if $pcs_pubecol_num[2] != $pcs_pubecol>>
		<<setinit $pcs_pubecol_num[3] -= 1>>
		<<if $pcs_pubecol_num[3] < 0>>
			<<setinit $pcs_pubecol_num[3] = 0>>

		<</if>>
		<<if $pcs_pubecol_num[3] == 0>>
			<<setinit $pcs_pubecol_num[2] = $pcs_pubecol>>

		<</if>>
	<</if>>
	<<if getvar("$pubesLength") < 2>>
		<<setinit $pcs_pubecol_num[2] = $pcs_pubecol>>

	<</if>>*/

		console.info("Deactivated: degradation of hair scrunches");
		/*
		<<if getvar("$hscrunch") > 0>>
		<<set $hscrunchrand = rand(1, 100)>>
		<<if getvar("$hscrunchrand") <= 8>>
			<<setn $hscrunch -= 1>>

		<</if>>
	<</if>>*/

		/*if(this.pcs_skin <= 300)
 			this.pcs_skin += Math.min(this.skinDailyGain * 2, 20) - this.skinDailyPenalty - 1;
		else if(this.$pcs_skin <= 600)
			this.pcs_skin += Math.min(this.skinDailyGain, 10) - this.skinDailyPenalty - 1;
		else if(this.pcs_skin <= 800)
 			this.pcs_skin += Math.min(this.skinDailyGain / 2, 5) - this.skinDailyPenalty - 1;
		else if(this.pcs_skin <= 900)
 			this.pcs_skin += Math.min(this.skinDailyGain / 3, 3) - this.skinDailyPenalty - 1;
		else if(this.pcs_skin <= 1000)
 			this.pcs_skin += Math.min(this.skinDailyGain / 5, 2) - this.skinDailyPenalty - 1;


		if(this.pcs_teeth < 0){
			// Daly degradation of perfect white teeth
			let tempteeth = 1;
			if(this.teeth['caffe_or_tea'] > 8)
				tempteeth += 1;

			if(this.teeth['smoked'] > 1)
				tempteeth += 1;

			tempteeth -= Math.min(3,	this.teeth['brushed']);

			this.teeth['degradation'] += Math.max(tempteeth, 0);

			this.teeth['brushed'] = 0;
			this.teeth['smoked'] = 0;
			this.teeth['caffe_or_tea'] = 0;
			if(this.teeth['degradation'] > 60)
			{
				// After a certain time of not taking care of your teeth you will loose you perfect whit smile.
				this.teeth['degradation'] = 0;
				this.pcs_teeth = 0;
			}
			this.teeth['degradation'] = Math.max(this.teeth['degradation'],0);
		}

		this.moisturizerDailyCount = 0;
		this.skinDailyGain = 0;
		this.skinDailyPenalty = 0;*/
	}

	get agility(){ return this.skillLevel('agility');}  set agility(v){ this.skillSetLevel('agility',v);}
	get strength(){return this.skillLevel('strength');} set strength(v){this.skillSetLevel('strength',v);}
	get vitality(){return this.skillLevel('vitality');} set vitality(v){this.skillSetLevel('vitality',v);}


	//#region Appearance
		get pcs_makeup(){
			/*if(this.cosmetic_tattoo > 0)
				return this.cosmetic_tattoo + 1;
			return this._pcs_makeup;*/
			console.warn('Usage of get $pc.pcs_makeup is deprecated');
			return Math.floor((this.makeupAmount + this.makeupQuality)/2);
		}
		set pcs_makeup(v){
			console.warn('Usage of set $pc.pcs_makeup is deprecated');
			//this._pcs_makeup = v;
			this.makeupAmount = v;
			this.makeupQuality= v;
		}

		_makeupAmount = 0;
		get makeupAmount(){
			return this._makeupAmount;
		}
		set makeupAmount(v){
			this._makeupAmount = v;
		}

		_makeupQuality = 0;
		get makeupQuality(){
			return this._makeupQuality;
		}
		set makeupQuality(v){
			this._makeupQuality = v;
		}

		cosmetic_tattoo = 0;

		get appearance():Appearance{
			return Appearance.get(this);
		}

		get hotcat():number{
			return this.appearance.currentValue;
		}

		
	//#endregion


	// ----- Toys -----
	analplugin = 0;
	vibratorin = 0;

	// ----- Skills -----

	get intelligence(){
		return this.skillLevel('intelligence');
	}

	//#region Skills
		_skills:{[key: string]:SkillOfCharacter} = {
		}

		skill(key: string):SkillOfCharacter{
			return this._skills[key] ?? {
				ceil:0,floor:0,experience:0,experienceHistory:[0],lastUsed:0
			}
		}

		get skillsAll(){
			let result = {};
			for(const [skillId, skillData] of Object.entries(this._skills)){
				result[skillId] = Object.assign({},skillData,{
					level: this.skillLevel(skillId)
				});
			}
			return result;
		}

		get activeEffectSkillGain():{[skillId:string]:number}{
			return this.#activeEffectValueDictByKey('skillGain','*');
		}

		get activeEffectSkillTagGain():{[skillId:string]:number}{
			return this.#activeEffectValueDictByKey('skillTagGain','*');
		}

		skillGainModifier(skillIdOrSkill:string|Skill):number{
			let modifier:number = 1;
			let skill:Skill;
			if(typeof skillIdOrSkill == "string")
				skill = Skill.get(skillIdOrSkill);
			else
				skill = skillIdOrSkill;

			const activeEffectSkillGain = this.activeEffectSkillGain;
			if(typeof activeEffectSkillGain[skill.id] == "number"){
				modifier *= activeEffectSkillGain[skill.id];
			}
			return modifier;
		}

		skillTagGainModifier(tag:string):number{
			const activeEffectSkillGain = this.activeEffectSkillTagGain;
			if(typeof activeEffectSkillGain[tag] != "number")
				return 1;
			return activeEffectSkillGain[tag];
		}

		
		/**
		 * How much experiences to gain depending on the experience already received today.
		 * @param {string} skillId
		 * @param {number} experience
		 * @returns {number}
		 */
		#skillGain(skillId:string,experience:number):number{
			return Skill.get(skillId).skillGain(this.skill(skillId),experience);
		}

		
		/**
		 * Initializes a skill if it doesn't exist.
		 * @param {string} skillId
		 */
		#skillInit(skillId:string){
			if(!(skillId in this._skills))
				this._skills[skillId] = {
					lastUsed: 0,
					experience: 0,
					experienceHistory:[0],
					ceil: 0,
					floor: 0
				}
		}

		#skill_dayly(){
			let today = State.variables.time.daystart;
			for (const [skillId, skillData] of Object.entries(this._skills)){
				this._skills[skillId].experienceHistory.push(skillData.experience);
				if(this._skills[skillId].experienceHistory.length > 30)
					this._skills[skillId].experienceHistory.shift();
				this._skills[skillId].ceil  = Math.max(skillData.experience, skillData.floor);
				this._skills[skillId].floor = Math.max(Math.ceil(skillData.experience/2), skillData.floor);

				let daysNotUsed = today - this._skills[skillId].lastUsed;
				let newexperience = skillData.experience;

				if(daysNotUsed > 14){
					let currentLevel = this.skill_exp2lvl(skillId,skillData.experience);
					let experienceForCurrentLevel = this.skill_lvl2exp(skillId,currentLevel);
					let experienceForPreviousLevel= this.skill_lvl2exp(skillId,currentLevel-1);
					let experienceDifferenceToPreviousLevel = experienceForCurrentLevel - experienceForPreviousLevel;
					let experienceLoss = Math.ceil(experienceDifferenceToPreviousLevel / 4);
					newexperience -= experienceLoss //* this.timeFactor;
				}

				this._skills[skillId].experience = Math.max(newexperience,this._skills[skillId].floor);
			}
		}

		skill_exp2lvl(skillId:string,experience:number){
			return Skill.get(skillId).experience2Skill(experience);
		}

		skill_lvl2exp(skillId:string,level:number){
			return Skill.get(skillId).skill2Experience(level);
		}

		
		/**
		 * Increases the experience in one skill, taking daily limits into account. Returns the number of actual experience points gained.
		 * @param {string} skillId
		 * @param {number} inc
		 * @returns {number}
		 */
		skillExperienceGain(skillId:string,inc:number,tags:string[]=[]):number{
			if(inc == 0)
				return;
			this.#skillInit(skillId);

			const gainModifier = this.skillGainModifier(skillId);
			inc *= gainModifier;

			for(const tag of tags){
				const tagGainModifier = this.skillTagGainModifier(tag);
				inc *= tagGainModifier;
			}

			const effectiveGain = this.#skillGain(skillId,inc);
			this._skills[skillId].experience += effectiveGain;
			console.log("Skill Experience Gain:",skillId,inc,effectiveGain,this._skills[skillId].experience );
			return effectiveGain;
		}

		skillsExperienceGain(skillObj:{[key: string]:number},factor=1){
			for(const [skillId,inc] of Object.entries(skillObj)){
				this.skillExperienceGain(skillId,inc*factor)
			}
		}


		skillExperienceHistory(skillId){
			this.#skillInit(skillId);
			let eh = clone(this._skills[skillId].experienceHistory);
			eh.push(this._skills[skillId].experience);
			return eh;
		}

		skillExperienceHistoryDayExpTuple(skillId){
			let today = State.variables.time.daystart;
			let eh = this.skillExperienceHistory(skillId);
			let result = [];
			let todayIndex = eh.length - 1;
			for(let i = 0; i < eh.length; i++){
				let day = -todayIndex + i + today;
				let tuple = [day,eh[i]];
				result.push(tuple);
			}
			return result;
		}

		skillFloor(skillId){
			this.#skillInit(skillId);
			return this._skills[skillId].floor;
		}

		skillLevel(skillId){
			this.#skillInit(skillId);
			return (
				this.skillLevelRaw(skillId) + 
				this.#skillLevelBonusOfActiveEffect(skillId)
			);
		}

		skillLevelAtStartOfDay(skillId){
			return this.skill_exp2lvl(skillId,this.skill(skillId).experienceHistory.last());
		}

		#skillLevelBonusOfActiveEffect(skillId:string){
			return this.#activeEffectValueDictByKey('skills','+')[skillId] ?? 0;
		}

		skillLevelRaw(skillId){
			return this.skill_exp2lvl(skillId,this.skill(skillId).experience);
		}

		//Is usually used for initialization, therefore we'll add one history entry if it doesn't exist
		skillSetLevel(skillId:string,lvl:number){
			this.#skillInit(skillId);
			let exp = this.skill_lvl2exp(skillId,lvl);
			this._skills[skillId].experience = exp;
			console.log("Setting skill level (Skill, Lvl, Exp)",skillId,lvl,exp);
		}
	//#endregion

	//#region Fetishes
		_fetishes = {};
		fetish(fetishId){
			return this._fetishes[fetishId] ?? 1;
		}

		fetishSet(fetishId,v){
			this._fetishes[fetishId] = v;
		}

		fetishes(fetishIds){
			if(typeof fetishIds == "string")
				return this.fetishes([fetishIds]);
			let result = 1;
			let indifferentCount = 0;
			let arouseCount = 0;
			for(const fetishId of fetishIds){
				const arousalOfFetish = this.fetish(fetishId);
				if(arousalOfFetish < 0)
					return arousalOfFetish;
				if(arousalOfFetish == 0)
					indifferentCount += 1;
				else if(arousalOfFetish > 1){
					result += arousalOfFetish - 1;
					arouseCount += 1;
				}
			}
			if(indifferentCount > 0 && arouseCount == 0)
				return 0;
			return result;
		}
	//#endregion

	//#region Sex-Stats

		_sexStatistics:SexStatistics = {};

		sexStatistic(activity:ESexEncounterType|ESexEncounterType[],familiarity:ESexEncounterFamiliarity|ESexEncounterFamiliarity[],awareness:('aware'|'unaware'|'both')):number{
			let result = 0;

			if(!activity)
				activity = Object.keys(this._sexStatistics) as ESexEncounterType[];
			else if(!Array.isArray(activity))
				activity = [activity];

			if(!familiarity)
				familiarity = [ESexEncounterFamiliarity.ACQUAINTANCE,ESexEncounterFamiliarity.FRIEND,ESexEncounterFamiliarity.LOVE,ESexEncounterFamiliarity.STRANGER];
			else if(!Array.isArray(familiarity))
				familiarity = [familiarity];

			for(const activityKey of activity){
				const activityData:SexStatistic = this._sexStatistics[activityKey] ?? {};
				for(const familiarityKey of familiarity){
					const familiarityData = activityData[familiarityKey] ?? [0,0];
					switch(awareness){
						case "aware":	result += familiarityData[0]; continue;
						case "unaware":	result += familiarityData[1]; continue;
						case "both":	result += familiarityData[0] + familiarityData[1]; continue;
					}
				}
			}

			return result;
		}

		
		/**
		 * As far as the character is aware of. The correct father will be stored in the pregnancy data.
		 * @type {{npcId:string,time:Date}[]}
		 */
		_sexPossibleFathers:{npcId:string,time:Date}[] = [];

		_sexEncounters:SexEncounter[] = [];

		sexEncounterRegister(npc:NPC|string,types:ESexEncounterType|ESexEncounterType[],familiarity:ESexEncounterFamiliarity,aware:boolean,time:Date){
			if(typeof npc == 'string')
				npc = State.variables.npcs.npc(npc);
			let npcId:string;
			if(npc){
				npcId = npc.id;
			}else
				npcId = '';

			if(!Array.isArray(types))
				types = [types];

			const awareIndex = aware ? 0 : 1;

			for(const type of types){
				this._sexStatistics[type] ??= {};
				this._sexStatistics[type][familiarity] ??= [0,0];
				this._sexStatistics[type][familiarity][awareIndex] += 1;
				if(npc){
					npc.lastSex ??= {};
					npc.lastSex[type] = time;
				}
			}

			this._sexEncounters.push({
				npcId: npcId,
				types: types,
				time: time,
				aware: aware,
				tags: []
			});
		}

		get isVirgin(){
			return (this.sexStatistic(ESexEncounterType.VAGINAL,null,'both') == 0);
		}
		get thinksIsVirgin(){
			return (this.sexStatistic(ESexEncounterType.VAGINAL,null,'aware') == 0);
		}
	//#endregion

	//#region Traits
		_traits = {
			nerd_status: 0
		}

		trait(key,def=0){
			if(key in this._traits)
				return this._traits[key];

			return def;
		}

		traitDec(key,v){
			this._traits[key] -= v;
		}

		traitInc(key,v){
			this._traits[key] += v;
		}

		traitSet(key,v){
			this._traits[key] = v;
		}
	//#endregion

	// ----- Arousal -----
	/*
		For checking arousal and when applicable triggering orgasms.
		action:
			All acts are from Sveta''s perspective and in cases of both giving and receiving, receiving should be used.
			It can be when receiving any of the following
			'clit_finger' - Clit being stimulated directly by a finger
			'clit_vibe' - Clit being stimulated directly by a vibrator (set low, use negative time and double/triple up for more power)
			'porn' - viewing pornographic material
			'voyeur_sex' - watching, usually as in spying on, other people have sex
			'voyeur' - watching, usually as in spying on, erotic acts of others
			'erotic' - being aroused by eroticism
			'erotic_nudity' - being aroused by nudity of others
			'trib' - rubbing pussy against another pussy
			'massage' - rubbing your body, back, feet, etc. with their hands/arms
			'cuni' - stimulation of your pussy by someones toungue
			'rimming' - stimulation of your anus by someones toungue
			'vaginal' 'vaginal_finger' 'vaginal_fist' 'vaginal_dildo' 'vaginal_strap' 'vaginal_vibe' - stimulation of your vagina with a penis and various others
			'self_fisting' - fisting your own vagina
			'anal' 'anal_finger' 'anal_fist' 'anal_dildo' 'anal_strap' 'anal_vibe' - stimulation of your anus with a penis and various others
			'self_fisting_anal' - fisting your own anus
			'kiss' - snogging, tonsil tennis, lip locking, etc.
			'BDSM' - receiving candle wax, flogging, leash play, bondage etc
			'pee' - being peed upon
			also when giving any of the following:
			'flashlite' - flashing underwear
			'flash' - flashing naked breasts/arse/vagina
			'massage_give' - rubbing their body, back, feet, etc. with your hands/arms
			'cuni_give' - stimulating someones pussy with your toungue
			'rimming_give' - stimulating someones anus with your toungue
			'vaginal_finger_give' 'vaginal_fist_give' 'vaginal_dildo_give' 'vaginal_strap_give' 'vaginal_vibe_give' - stimulating someones vagina in various ways
			'clit_finger_give' - stimulating someones clit
			'anal_finger_give' 'anal_fist_give' 'anal_dildo_give' 'anal_vibe_give' 'anal_strap_give' - stimulating someones anus in various ways
			'hj' - jerking a guy off with your hand
			'bj' - sucking a guy off
			'dildo_suck' - simulating a bj on a dildo/strapon
			'titjob' - using boobs to jerk off a guy
			'footjob' - using feet to jerk off a guy
			'BDSM_give' - giving candle wax, flogging, leash play, bondage etc
			'pee_give' - peeing on somone
			finally
			'foreplay' - receiving other stuff
			'foreplay_give' - giving other stuff
		time
			for time taken in minutes - it is use partly for arousal calculation and partly for moving time ahead. If you want to calculate just the arousal and do not move time (simultaneous stimulation), use negative value.
		npcId
			specify the involved npc
		flags
			Are optional but can be themes involved in the act and can be any of the following:
			'maso' 'bound' 'beast' 'exhibitionism' 'rough' 'prostitution' 'dom' 'sub' 'incest'
			'feet' 'lesbian' 'group' 'gangbang' 'humiliation' 'deepthroat' 'unknown' 'gloryhole' 'rape' 'futa' 'masturbate'
			'unaware'
	*/

	_analCapacity = 0.5;
	get analCapacity(){
		return this._analCapacity;
	}

	_vaginalCapacity = 1;
	get vaginalCapacity(){
		return this._vaginalCapacity;
	}

	get vaginalLubrication(){
		return this.horny / 100;
	}

	analCapacityAdaptTo(v){
		if(v <= this._analCapacity)
			return;
		const sizeDifference = v - this._analCapacity;
		if(sizeDifference <= 1)
			this._analCapacity = Math.min(v, this._analCapacity + 0.1);
		else
			this._analCapacity = Math.min(v, this._analCapacity + sizeDifference / 10);
	}

	vaginalCapacityAdaptTo(v){
		if(v <= this._vaginalCapacity)
			return;
		const sizeDifference = v - this._vaginalCapacity;
		if(sizeDifference <= 1)
			this._vaginalCapacity = Math.min(v, this._vaginalCapacity + 0.1);
		else
			this._vaginalCapacity = Math.min(v, this._vaginalCapacity + sizeDifference / 10);
	}

	
	current_arousal_scene_actions = [];

	arouse(action,time,npcId=undefined,flags=undefined){
		console.warn("PlayerCharacter.arouse() is deprecated. Use <<arouse>> instead",action,time,npcId,flags);
	}

	//TODO
	arousalEnd(){
		console.warn("PlayerCharacter.arousalEnd() is deprecated");
	}

	//#region Cum
		/*
			Cum-Locations
			0 = 'In your Vagina'
			1 = 'On your labia'
			2 = 'On your panties over your vagina'
			3 = 'In your anus'
			4 = 'On your butt'
			5 = 'On your panties over your butt'
			6 = 'On your clothes in your groin area'
			7 = 'On your clothes'
			8 = 'On your back'
			9 = 'On your legs'
			10 = 'On your arms'
			11 = 'On your face'
			12 = 'Inside your mouth'
			13 = 'On your hands'
			14 = 'On your stomach'
			15 = 'On your breasts'
			16 = 'In your hair'
			17 = 'In a condom in your vagina
		*/

		_cum:{ [key: string]: Array<any>} = {};

		get cums(){
			this._cumPurgeExpired();
			return this._cum;
		}

		
		/**
		 * Removes cum-data on the inside of the PCs body.
		 * @date 10/8/2023 - 2:16:00 PM
		 */
		_cumPurgeExpired(){
			const bodyparts = setup.getBodyparts();
			let updatedCumData = {};
			const maxAgeInMinutes = 360; // 6 Hours
			let oldestPossibleDateTime = State.variables.time.now;
			oldestPossibleDateTime.setUTCMinutes(oldestPossibleDateTime.getUTCMinutes() - maxAgeInMinutes);

			for (const [bodypart, cumData] of Object.entries(this._cum)) {
				if(bodyparts[bodypart].inside)
					updatedCumData[bodypart] = cumData.filter(cum => cum.time.getTime() >= oldestPossibleDateTime.getTime());
				else
					updatedCumData[bodypart] = cumData;
			}
			this._cum = updatedCumData;
		}

		cum(npcId,bodypartId){
			let bodypartData = setup.getBodypart(bodypartId);
			if(!bodypartData || bodypartData.cumDisabled)
				console.warn('The following bodypart is not enabled for $pc.cum():',bodypartId);
			bodypartId = bodypartData.id;

			const time = State.variables.time;
			const now = time.now;
			const cumData = {
				npc: npcId,
				time: now
			};

			this._cum[bodypartId] ??= [];
			this._cum[bodypartId].push(cumData);
		}

		cumCleanAll(){
			this._cum={};
		}

		cumCleanByActivity(activityId){
			const bodyparts = Object.entries(setup.getBodyparts());
			const bodypartIdsToClean = bodyparts.filter(([id,bodypart]) => bodypart['clean'].includes(activityId)).map(([id,bodypart]) => id);
			for(let bodypartIdToClean of bodypartIdsToClean)
				this._cum[bodypartIdToClean] = [];
		}

		get cumBodypartIds(){
			return Object.entries(this.cums).filter(([bodypartId,cumArray]) => cumArray.length > 0).map(([bodypartId,cumArray]) => bodypartId);
		}

		get cumVisibleBodypartIds(){
			return this.cumBodypartIds.filter(bodypartId=>!this.bodyPartCovered(bodypartId));
		}

	//#endregion


	
	vgape = 0;
	agape = 0;
	spanked = 0;

	//#region Pain
		_pain = new setup.Pain();

		pain(bodypart:string){return this._pain.painByBodypart(bodypart)}
		get painByBodyparts(){return this._pain.painByBodyparts}
		get painByBodypartsSorted(){return this._pain.painByBodypartsSorted}
		painDeteriorate(timeToAdd:number){return this._pain.painDeteriorate(timeToAdd,this.activeEffectPainRegen)}
		




		get activeEffectPainGain():{[bodypartId:string]:number}{
			return this.#activeEffectValueDictByKey('painGain','*');
		}

		get activeEffectPainRegen():{[bodypartId:string]:number}{
			return this.#activeEffectValueDictByKey('painRegen','*');
		}

		painGainModifier(bodypartId:string,reason=''):number{
			const activeEffectPainGain = this.activeEffectPainGain;
			if(typeof activeEffectPainGain[bodypartId] != "number")
				return 1;
			return activeEffectPainGain[bodypartId];
		}
		
		painInc(bodypartId:string,reason='',increase:number){
			increase *= this.painGainModifier(bodypartId);
			return this._pain.painInc(bodypartId,reason,increase);
		}
		painReasonsByBodypartSorted(bodypart){return this._pain.reasonsByBodypartSorted(bodypart)}
		get painTotal(){return this._pain.painTotal}

		get painOfActiveEffects(){
			var painRaw = {};
			const activeEffects = this.#activeEffects();
			for(const activeEffect of Object.values(activeEffects)){
				for(const [bodypartId,reasonData] of Object.entries(activeEffect.pain ?? {})){
					painRaw[bodypartId] ??= {};
					for(const [reasonId,painValue] of Object.entries(reasonData ?? {})){
						painRaw[bodypartId][reasonId] ??= 0;
						painRaw[bodypartId][reasonId] += painValue;
					}
				}
			}

			return painRaw;
		}

		get painReductionOfActiveEffects(){
			return this.#activeEffectValueByKey('painReduction','dim',0.5);
		}
	//#endregion

	timeFactor = 1; //Used for cheating


	//#region  Fame
		_fame = {};

		fame(location,type=null){
			if(!type){
				console.warn('Calling $pc.fame in deprecated format.',location);
				let idParts = location.split('_');
				location = idParts[0];
				let type = idParts[1];
				return this.fame(location,type);
			}
			if(!(location in this._fame))
				return 0;
			if(type == 'slut')
				return this.fame(location,'sex') + this.fame(location,'prostitute');
			return this._fame[location][type] || 0;
		}

		fameDec(location,type,amount,local=false){
			this.fameInc(location,type,amount * -1,local);
		}

		fameInc(location,type,amount,local=false){
			this._fame[location] ??= {};
			this._fame[location][type] ??= 0;

			let current = this._fame[location][type];

			if(typeof amount == "string"){
				if(current > 900){ //The original says 1000... how is this possible if 1000 is supposed to be the soft cap?
					switch(amount){
						case 'tiny':
						case 'small':
							return;
						case 'medium':	amount = rand(0,1);break;
						case 'large':	amount = rand(1,2);break;
						case 'huge':	amount = rand(1,4);break;
						case 'BronzeMedal':	amount = rand(15,25);break;
						case 'SilverMedal':	amount = rand(25,35);break;
						case 'GoldMedal':	amount = rand(35,45);break;
						default:
							console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
							amount = 0;
					}
				}else if(current > 700){
					switch(amount){
						case 'tiny':	amount = rand(0,1);break;
						case 'small':	amount = rand(1,2);break;
						case 'medium':	amount = rand(1,4);break;
						case 'large':	amount = rand(6,12);break;
						case 'huge':	amount = rand(10,24);break;
						case 'BronzeMedal':	amount = rand(25,60);break;
						case 'SilverMedal':	amount = rand(60,100);break;
						case 'GoldMedal':	amount = rand(100,150);break;
						default:
							console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
							amount = 0;
					}
				}else if(current > 400){
					switch(amount){
						case 'tiny':	amount = rand(1,2);break;
						case 'small':	amount = rand(1,4);break;
						case 'medium':	amount = rand(6,12);break;
						case 'large':	amount = rand(10,24);break;
						case 'huge':	amount = rand(20,50);break;
						case 'BronzeMedal':	amount = rand(50,100);break;
						case 'SilverMedal':	amount = rand(100,150);break;
						case 'GoldMedal':	amount = rand(150,200);break;
						default:
							console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
							amount = 0;
					}
				}else{
					switch(amount){
						case 'tiny':	amount = rand(1,4);break;
						case 'small':	amount = rand(6,12);break;
						case 'medium':	amount = rand(10,24);break;
						case 'large':	amount = rand(20,50);break;
						case 'huge':	amount = rand(40,70);break;
						case 'BronzeMedal':	amount = rand(150,250);break;
						case 'SilverMedal':	amount = rand(250,350);break;
						case 'GoldMedal':	amount = rand(350,450);break;
						default:
							console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
							amount = 0;
					}
				}
			}else{
				if(current > 900)
					amount /= 10;
				else if(current > 700)
					amount /= 6;
				else if(current > 400)
					amount /= 3;
			}

			if(amount == 0)
				return;

			let target = Math.clamp(current+amount,0,1000);

			this._fame[location][type] = target;

			if(type in fameAlwaysLocale)
				local = true;
		}
	//#endregion


	//#region Effects
		_effects:{ [key: string]: { expiration:Date; [key: string]: any; } } = {};
		
		get activeEffectIds():string[]{
			return Object.keys(this.#activeEffects());
		}

		#activeEffects(fields:string[]=[]):{[key: string]:Effect}{
			let result:{[key: string]:Effect} = {};
			const time = State.variables.time;
			
			for(const [effectid,effect] of Object.entries(this.drugsActiveEffects)){
				if(fields.length && !Object.keys(effect).includesAny(fields))
					continue;
				result[effectid] = effect;
			}


			for(const [effectid,effectData] of Object.entries(this._effects)){
				if(effectData.expiration === undefined)
					continue;
				if(
					(!fields.length || Object.keys(effectData).includesAny(fields)) &&
					(effectData.expiration === null || time.isFuture(effectData.expiration))
				)
					result[effectid] = Object.assign({},setup.getEffect(effectid),effectData);
			}

			for(const [effectid,effectMetaData] of Object.entries(setup.activeEffects)){
				let effect:Effect;
				if(typeof effectMetaData.effect == "string")
					effect = setup.getEffect(effectMetaData.effect);
				else
					effect = clone(effectMetaData.effect);

				//We need to check the field first to avoid infinite recursions
				if(fields.length && !Object.keys(effect).includesAny(fields))
					continue;
				
				if(typeof effectMetaData.isActive == "boolean"){
					if(!effectMetaData.isActive)
						continue;
				}else{
					if(!effectMetaData.isActive(this))
						continue;
				}
				
				result[effectid] = effect;
			}

			return result;
		}

		get activeEffectsMoodlets():{ [key: string]: ActiveMoodlet; }{
			const time = State.variables.time;
			return Object.fromEntries(
				this.activeEffectsMoodletIDs.map(
					(moodletId) => 
						[
							moodletId,
							ActiveMoodlet.create(moodletId, {expiration: time.endTime})
						]
				)
			);

		}

		get activeEffectsMoodletIDs():Array<string>{
			var result = [];
			const activeEffects = this.#activeEffects();
			for(const activeEffect of Object.values(activeEffects)){
				if(activeEffect.moodlet)
					result.push(activeEffect.moodlet);
			}
			return result;
		}

		get activeEffectsSidebar(){
			return Object.values(this.#activeEffects()).filter(effect=>effect.sidebar);
		}

		
		/**
		 * @param {string} key
		 * @param {undefined|'+'|'*'|'dim'} [reduceMode='+']
		 * @param {any[]} args Additional arguments.
		 * @returns {number}
		 */
		#activeEffectValueByKey(key,reduceMode='+',...args):number{
			const valueArray = this.#activeEffectValuesByKey(key);
			switch (reduceMode) {
				case '*':
					return valueArray.reduce((accumulator, currentValue) => accumulator * currentValue,1);
				case 'dim':
					const base = args[0] ?? 0.5;
					return valueArray.sort((a,b)=>b-a).reduce((accumulator, currentValue,index) => accumulator + Math.pow(base,index) * currentValue,0);
				case '+':		
				default:
					return valueArray.reduce((accumulator, currentValue) => accumulator + currentValue,0);
			}
		}

		#activeEffectValueDictByKey(key:string,reduceMode:('+'|'*')='*',...args:any):{[key:string]:number}{
			const result:{[skillId:string]:number} = {};

			const valueArray:{[skillId:string]:number}[] = this.#activeEffectValuesByKey(key);

			for(const skillModifiers of valueArray){
				for(const [skillId, skillModifier] of Object.entries(skillModifiers)){
					switch(reduceMode){
						case '*':
							result[skillId] ??= 1;
							result[skillId] *= skillModifier;
							continue;
						case '+':
							result[skillId] ??= 0;
							result[skillId] += skillModifier;
							continue;
					}
				}
			}

			return result;
		}

		

		
		/**
		 * @param {string} key
		 * @returns {{}}
		 */
		#activeEffectValuesByKey(key:string){
			var result =[];
			const activeEffects = this.#activeEffects([key]);
			for(const effect of Object.values(activeEffects)){
				result.push(effect[key]);
			}
			return result;
		}

		/**
		 * Description placeholder
		 * @date 10/3/2023 - 1:07:07 PM
		 *
		 * @param {string} effectId
		 * @param {number|Date|null} expirationOrDuration - Either a date when the effect expires, a number that indicates the minutes til it expires, or null if it won't expire.
		 */
		effectAdd(effectId:string, expirationOrDuration, metadata={}){
			const time = State.variables.time;
			let expiration = expirationOrDuration;
			if(typeof expirationOrDuration == 'number')
				expiration = time.nowWithMinutesOffset(expirationOrDuration);
			this._effects[effectId] = Object.assign({expiration:expiration},metadata) ;
		}

		effectDelete(effectId:string){
			delete this._effects[effectId];
		}

		
		/**
		 * @param {string} effectId
		 * @returns {boolean}
		 */
		effectIsActive(effectId:string):boolean{
			return (effectId in this.#activeEffects());
		}

	//#endregion

	//#region  Timed Actions
		execute_every_15_minutes(){
			const time = State.variables.time;

			let timeMod = 0.25 * this.timeFactor; //We are doing some hourly calculations 4 times as often

			let change = {
				'energy': 0,
				'hydra': 0
			}

			if(this.isSleeping){
				change.energy = timed_stat_changes['energy']['sleep'] * timeMod;
				change.hydra  = timed_stat_changes['hydra']['sleep']  * timeMod;
			}else{
				change.energy = timed_stat_changes['energy']['default'] * timeMod;
				change.hydra  = timed_stat_changes['hydra']['default']  * timeMod;
			}

			this.pcs_energy -= change.energy;
			this.pcs_hydra  -= change.hydra;
			this.energyDemand += change.energy;


			if(this.isSleeping){
				this.pcs_sleep -= timed_stat_changes['sleep']['sleep'] * timeMod ;
				this.pcs_stam += this.stammax / 10;
			}else{
				this.pcs_sleep -= timed_stat_changes['sleep']['default'] * timeMod ;
				this.pcs_stam += this.stammax / 20;
			}


			// ----- Dying -----
			for (const [riskId, riskData] of Object.entries(dieRisks)){
				this._death[riskId] ??= {stage:0};
				if(this[riskData['variable']] == 0){
					if(this._death[riskId].stage == 0 || !time.isFuture(this._death[riskId].nextStageDate)){
						if((this._death[riskId].stage ?? 0) >= riskData.durations.length){
							this.gameover = riskId;
							console.warn("GAMEOVER set",riskId);
						}else{
							this._death[riskId].nextStageDate = time.nowWithMinutesOffset(riskData.durations[this._death[riskId].stage]);
							this._death[riskId].stage += 1;
							this.moodletApplyById(riskId+'_'+this._death[riskId].stage);
						}
					}
				}
			}

		}

		execute_every_1_hour(){
			/*let timeFactor = this.timeFactor;
			if(this.mood <= 20)
				this.will_counter -= 2 * timeFactor;

			if(this.willpowermax > 100){
				if(this.pcs_willpwr < 25)
					this.will_counter -= 1 * timeFactor;
			}else{
				if(this.pcs_willpwr < this.willpowermax / 4)
					this.will_counter -= 1 * timeFactor;
			}
			if(this.will_counter <= -10){
				this.willpowermax -= 1;
				this.will_counter = 0;
			}

			



			this.pcs_lipbalm -= 1 * timeFactor;*/

		}

		execute_every_1_day(){
			this.bodyDailyUpdate();
			this.#skill_dayly();
			this.#moodletsClean();
		}

		execute_every_timeUpdate(timeToAdd:number){
			let timestamp = State.variables.time.minutesTimestamp;
			if(this.deodorant_on == 1 && timestamp > this.deodorant_time)
				this.deodorant_on = 0;
			
			this.horny -= timeToAdd * this.hornyDeteriorationRate;

			this.drugsDeteriorate(timeToAdd);

			this.painDeteriorate(timeToAdd);

			this.#mentalHealthRegenerate(timeToAdd);
			this.#willpowerRegenerate(timeToAdd);
		}
	//#endregion	

	//#region Deco
		// Deco includes tattoos, piercings, glasses, etc.
		_deco = {}
		_decoOwned = {}
		decoAdd(type,position,index){
			if(!this._decoOwned[type])
				this._decoOwned[type] = {};
			if(!this._decoOwned[type][position])
				this._decoOwned[type][position] = [];
			if(!this._decoOwned[type][position].includes(index))
				this._decoOwned[type][position].push(index);
		}
		decoHas(type:string,position:string,index:number){
			if(!(this._decoOwned[type]?.[position]))
				return false;
			return this._decoOwned[type][position].includes(index);
		}

		
		/**
		 * Returns the index of a decoration of `type` worn at `position`. 
		 * @param {string} type
		 * @param {string} [position='default']
		 * @returns {number} 0 if there is no decoration, -1 if the type is prepared but empty (such as having a piercing) or >0 if a decoration is active.
		 */
		decoGet(type:string,position: string='default'):number{
			return this._deco[type]?.[position] ?? 0;
		}
		decoImage(type:string,position:string){
			let constantDecoData = setup.getDeco(type,position,this.decoGet(type,position));
			return constantDecoData.image;
		}
		get decoOwned(){return this._decoOwned;}
		decoRemove(type,position='default'){
			this.decoSet(type,position,null);
		}
		decoSet(type,position='default',v){
			if(!(type in this._deco))
				this._deco[type] = {};
			this._deco[type][position] = v;
		}
		decoWear(type,position='default',v){
			this.decoSet(type,position,v);
			if(v && v != -1)
				this.decoAdd(type,position,v);
		}
	//#endregion

	// ----- Bodyparts -----
	bodyPartCovered(bodypartId){
		let bodypartData = setup.getBodypart(bodypartId);
		if(!bodypartData){
			console.error('Data not found by $pc.bodyPartCovered for bodypart',bodypartId);
			return false;
		}

		if(typeof bodypartData.covered === "undefined")
			return false;
		if(typeof bodypartData.covered === "boolean")
			return bodypartData.covered;
		/*if(typeof bodypartData.covered === "string")
			return Scripting.evalTwineScript(bodypartData.covered);*/
		if(typeof bodypartData.covered === "function")
			return bodypartData.covered(this);
		
		console.error('Unsupported type of covered of bodypart in $pc.bodyPartCovered',bodypartId,bodypartData);
		return false;
	}

	// ----- Init -----
	init_final(){
		for (const [skillId, skillData] of Object.entries(this._skills)){
			this._skills[skillId].experienceHistory = [skillData.experience];
		}
	}


	//#region Occupation
		get isSchoolStudent(){
			return State.variables.q.questIsActive("school");
		}
	//#endregion

	//#region Vehicles
		_vehicleInUse = null;
		get vehicleInUse(){return this._vehicleInUse;}
		set vehicleInUse(v){this._vehicleInUse = v;}

		get vehicleData(){
			let inventory = State.variables.inventory;
			let vehicleData = inventory.metadata(this.vehicleInUse,'vehicles');
			return vehicleData;
		}

		vehicleCanEnterPassage(passage){
			var vehicleType = this.vehicleType;
			if(vehicleType != 'car')
				return true;
			var passageTags = tags(passage);
			return passageTags.includes('car');
		}

		get vehicleSpeed(){
			return this.vehicleData.speed ?? 1;
		}

		get vehicleType(){
			if(!this.vehicleInUse)
				return 'walk';
			let inventory = State.variables.inventory;
			let vehicleData = inventory.metadata(this.vehicleInUse,'vehicles');
			return vehicleData.type;
		}
	//#endregion

	//#region Menstruation Cycle

		_cycleStart = -7;
		_cycleLength = 28;
		_cycleLengthLast = 28;

		get cycleDay(){
			const time = State.variables.time;
			const daystart = time.daystart;
			return daystart - this.cycleStart;
		}
		get cycleLength(){return this._cycleLength;}					// the length of the current cycle
		set cycleLength(v){this._cycleLength = v;}
		get cycleLengthLast(){return this._cycleLengthLast;}
		set cycleLengthLast(v){this._cycleLengthLast = v;}
		get cycleStart(){return this._cycleStart;}							// the daystart of the current cycle
		set cycleStart(v){this._cycleStart = v;}
		cycleStartMessageSent = true;
		cycleStartHour = 17;
		get menstruationLength(){return 3;}
		get ovulationDay(){return this.cycleStart + this.cycleLength - 14;}
		ovulationHour = 19;

		get cyclePhase(){
			if(this.effectIsActive('cycleBlocked'))
				return 'blocked';

			const today = State.variables.time.daystart;

			if(today - this.cycleStart - 1 > this.cycleLength || (today - this.cycleStart > this.cycleLength && State.variables.time.hour >= this.cycleStartHour))
				this.cycleStartNew();

			if(today == this.cycleStart){
				//if(State.variables.time.hour < this.cycleStartHour)
				//	return 'end';
				return 'menses';
			}else if(this.cycleStart + this.menstruationLength >= today){
				return 'menses';
			}else if(today < this.ovulationDay){
				return 'start';
			}else if(today == this.ovulationDay && State.variables.time.hour < this.ovulationHour){
				return 'start';
			}else{
				return 'end';
			}

		}

		cycleStartNew(){
			const today = State.variables.time.daystart;
			this.cycleStart = today;
			this.cycleStartMessageSent = false;

			this.cycleLengthLast = this.cycleLength;

			if(this.cycleLength >= 35)
				this.cycleLength += rand(-3,0);
			else if(this.cycleLength >= 33)
				this.cycleLength += rand(-2,1);
			else if(this.cycleLength >= 26)
				this.cycleLength += rand(-1,1);
			else if(this.cycleLength >= 24)
				this.cycleLength += rand(-1,2);
			else
				this.cycleLength += rand(0,3);

			this.cycleStartHour = rand(0,23);
			this.ovulationHour = rand(0,23);
		}

		get cycleArousalModificator(){
			switch(this.cyclePhase){
				case 'end':
					return {factor: 0.5, target: 0};
				case 'menses':
					return {factor: 1, target: 0};
				case 'start':
				default:
					const ovulationDay = this.ovulationDay;
					const daystart = State.variables.time.daystart;
					const daysTilOvolationDay = ovulationDay - daystart;
					if(daysTilOvolationDay <= 1)
						return {factor: 2, target: 30};
					if(daysTilOvolationDay <= 2)
						return {factor: 1.5, target: 10};
					return {factor: 1, target: 0};
			}
		}


		get cycleProductsEnabled(){
			const today = State.variables.time.daystart;

			if(this.cyclePhase == 'menses')
				return true;

			if(today - this.cycleStart >= this.cycleLength)
				return true;

			return false;
		}

		_cycleProduct = null;
		get cycleProduct() {
			return this._cycleProduct;
		}
		set cycleProduct(value) {
			this._cycleProduct = value;
		}
		cycleProductSince = null;
		

		get cycleProductsUsed(){
			if(State.variables.time.secondsSinceDate(this.cycleProductSince) > 43200)
				return false;
			return !(!(this._cycleProduct));
		}
	//#endregion

	//#region Indecencies
		get indecencies(){
			let result = [];

			if(this.cyclePhase == 'menses' && !this.cycleProductsUsed)
				result.push('menses_blood');

			if(State.variables.wardrobe.isNaked){
				if(!State.variables.wardrobe.bra.isValidItem)
					result.push('naked_breast');
				if(!State.variables.wardrobe.panties.isValidItem)
					result.push('naked_pussy');
				
			}

			if(this.cumVisibleBodypartIds.length > 0)
				result.push('cum');

			return result;
		}

		get isIndecent(){
			return (!(this.indecencies.length == 0));
		}
	//#endregion

	//#region SYSTEM
		constructor(){}

		_init(playerCharacter: { [x: string]: any; }){
			Object.keys(playerCharacter).forEach(function (pn) {
				this[pn] = clone(playerCharacter[pn]);
			}, this);

			return this;
		}

		clone = function () {
			return (new setup.PlayerCharacter())._init(this);
		};

		toJSON = function () {
			var ownData = {};
			Object.keys(this).forEach(function (pn) {
				if(typeof this[pn] !== "function")
					ownData[pn] = clone(this[pn]);
			}, this);
			return JSON.reviveWrapper('(new setup.PlayerCharacter())._init($ReviveData$)', ownData);
		};
	//#endregion
}

setup.PlayerCharacter = PlayerCharacter;
