PlayerCharacter.ts 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455
  1. declare let rand: Window["rand"];
  2. const bmi_attractive_range = [18.5,30];
  3. const timed_stat_changes ={ //Hourly
  4. energy: {
  5. sleep: 2,
  6. default: 6
  7. },
  8. hydra: {
  9. sleep: 4,
  10. default: 12
  11. },
  12. sleep:{
  13. sleep: -8,
  14. default: 4
  15. }
  16. }
  17. const dieRisks = {
  18. 'hunger':{
  19. variable: 'pcs_energy',
  20. durations: [720,720,14400]
  21. },
  22. 'thirst':{
  23. variable: 'pcs_hydra',
  24. durations: [720,720,1440]
  25. }
  26. }
  27. const fameAlwaysLocale = ['sex','prostitute'];
  28. class PlayerCharacter{
  29. gameover = 0;
  30. _death = {};
  31. name_first = 'Svetlana';
  32. name_last = 'Lebedev';
  33. name_nick = 'Sveta';
  34. get name_full(){return `${this.name_first} ${this.name_last}`;}
  35. avatar = '';
  36. dailyUpdate(){
  37. this.bodyDailyUpdate();
  38. }
  39. birthday = 1;
  40. birthmonth = 4;
  41. birthyear = 1999;
  42. get birthdayDate(){
  43. return new Date(Date.UTC(this.birthyear,this.birthmonth-1,this.birthday));
  44. }
  45. get age(){
  46. let age = State.variables.time.ageOfDate(this.birthday,this.birthmonth,this.birthyear);
  47. if(age < 16)
  48. {
  49. console.error("Critical Error: Playercharacter too young");
  50. Engine.restart();
  51. }
  52. return age;
  53. }
  54. set age(v){
  55. v = Math.max(v,16);
  56. let currentAge = this.age;
  57. let difference = v - currentAge;
  58. this.birthyear -= difference;
  59. console.log("AGE set to "+v);
  60. }
  61. /*get ageDays(){
  62. const oneDay = 86400000;
  63. let now = State.variables.time.now;
  64. let bday = this.birthdayDate;
  65. return Math.floor((now - bday)/oneDay);
  66. }*/
  67. _visualAgeDaysOffset = 0;
  68. get visualAgeDays(){return this._visualAgeDaysOffset}
  69. set visualAgeDays(v){this._visualAgeDaysOffset = v;}
  70. get visualAgeDaysInverse(){
  71. return this._visualAgeDaysOffset * -1;
  72. }
  73. set visualAgeDaysInverse(v){
  74. this._visualAgeDaysOffset = v * -1;
  75. }
  76. get visualAge(){
  77. return State.variables.time.ageOfDate(this.visualBirthday);
  78. }
  79. set visualAge(v){
  80. let currentVisualAge = this.visualAge;
  81. let offset = v - currentVisualAge;
  82. this._visualAgeDaysOffset += Math.floor(offset * 365.25);
  83. }
  84. get visualBirthday(){
  85. let visualBirthday = this.birthdayDate;
  86. visualBirthday.setUTCDate(visualBirthday.getUTCDate() - this._visualAgeDaysOffset);
  87. return visualBirthday;
  88. }
  89. get consciousness(){
  90. return this.#activeEffectValueByKey('consciousness','*');
  91. }
  92. /* Mood */
  93. _mood = new setup.Mood();
  94. get mood(){return this._mood.mood;}
  95. set mood(v){this._mood.mood = v}
  96. //get moodlets(){return this._mood.moodlets}
  97. get moodletsActive(){return this._mood.moodletsActive}
  98. get moodletsActiveByGroup(){return this._mood.moodletsActiveByGroup}
  99. get moodletsActiveEffect(){return this._mood.moodletsActiveEffect}
  100. get moodletsActiveByGroupAccumulationApplied(){return this._mood.moodletsActiveByGroupAccumulationApplied}
  101. moodletApplyById(moodletId,minutes=0){return this._mood.moodletApplyById(moodletId,minutes)}
  102. //moodletCombinedData(moodletId){return this._mood.moodletCombinedData(moodletId)}
  103. moodletDeactivateById(moodletId){return this._mood.moodletDeactivateById(moodletId)}
  104. moodletIncTime(moodletId,minutes,skipIncludedMoodlets=false){return this._mood.moodletIncTime(moodletId,minutes,skipIncludedMoodlets)}
  105. //moodletIsActive(moodletId){return this._mood.moodletIsActive(moodletId)}
  106. moodletUpdate(moodletId,updateObject){return this._mood.moodletUpdate(moodletId,updateObject)}
  107. #moodletsClean(){return this._mood._moodletsClean();}
  108. get moodletsSpecial():{[key: string]: ActiveMoodlet}{
  109. //return Object.assign({},this.moodletPain,this.activeEffectsMoodlets);
  110. return {pain:this.moodletPain};
  111. }
  112. get moodletPain():ActiveMoodlet{
  113. return PainMoodlet.createPainMoodlet(this.painTotal);
  114. }
  115. _pcs_willpwr = 70;
  116. get pcs_willpwr(){return this._pcs_willpwr;}
  117. set pcs_willpwr(v){
  118. this._pcs_willpwr = Math.clamp(v,0,this.willpowermax);
  119. console.log("Willpower set to "+this._pcs_willpwr);
  120. }
  121. will_counter = 0;
  122. _willpowermax = 70;
  123. get willpowermax(){return this._willpowermax}
  124. set willpowermax(v){this._willpowermax = Math.max(50,v);}
  125. pcs_willpower_feeder = 0;
  126. /* Hunger and eating */
  127. _energyBalance = 0; // A value between -6 and 6 that determines how fast you gain or lose bmi.
  128. _energy = 0; // The energy you consumed today.
  129. _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.
  130. _pcs_energy = 100;
  131. /**
  132. * This is effecitvely the hunger-bar.
  133. * @date 7/23/2023 - 5:56:29 PM
  134. *
  135. * @type {number}
  136. */
  137. get pcs_energy(){return this._pcs_energy;}
  138. set pcs_energy(v){
  139. this._pcs_energy = Math.clamp(v,0,100);
  140. if(v > 0 && this._death['hunger']?.stage){
  141. this._death['hunger'] = {stage:0};
  142. for(let i=1;i<=dieRisks.hunger.durations.length;i++)
  143. this.moodletDeactivateById('hunger_'+i);
  144. }
  145. }
  146. _dieHungerStage = 0;
  147. _dieHungerNextStageDate = undefined;
  148. get energy(){return this._energy;}
  149. set energy(v){this._energy = v;}
  150. get energyBalance(){return this._energyBalance;}
  151. set energyBalance(v){this._energyBalance = v;}
  152. get energyDemand(){return this._energyDemand;}
  153. set energyDemand(v){this._energyDemand = v;}
  154. /**
  155. * Energy Demand adjusted for the current BMI.
  156. * @date 7/27/2023 - 11:47:05 AM
  157. *
  158. * @readonly
  159. * @type {number}
  160. */
  161. get energyRequirement(){
  162. let baseRequirement = this.energyDemand; //timed_stat_changes.energy.sleep * 8 + timed_stat_changes.energy.default * 16;
  163. let requirementFactor = baseRequirement / 100;
  164. let bmi = this.bmi;
  165. // The following calculations assume that the base requirement is 100, therefore we need to multiplicate them with requirementFactor
  166. if(bmi < 19)
  167. return requirementFactor * (100 - Math.sqrt(19-bmi) * 10);
  168. if (bmi > 25)
  169. return requirementFactor * (100 + 5 * (bmi - 25));
  170. return baseRequirement;
  171. }
  172. _energyHistory = [];
  173. get energyHistory(){return this._energyHistory;}
  174. _energyHistoryLengthTarget = 7;
  175. get energyHistoryLengthTarget(){return this._energyHistoryLengthTarget;}
  176. set energyHistoryLengthTarget(v){
  177. this._energyHistoryLengthTarget = v;
  178. while(this._energyHistory.length > v)
  179. this._energyHistory.shift();
  180. }
  181. dailyEnergyUpdate(){
  182. let energyRequirement = this.energyRequirement;
  183. let energy = this.energy;
  184. let energyQuota = energy / energyRequirement;
  185. let balanceTarget = 0;
  186. if(energyQuota < 0.5)
  187. balanceTarget = -6;
  188. else if(energyQuota < 0.8)
  189. balanceTarget = -3;
  190. else if(energyQuota > 1.5)
  191. balanceTarget = 6;
  192. else if(energyQuota > 1.2)
  193. balanceTarget = 3;
  194. let balanceCurrent = this.energyBalance;
  195. let balanceDifference = balanceTarget - balanceCurrent;
  196. // Gaining weight is way easier than losing it.
  197. if(balanceDifference < 0)
  198. balanceDifference = balanceDifference / 3;
  199. else
  200. balanceDifference = balanceDifference * 2 / 3;
  201. this.energyBalance += balanceDifference;
  202. // Change the BMI
  203. let bmi_change = Math.pow(this.energyBalance,2) * 0.005 * Math.sign(this.energyBalance);
  204. this.bmi += bmi_change;
  205. console.log("$pc.dailyEnergyUpdate(): energyRequirement,energy,balanceOld, balanceNew, bmiChange, bmiNew",energyRequirement,energy,balanceCurrent,this.energyBalance,bmi_change,this.bmi);
  206. this._energyHistory.push({
  207. energyRequirement: energyRequirement,
  208. energy: energy,
  209. balanceTarget: balanceTarget,
  210. balanceChange: balanceDifference,
  211. balanceNew: this.energyBalance,
  212. bmiChange: bmi_change,
  213. bmiNew: this.bmi
  214. });
  215. this.energy = 0;
  216. this.energyDemand = 0;
  217. }
  218. _pcs_hydra = 100;
  219. _dieThirstStage = 0;
  220. _dieThirstNextStageDate = undefined;
  221. get pcs_hydra(){return this._pcs_hydra;}
  222. set pcs_hydra(v){
  223. /*if(v < 0){
  224. v = 0;
  225. this.pcs_health -= 5;
  226. setup.msgPredefined("warn_hydra_low");
  227. }
  228. */
  229. this._pcs_hydra = Math.clamp(v,0,100);
  230. if(v > 0 && this._death['thirst']?.stage){
  231. this._death['thirst'] = {stage:0};
  232. for(let i=1;i<=dieRisks.thirst.durations.length;i++)
  233. this.moodletDeactivateById('thirst_'+i);
  234. }
  235. }
  236. minutesTilStat(stat,target=0,isAsleep=undefined){
  237. const time = State.variables.time;
  238. isAsleep ??= (this.isSleeping == 1);
  239. let current = 0;
  240. let configIndex;
  241. switch(stat){
  242. case 'energy':
  243. case 'hunger':
  244. configIndex = 'energy';
  245. current = this.pcs_energy;
  246. break;
  247. case 'hydra':
  248. case 'thirst':
  249. configIndex = 'hydra';
  250. current = this.pcs_hydra;
  251. break;
  252. }
  253. const timeMod = 0.25 * this.timeFactor;
  254. let change = 0;
  255. if(isAsleep)
  256. change = timed_stat_changes[configIndex]['sleep'] * timeMod;
  257. else
  258. change = timed_stat_changes[configIndex]['default'] * timeMod;
  259. let requiredUpdates = Math.ceil((current - target) / (change || 1));
  260. const minutesToNext15MinutesInterval = 15 - time.now.getUTCMinutes() % 15;
  261. return Math.max(0,(requiredUpdates - 1)*15 + minutesToNext15MinutesInterval);
  262. }
  263. _pcs_sleep = 100;
  264. get pcs_sleep(){return this._pcs_sleep;}
  265. set pcs_sleep(v){
  266. /*if(v < 0){
  267. v = 0;
  268. this.mood -= 5;
  269. setup.msgPredefined("warn_sleep_low");
  270. }*/
  271. this._pcs_sleep = Math.clamp(v,0,100);
  272. console.log("Sleep set to "+this._pcs_sleep);
  273. }
  274. isSleeping = 0;
  275. _pcs_sweat = 0;
  276. get pcs_sweat(){return this._pcs_sweat;}
  277. set pcs_sweat(v){
  278. this._pcs_sweat = v;
  279. console.log("Sweat set to "+this._pcs_sweat);
  280. }
  281. sweatAdd(v){
  282. this.pcs_sweat += v;
  283. }
  284. get hornyMin(){
  285. const cycleArousalModificator = this.cycleArousalModificator;
  286. return cycleArousalModificator.target;
  287. }
  288. _pcs_horny = 0;
  289. get horny(){return Math.max(this._pcs_horny,this.hornyMin);}
  290. set horny(v){
  291. if(typeof v != "number" || isNaN(v)){
  292. console.error("Trying to set pcs_horny to non-number",v);
  293. return;
  294. }
  295. this._pcs_horny = Math.clamp(v,0,100);
  296. console.log("Horny set to "+this._pcs_horny);
  297. }
  298. /**
  299. * How much horny is supposed to decrease each minute.
  300. * @readonly
  301. * @type {number}
  302. */
  303. get hornyDeteriorationRate(){
  304. const cycleArousalModificator = this.cycleArousalModificator;
  305. return (1 / cycleArousalModificator.factor);
  306. }
  307. //_pcs_inhib = 0;
  308. get pcs_inhib(){
  309. //return this._pcs_inhib;
  310. return this.skillLevel('inhibition');
  311. }
  312. set pcs_inhib(v){
  313. //this._pcs_inhib = Math.clamp(v,0,100);
  314. //console.log("Inhibition set to "+this._pcs_inhib);
  315. this.skillSetLevel('inhibition',v);
  316. }
  317. _frost = 0;
  318. get frost(){
  319. if(this.alko > 0)
  320. return 0;
  321. return this._frost;
  322. }
  323. set frost(v){
  324. this._frost = v;
  325. }
  326. /**
  327. * @type {setup.Drugs}
  328. */
  329. _drugs = new setup.Drugs();
  330. get drugsActiveEffects(){return this._drugs.activeEffects}
  331. get drugsActiveEffectIds(){return this._drugs.activeEffectIds}
  332. drugsDeteriorate(minutes){return this._drugs.deteriorate(minutes)}
  333. drugVolInc(drugId, inc){return this._drugs.volInc(drugId, inc)}
  334. drugVolSet(drugId,v){return this._drugs.volSet(drugId, v)}
  335. drugVol(drugId){return this._drugs.vol(drugId)}
  336. get alko(){return this.drugVol('alcohol')}
  337. set alko(v){this.drugVolSet('alcohol',v)}
  338. // ----- Appearance History ------
  339. _appearanceHistory = [];
  340. appearanceHistoryPush(varname,val){
  341. this._appearanceHistory.push({
  342. time: State.variables.time.now,
  343. varname: varname,
  344. val: val
  345. });
  346. }
  347. // ----- Body ------
  348. bodyImage(part){
  349. }
  350. _pcs_vag = 0
  351. get pcs_vag(){return this._pcs_vag;}
  352. set pcs_vag(v){this._pcs_vag = Math.min(36,v);}
  353. _pcs_ass = 0
  354. get pcs_ass(){return this._pcs_ass;}
  355. set pcs_ass(v){this._pcs_ass = Math.min(36,v);}
  356. _pcs_throat = 0
  357. get pcs_throat(){return this._pcs_throat;}
  358. set pcs_throat(v){this._pcs_throat = Math.min(36,v);}
  359. _pcs_health = 0;
  360. get pcs_health(){return Math.min(this._pcs_health, this.healthmax)}
  361. set pcs_health(v){
  362. if(v < 0)
  363. this.gameover = 1;
  364. this._pcs_health = Math.min(v, this.healthmax);
  365. }
  366. get healthmax(){
  367. let healthmax_calc = Math.max(1,(this.vitality * 10 + this.strength * 5));
  368. let mult_by_pain = 1;
  369. let pain_total = this.painTotal;
  370. if(pain_total > 80)
  371. mult_by_pain = 0.20;
  372. else if(pain_total > 60)
  373. mult_by_pain = 0.40;
  374. else if(pain_total > 40)
  375. mult_by_pain = 0.60;
  376. else if(pain_total > 20)
  377. mult_by_pain = 0.80;
  378. else if(pain_total > 0)
  379. mult_by_pain = 0.90;
  380. healthmax_calc = Math.ceil(healthmax_calc * mult_by_pain);
  381. return healthmax_calc;
  382. }
  383. _pcs_stam = 0;
  384. get pcs_stam(){return this._pcs_stam}
  385. set pcs_stam(v){this._pcs_stam = Math.min(v, this.stammax)}
  386. get stammax(){return Math.max(1,5 * (2 * this.vitality + this.agility + this.strength) / 2)}
  387. get speed(){return (2 * (this.strength + this.agility) + this.vitality) / 5}
  388. genbsize = 12; // the set genetic bust size
  389. nbsize = 12; // starts at a set genetic bust size, but can be adjusted down if salo drops too low
  390. /*get wratio(){ // waist to hips ratio set in body_shape
  391. let wrtemp = ((2 * this.healthiness + this.muscularity + this.dexterity) / 4);
  392. let result = 85;
  393. if(wrtemp < 11)
  394. result = 85 + (11 - (this.healthiness + this.muscularity + this.dexterity) / 3);
  395. else if(wrtemp < 20)
  396. result = 85;
  397. else if(wrtemp < 35)
  398. result = 85 - (wrtemp - 20) / 3;
  399. else if(wrtemp < 55)
  400. result = 80 - (wrtemp - 35) / 4;
  401. else if(wrtemp < 80)
  402. result = 75 - (wrtemp - 55) / 5;
  403. else
  404. result = 70;
  405. if ((this.healthiness + this.muscularity + this.dexterity) / 3 > 100)
  406. result -= ((this.healthiness + this.muscularity) / 2 - this.dexterity) / 5;
  407. result = Math.max(result,65);
  408. return result;
  409. }
  410. get bratio(){ //band to waist ratio set in body_shape-->
  411. let brtemp = (2 * this.muscularity + this.healthiness + this.dexterity) / 4;
  412. if (brtemp < 10)
  413. return 105;
  414. else if (brtemp <= 23)
  415. return 106;
  416. else if (brtemp <= 80)
  417. return 106 + (brtemp - 23) / 3;
  418. else
  419. return 125;
  420. }
  421. get hratio(){ //hip to height ratio set in body_shape
  422. let hrtemp = (2 * this.dexterity + this.healthiness + this.muscularity) / 4;
  423. if (hrtemp < 35)
  424. return 60;
  425. else if (hrtemp < 45)
  426. return 59;
  427. else if (hrtemp < 60)
  428. return 58;
  429. else if (hrtemp < 80)
  430. return 57;
  431. else
  432. return 56;
  433. }*/
  434. _fat = 0; //the fat the character consumed recently
  435. get fat(){
  436. return this._fat;
  437. }
  438. set fat(v){
  439. console.log("FAT set to "+v);
  440. this._fat = v;
  441. }
  442. vofat = 0; //used as a place to put extra salo at extreme high values (i.e. really, really fat) set in body_shape
  443. _salo = 80; //the fat the character carries around with her
  444. get salo(){
  445. return this._salo;
  446. }
  447. set salo(v){
  448. this._salo = Math.min(v,250);
  449. console.log("SALO set to "+this._salo);
  450. }
  451. salocatnow = 0; //the current category of salo
  452. salolast = 0;
  453. salocatlast = 0; //the previous category of salo
  454. silicone = 0;
  455. silicone_butt = 0;
  456. butt_cheat = 0;
  457. magicf2b = 0; //magicf2b = set in body_shape for the fat moved to bust
  458. /*get pcs_hips(){return (this.pcs_hgt * this.hratio) / 100 + this.vhips;}
  459. get pcs_waist(){return (this.pcs_hips * this.wratio) / 100 + this.vofat;}
  460. get pcs_band(){return (this.pcs_waist * this.bratio) / 100 + this.vofat;}
  461. get pcs_bust(){return (this.pcs_waist * this.bratio) / 100 + this.nbsize + this.magicf2b + this.silicone;}
  462. get pcs_butt(){return (this.pcs_hips / 10) + this.silicone_butt + this.butt_cheat;}*/
  463. //get pcs_cupsize(){return (this.pcs_bust - this.pcs_band);}
  464. _cupsize = 15;
  465. get pcs_cupsize(){return this._cupsize;}
  466. set pcs_cupsize(v){this._cupsize = v;}
  467. get tits(){
  468. if(this.pcs_cupsize <= 5) return 0;
  469. if(this.pcs_cupsize <= 10) return 1;
  470. if(this.pcs_cupsize <= 15) return 2;
  471. if(this.pcs_cupsize <= 20) return 3;
  472. if(this.pcs_cupsize <= 25) return 4;
  473. if(this.pcs_cupsize <= 30) return 5;
  474. if(this.pcs_cupsize <= 35) return 6;
  475. if(this.pcs_cupsize <= 40) return 7;
  476. if(this.pcs_cupsize <= 45) return 8;
  477. if(this.pcs_cupsize <= 50) return 9;
  478. if(this.pcs_cupsize <= 55) return 10;
  479. return 11;
  480. }
  481. set tits(v){
  482. this.pcs_cupsize = Math.ceil((v+1)*5);
  483. }
  484. get titsize(){
  485. switch(this.tits){
  486. case 0: return 'AA cup';
  487. case 1: return 'A cup';
  488. case 2: return 'B cup';
  489. case 3: return 'C cup';
  490. case 4: return 'D cup';
  491. case 5: return 'E cup';
  492. case 6: return 'F cup';
  493. case 7: return 'G cup';
  494. case 8: return 'H cup';
  495. case 9: return 'I cup';
  496. case 10: return 'J cup';
  497. case 11: return 'K cup';
  498. default: return '??? cup';
  499. }
  500. }
  501. get pcs_weight(){
  502. /*//pcs_weight: Svetas weight in kg
  503. //Optimal Weight (170cm): 60kg, 60 salo
  504. //This is an approximation, so that a sveta with a different height has the same BMI for the same salo values.
  505. //The default height of 170 cm is used as a starting point. The bmi deviates by 0.06 for a deviation of 10cm and 0.4 for a deviation of 20cm.} -->
  506. return 30 + this.salo / 2 + (this.pcs_hgt - 170) * 7 / 10;
  507. //Do not remove (julzor)
  508. //This is the old more complex function, but it doesn''t work correctly.
  509. //This variable is actually only used ONCE (except for the BMI calculation), when stripping, to see if you fall off the pole from being too heavy. I am reluctant to touch it, since the math is not explained. Maybe later.
  510. //Weight = (height in cm x bottom of average weight in kilos for a 170cm female / 170cm + (hip modifier + 2 x overrun fat)/4 + cup size} -->
  511. //result = ($pcs_hgt * 62 / 170) + (vhips + (vofat * 2)) / 4 + tits-->*/
  512. return Math.pow(this.pcs_hgt / 100,2) * this.bmi;
  513. }
  514. _bmi = 20;
  515. get bmi(){
  516. /*let bmi_all_decimal_places = 10000 * (30 + this.salo / 2 + (this.pcs_hgt - 170) * 7 / 10) / (this.pcs_hgt * this.pcs_hgt);
  517. let bmi_rounded_to_one_decimal = Math.round(bmi_all_decimal_places * 10) / 10;
  518. return bmi_rounded_to_one_decimal;*/
  519. return this._bmi;
  520. }
  521. set bmi(v){
  522. this._bmi = v;
  523. }
  524. get bmi_is_attractive(){
  525. let bmi = this.bmi;
  526. return (bmi >= bmi_attractive_range[0] && bmi <= bmi_attractive_range[1]);
  527. }
  528. get bmi_desc(){
  529. let bmi = this.bmi;
  530. if(bmi < 16)
  531. return 'You are severely underweight.';
  532. else if(bmi < 19)
  533. return 'You are underweight';
  534. else if(bmi < 25)
  535. return 'You have a normal, healthy weight.';
  536. else if(bmi < 30)
  537. return 'You are overweight.';
  538. else if(bmi < 35)
  539. return 'You are moderately obese.';
  540. else if(bmi < 40)
  541. return 'You are severely obese.';
  542. else if(bmi < 45)
  543. return 'You are very severely obese.';
  544. else
  545. return 'You are morbidly obese.';
  546. }
  547. pcs_hgt = 170; // Height in cm
  548. pcs_hairlng = 300; // Hair length in mm
  549. get hairLength(){return this.pcs_hairlng;}
  550. set hairLength(v){this.pcs_hairlng = v;}
  551. hairColor = 0;
  552. hairColorNatural = 0;
  553. hairDyeFade = 0;
  554. hairDye(newColor,fadeDuration=undefined){
  555. this.appearanceHistoryPush('hairColor',newColor);
  556. this.hairColor = newColor;
  557. this.hairDyeFade = fadeDuration ?? 30;
  558. }
  559. pubesLength = 0; // Pubes hair length in mm
  560. pubestyle = 0; // The style the pubes get shaved into. 0 is not shaving.
  561. pubesState = 0; // 0: Default, 1: lasered
  562. get pubesGrowth(){
  563. switch(this.pubesState){
  564. case 0: return 0.5;
  565. case 1: return 0;
  566. }
  567. }
  568. get pubesAreLasered(){return (this.pubesState == 1);}
  569. set pubesAreLasered(v){
  570. if(v){
  571. this.pubesLength = 0;
  572. this.pubesState = 1;
  573. }else{
  574. this.pubesState = 0;
  575. }
  576. }
  577. preg = 0; // 1: is pregnant, 0: is not pregnant
  578. knowpreg = 0; // 1: Is pregnant and knows it, 0: doesn't know is pregnant but could be
  579. 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)
  580. PregChem = 0; // Size of the pregnancy
  581. clit_size = 0;
  582. get isPregnancyAware(){ // Either knows she is pregnant, correctly assumes to be pregnant or is big enough to no longer be in denial.
  583. if (this.knowpreg == 1) return 1;
  584. if (this.preg == 1 && this.thinkpreg == 1) return 1;
  585. if (this.preg == 1 && this.PregChem > 3600) return 1;
  586. return 0;
  587. }
  588. get bodset(){ //body image and descriptor control variable, used to indicate which image and descriptor set is in use
  589. /*if(this.isPregnancyAware == 1) return 3;
  590. if (this.muscularity >= 70) return 2;
  591. if (this.muscularity <= 40) return 0;*/
  592. return 1;
  593. }
  594. get body(){
  595. /*let bodimgsets = State.variables.bodimgsets;
  596. if(this.isPregnancyAware){
  597. if(this.PregChem > 6216)
  598. return bodimgsets[(this.bodset * 10) + 8];
  599. if(this.PregChem < 2688)
  600. return bodimgsets[this.bodset * 10];
  601. return bodimgsets[Math.floor(this.bodset * 10 + ((this.PregChem - 2184) / 504))];
  602. }
  603. if(this.salocatnow <= 7)
  604. return bodimgsets[((this.bodset * 10) + this.salocatnow)];
  605. return bodimgsets[(this.bodset * 10) + 7];*/
  606. return null;
  607. }
  608. /*pcs_teeth = 0; //-1: perfectly white
  609. teeth = {
  610. brushed: 0,
  611. caffe_or_tea: 0,
  612. degradation:0 ,
  613. smoked: 0
  614. }
  615. get teethQuality(){return this.pcs_teeth}*/
  616. _teeth = 1;
  617. _teeethMissing = [];
  618. get teeth(){return this._teeth;}
  619. set teeth(v){this._teeth=Math.max(0,v);}
  620. get teethQuality(){
  621. let t = this.teeth;
  622. if(t < 100)
  623. return 0;
  624. else if(t < 1000)
  625. return 1;
  626. return 2;
  627. }
  628. set teethQuality(v){
  629. switch(v){
  630. case 0: this.teeth = 50; break;
  631. case 1: this.teeth = 500; break;
  632. case 2: this.teeth = 1500; break;
  633. }
  634. }
  635. get teethMissingCount(){
  636. return this._teeethMissing.length;
  637. }
  638. //pcs_breath = 0;
  639. _pcs_lipbalm = 0;
  640. get pcs_lipbalm(){return this._pcs_lipbalm;}
  641. set pcs_lipbalm(v){this._pcs_lipbalm = Math.max(0,v);}
  642. _leghair = 0;
  643. /**
  644. * The length of the hair on the legs in mm.
  645. * @date 7/23/2023 - 9:25:25 AM
  646. *
  647. * @type {number}
  648. */
  649. get legHair(){return this._leghair;}
  650. set legHair(v){this._leghair = v;}
  651. legHairState = 0; //0: default, 1: lasered
  652. get legHairGrowth(){
  653. switch(this.legHairState){
  654. case 0: return (this.age < 18 ? 0.14 : 0.21);
  655. case 1: return 0;
  656. }
  657. }
  658. get legHairIsLasered(){return (this.legHairState == 1);}
  659. set legHairIsLasered(v){
  660. if(v){
  661. this.legHair = 0;
  662. this.legHairState = 1;
  663. }else{
  664. this.legHairState = 0;
  665. }
  666. }
  667. get legHairVisibility(){
  668. if(this.legHair <= 0)
  669. return 0;
  670. else if(this.legHair <= 0.5) // ~3 Days
  671. return 1;
  672. else if(this.legHair <= 1.5) // ~1 Week
  673. return 2;
  674. else if(this.legHair <= 6) // ~1 Month
  675. return 3;
  676. return 4;
  677. }
  678. set legHairVisibility(v){
  679. switch (v) {
  680. case 0: this._leghair = 0; return;
  681. case 1: this._leghair = 0.2; return;
  682. case 2: this._leghair = 1; return;
  683. case 3: this._leghair = 4; return;
  684. default:this._leghair = 20; return;
  685. }
  686. }
  687. pcs_hairbsh = 0;
  688. deodorant_on = 0;
  689. deodorant_time = 0;
  690. moisturizerDailyCount = 0;
  691. skinDailyGain = 0;
  692. skinDailyPenalty = 0;
  693. _pcs_skin = 500;
  694. get pcs_skin(){
  695. return this._pcs_skin;
  696. }
  697. set pcs_skin(v){
  698. this._pcs_skin = Math.clamp(v,0,1000);
  699. }
  700. get skinAppearance(){
  701. return Math.round(this.pcs_skin / 200 - 2.5);
  702. }
  703. set skinAppearance(v){
  704. switch(v){
  705. case -3: this.pcs_skin = 0; return;
  706. case -2: this.pcs_skin = 100; return;
  707. case -1: this.pcs_skin = 300; return;
  708. case 0: this.pcs_skin = 500; return;
  709. case 1: this.pcs_skin = 700; return;
  710. case 2: this.pcs_skin = 900; return;
  711. case 3: this.pcs_skin = 1000; return;
  712. }
  713. }
  714. tan = 0;
  715. bodyDailyUpdate(){
  716. this.dailyEnergyUpdate();
  717. /*if(this.muscularity > this.strength)
  718. this.muscularity -= 1;
  719. else if(this.muscularity < this.strength)
  720. this.muscularity += 1;
  721. if(this.healthiness > this.vitality)
  722. this.healthiness -= 1;
  723. else if(this.healthiness < this.vitality)
  724. this.healthiness += 1;
  725. if(this.dexterity > this.agility)
  726. this.dexterity -= 1;
  727. else if(this.dexterity < this.agility)
  728. this.dexterity += 1;*/
  729. /*if(this.fat > (17 + this.healthiness / 25)){
  730. this.salo += 1;
  731. this.fat = 0;
  732. }
  733. else if(this.fat < (-2 - (this.healthiness / 10))){
  734. this.salo -= 1;
  735. this.fat = 0;
  736. }
  737. this.fat = this.fat / 4;
  738. this.bodySaloCalc();*/
  739. this.hairLength += 1;
  740. /*if(this.pcs_lashes > 2){
  741. if(this.lashextensionstyle >= 1){
  742. this.lashextensionduration -= 1;
  743. if(this.lashextensionduration >= 1 && this.lashextensionduration <= 4)
  744. 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.");
  745. if(this.lashextensionduration <= 0){
  746. 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.");
  747. this.pcs_lashes = this.pcs_naturallashes;
  748. this.lashextensionstyle = 0;
  749. this.lashextensionduration = 0;
  750. this.lashextensionnew = 0;
  751. }
  752. }
  753. if(this.false_lashes > 0){
  754. this.false_lashes -= 1;
  755. if (this.false_lashes == 0){
  756. message("bad","Your false lashes came off in the night; there's no recovering them now.");
  757. this.pcs_lashes = this.pcs_naturallashes;
  758. }
  759. 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.");
  760. }
  761. }*/
  762. // Hair colour change
  763. //TODO: Dasdas
  764. /*if(this.pcs_haircol != this.nathcol){
  765. this.hairDyeFade = Math.max(this.hairDyeFade - 1 , 0);
  766. }*/
  767. // Leg and pubes hair growth
  768. this.legHair += this.legHairGrowth;
  769. //Pubic hair growth at 1/2 per night
  770. this.pubesLength += this.pubesGrowth;
  771. this.teeth += 1;
  772. console.info("Deactivated: degradation of pube hair coloring");
  773. /*<!-- !!pubic hair colouring-->
  774. <!-- !! pcs_pubecol = natural colour-->
  775. <!-- !! pcs_pubecol_num[1] = flag for saveupdate-->
  776. <!-- !! pcs_pubecol_num[2] = actual colour-->
  777. <!-- !! pcs_pubecol_num[3] = countdown timer for dye-->
  778. <<if $pcs_pubecol_num[2] != $pcs_pubecol>>
  779. <<setinit $pcs_pubecol_num[3] -= 1>>
  780. <<if $pcs_pubecol_num[3] < 0>>
  781. <<setinit $pcs_pubecol_num[3] = 0>>
  782. <</if>>
  783. <<if $pcs_pubecol_num[3] == 0>>
  784. <<setinit $pcs_pubecol_num[2] = $pcs_pubecol>>
  785. <</if>>
  786. <</if>>
  787. <<if getvar("$pubesLength") < 2>>
  788. <<setinit $pcs_pubecol_num[2] = $pcs_pubecol>>
  789. <</if>>*/
  790. console.info("Deactivated: degradation of hair scrunches");
  791. /*
  792. <<if getvar("$hscrunch") > 0>>
  793. <<set $hscrunchrand = rand(1, 100)>>
  794. <<if getvar("$hscrunchrand") <= 8>>
  795. <<setn $hscrunch -= 1>>
  796. <</if>>
  797. <</if>>*/
  798. /*if(this.pcs_skin <= 300)
  799. this.pcs_skin += Math.min(this.skinDailyGain * 2, 20) - this.skinDailyPenalty - 1;
  800. else if(this.$pcs_skin <= 600)
  801. this.pcs_skin += Math.min(this.skinDailyGain, 10) - this.skinDailyPenalty - 1;
  802. else if(this.pcs_skin <= 800)
  803. this.pcs_skin += Math.min(this.skinDailyGain / 2, 5) - this.skinDailyPenalty - 1;
  804. else if(this.pcs_skin <= 900)
  805. this.pcs_skin += Math.min(this.skinDailyGain / 3, 3) - this.skinDailyPenalty - 1;
  806. else if(this.pcs_skin <= 1000)
  807. this.pcs_skin += Math.min(this.skinDailyGain / 5, 2) - this.skinDailyPenalty - 1;
  808. if(this.pcs_teeth < 0){
  809. // Daly degradation of perfect white teeth
  810. let tempteeth = 1;
  811. if(this.teeth['caffe_or_tea'] > 8)
  812. tempteeth += 1;
  813. if(this.teeth['smoked'] > 1)
  814. tempteeth += 1;
  815. tempteeth -= Math.min(3, this.teeth['brushed']);
  816. this.teeth['degradation'] += Math.max(tempteeth, 0);
  817. this.teeth['brushed'] = 0;
  818. this.teeth['smoked'] = 0;
  819. this.teeth['caffe_or_tea'] = 0;
  820. if(this.teeth['degradation'] > 60)
  821. {
  822. // After a certain time of not taking care of your teeth you will loose you perfect whit smile.
  823. this.teeth['degradation'] = 0;
  824. this.pcs_teeth = 0;
  825. }
  826. this.teeth['degradation'] = Math.max(this.teeth['degradation'],0);
  827. }
  828. this.moisturizerDailyCount = 0;
  829. this.skinDailyGain = 0;
  830. this.skinDailyPenalty = 0;*/
  831. }
  832. get agility(){ return this.skillLevel('agility');} set agility(v){ this.skillSetLevel('agility',v);}
  833. get strength(){return this.skillLevel('strength');} set strength(v){this.skillSetLevel('strength',v);}
  834. get vitality(){return this.skillLevel('vitality');} set vitality(v){this.skillSetLevel('vitality',v);}
  835. // ---- Appearance ------
  836. get pcs_makeup(){
  837. /*if(this.cosmetic_tattoo > 0)
  838. return this.cosmetic_tattoo + 1;
  839. return this._pcs_makeup;*/
  840. console.warn('Usage of get $pc.pcs_makeup is deprecated');
  841. return Math.floor((this.makeupAmount + this.makeupQuality)/2);
  842. }
  843. set pcs_makeup(v){
  844. console.warn('Usage of set $pc.pcs_makeup is deprecated');
  845. //this._pcs_makeup = v;
  846. this.makeupAmount = v;
  847. this.makeupQuality= v;
  848. }
  849. _makeupAmount = 0;
  850. get makeupAmount(){
  851. return this._makeupAmount;
  852. }
  853. set makeupAmount(v){
  854. this._makeupAmount = v;
  855. }
  856. _makeupQuality = 0;
  857. get makeupQuality(){
  858. return this._makeupQuality;
  859. }
  860. set makeupQuality(v){
  861. this._makeupQuality = v;
  862. }
  863. cosmetic_tattoo = 0;
  864. get appearance(){
  865. return setup.func('Appearance','appearance');
  866. }
  867. get hotcat(){
  868. return this.appearance.hotcat.temp;
  869. }
  870. // ----- Toys -----
  871. analplugin = 0;
  872. vibratorin = 0;
  873. // ----- Skills -----
  874. get intelligence(){
  875. return this.skillLevel('intelligence');
  876. }
  877. _skills:{[key: string]:any} = {
  878. }
  879. get skillsAll(){
  880. let result = {};
  881. for(const [skillId, skillData] of Object.entries(this._skills)){
  882. result[skillId] = Object.assign({},skillData,{
  883. level: this.skillLevel(skillId)
  884. });
  885. }
  886. return result;
  887. }
  888. get skillExperienceGainMult(){
  889. let lackOfSleep = 0; //TODO: get the correct value
  890. if(lackOfSleep >= 20)
  891. return 0.25;
  892. else if(lackOfSleep >= 10)
  893. return 0.5;
  894. else if(lackOfSleep >= 5)
  895. return 0.75;
  896. else if(lackOfSleep >= 2)
  897. return 0.9;
  898. return 1;
  899. }
  900. #skillInit(skillId){
  901. if(!(skillId in this._skills))
  902. this._skills[skillId] = {
  903. lastUsed: 0,
  904. experience: 0,
  905. experienceHistory:[],
  906. ceil: 0,
  907. floor: 0
  908. }
  909. }
  910. #skill_dayly(){
  911. let today = State.variables.time.daystart;
  912. for (const [skillId, skillData] of Object.entries(this._skills)){
  913. this._skills[skillId].experienceHistory.push(skillData.experience);
  914. if(this._skills[skillId].experienceHistory.length > 30)
  915. this._skills[skillId].experienceHistory.shift();
  916. this._skills[skillId].ceil = Math.max(skillData.experience, skillData.floor);
  917. this._skills[skillId].floor = Math.max(Math.ceil(skillData.experience/2), skillData.floor);
  918. let daysNotUsed = today - this._skills[skillId].lastUsed;
  919. let newexperience = skillData.experience;
  920. if(daysNotUsed > 14){
  921. let currentLevel = this.#skill_exp2lvl(skillId,skillData.experience);
  922. let experienceForCurrentLevel = this.#skill_lvl2exp(skillId,currentLevel);
  923. let experienceForPreviousLevel= this.#skill_lvl2exp(skillId,currentLevel-1);
  924. let experienceDifferenceToPreviousLevel = experienceForCurrentLevel - experienceForPreviousLevel;
  925. let experienceLoss = Math.ceil(experienceDifferenceToPreviousLevel / 4);
  926. newexperience -= experienceLoss //* this.timeFactor;
  927. }
  928. this._skills[skillId].experience = Math.max(newexperience,this._skills[skillId].floor);
  929. }
  930. }
  931. #skill_exp2lvl(skillId,exp){
  932. return Math.floor(Math.pow(exp,1/2));
  933. }
  934. #skill_lvl2exp(skillId,lvl){
  935. return Math.pow(lvl,2);
  936. }
  937. skillExperienceGain(skillId,inc){
  938. if(inc == 0)
  939. return;
  940. this.#skillInit(skillId);
  941. this._skills[skillId].experience += inc;
  942. console.log("Skill Experience Gain:",skillId,inc,this._skills[skillId].experience );
  943. }
  944. skillsExperienceGain(skillObj:{[key: string]:number},factor=1){
  945. for(const [skillId,inc] of Object.entries(skillObj)){
  946. this.skillExperienceGain(skillId,inc*factor)
  947. }
  948. }
  949. skillExperienceHistory(skillId){
  950. this.#skillInit(skillId);
  951. let eh = clone(this._skills[skillId].experienceHistory);
  952. eh.push(this._skills[skillId].experience);
  953. return eh;
  954. }
  955. skillExperienceHistoryDayExpTuple(skillId){
  956. let today = State.variables.time.daystart;
  957. let eh = this.skillExperienceHistory(skillId);
  958. let result = [];
  959. let todayIndex = eh.length - 1;
  960. for(let i = 0; i < eh.length; i++){
  961. let day = -todayIndex + i + today;
  962. let tuple = [day,eh[i]];
  963. result.push(tuple);
  964. }
  965. return result;
  966. }
  967. skillFloor(skillId){
  968. this.#skillInit(skillId);
  969. return this._skills[skillId].floor;
  970. }
  971. skillLevel(skillId){
  972. this.#skillInit(skillId);
  973. return (
  974. this.#skill_exp2lvl(skillId,this._skills[skillId].experience) +
  975. this.#skillLevelBonusOfActiveEffect(skillId)
  976. );
  977. }
  978. #skillLevelBonusOfActiveEffect(skillId){
  979. let result = 0;
  980. for(const effect of Object.values(this.activeEffects))
  981. result += effect.skills?.[skillId] ?? 0;
  982. return result;
  983. }
  984. //Is usually used for initialization, therefore we'll add one history entry if it doesn't exist
  985. skillSetLevel(skillId,lvl){
  986. this.#skillInit(skillId);
  987. let exp = this.#skill_lvl2exp(skillId,lvl);
  988. this._skills[skillId].experience = exp;
  989. console.log("Setting skill level (Skill, Lvl, Exp)",skillId,lvl,exp);
  990. }
  991. // ----- Fetishes -----
  992. _fetishes = {};
  993. fetish(fetishId){
  994. return this._fetishes[fetishId] ?? 1;
  995. }
  996. fetishSet(fetishId,v){
  997. this._fetishes[fetishId] = v;
  998. }
  999. fetishes(fetishIds){
  1000. if(typeof fetishIds == "string")
  1001. return this.fetishes([fetishIds]);
  1002. let result = 1;
  1003. let indifferentCount = 0;
  1004. let arouseCount = 0;
  1005. for(const fetishId of fetishIds){
  1006. const arousalOfFetish = this.fetish(fetishId);
  1007. if(arousalOfFetish < 0)
  1008. return arousalOfFetish;
  1009. if(arousalOfFetish == 0)
  1010. indifferentCount += 1;
  1011. else if(arousalOfFetish > 1){
  1012. result += arousalOfFetish - 1;
  1013. arouseCount += 1;
  1014. }
  1015. }
  1016. if(indifferentCount > 0 && arouseCount == 0)
  1017. return 0;
  1018. return result;
  1019. }
  1020. // ----- Sex-Stats -----
  1021. /*
  1022. Each Sex-Encounter has the following format:
  1023. {
  1024. npc: The ID of the npc Sveta had sex with. If she has sex with more than one character (e.g. threesome) an encounter has to be created for each of them.
  1025. time:The time it happened (Date-Object (UTC-time))
  1026. aware: Is Sveta aware of the sex-act? False in case of hypnosis, blackouts, etc.
  1027. civ: Cum in vagina
  1028. civa:Cum in vagina aware
  1029. tags: Array of tags
  1030. }
  1031. */
  1032. _sexEncounters = [];
  1033. sexEncounterAdd(time,events){
  1034. this._sexEncounters.push({
  1035. time: time,
  1036. events: events
  1037. });
  1038. }
  1039. /*sexEncountersFiltered(maxage=Infinity,aware=undefined,civ=undefined,civa=undefined){
  1040. let result = [];
  1041. for (const sexEncounter of this._sexEncounters) {
  1042. let ageOfEncounterInDays = Math.floor(State.variables.time.secondsSinceDate(sexEncounter.time) / 86400);
  1043. if(ageOfEncounterInDays > maxage)
  1044. continue;
  1045. if(typeof aware === "boolean" && aware !== sexEncounter.aware) continue;
  1046. if(typeof civ === "boolean" && civ !== sexEncounter.civ) continue;
  1047. if(typeof civa === "boolean" && civa !== sexEncounter.civa) continue;
  1048. result.push(sexEncounter);
  1049. }
  1050. return result;
  1051. }*/
  1052. sexEncountersFilteredByNpc(maxage=Infinity,aware=undefined){
  1053. let result = {};
  1054. for(const encounter of this._sexEncounters){
  1055. for(const event of encounter.events){
  1056. if(typeof aware === "boolean"){
  1057. if((aware && event.unaware) || (!aware && !event.unaware))
  1058. continue;
  1059. }
  1060. result[event.npc] ??= 0;
  1061. result[event.npc] += 1;
  1062. }
  1063. }
  1064. return result;
  1065. }
  1066. //Every Stat has two values: the number you are aware of and the number you are unaware of
  1067. /** @access private */
  1068. _sexStats = {
  1069. vaginal: [0,0],
  1070. vaginal_fist:[0,0],
  1071. vaginal_dildo:[0,0],
  1072. vaginal_strap:[0,0]
  1073. }
  1074. /**
  1075. * Returns a sexstat or, it it isn't set, def. Returns undefined in case of an error.
  1076. * @example
  1077. * sexStat('vaginal_total','both',0);
  1078. * @date 5/29/2023 - 9:32:32 AM
  1079. *
  1080. * @param {string} key
  1081. * @param {string} [awareness='both'] - Either 'aware' or 'unaware' or 'both' (the sum of the former)
  1082. * @param {number} [def=0]
  1083. * @returns {number|undefined}
  1084. */
  1085. sexStat(key,awareness='both',def=0){
  1086. switch(key){
  1087. case 'vaginal_total':
  1088. return 0;//return (this.stat('vaginal',awareness) + this.stat('vaginal_fist',awareness) + this.stat('vaginal_dildo',awareness) + this.stat('vaginal_strap',awareness));
  1089. }
  1090. if(!(key in this._sexStats))
  1091. return def;
  1092. switch(awareness){
  1093. case 'both':
  1094. return this._sexStats[key][0] + this._sexStats[key][1];
  1095. case 'aware':
  1096. return this._sexStats[key][0];
  1097. case 'unaware':
  1098. return this._sexStats[key][1];
  1099. default:
  1100. console.error('Invalid argument for PlayerCharacter.sexStat: awareness',awareness);
  1101. }
  1102. return undefined;
  1103. }
  1104. /**
  1105. * Increases a sex-stat by inc.
  1106. * @example
  1107. * sexStatInc('vaginal','aware',1)
  1108. * @date 5/29/2023 - 9:24:04 AM
  1109. *
  1110. * @param {string} key
  1111. * @param {string} awareness - Either 'aware' or 'unaware'
  1112. * @param {number} [inc=1]
  1113. */
  1114. sexStatInc(key,awareness,inc=1){
  1115. let currentValue = this.sexStat(key,awareness);
  1116. this.sexStatSet(key,awareness,currentValue+inc);
  1117. }
  1118. /**
  1119. * Sets a sex-stat to v.
  1120. * @example
  1121. * sexStatSet('vaginal','aware',1)
  1122. * @date 5/29/2023 - 9:30:58 AM
  1123. *
  1124. * @param {string} key
  1125. * @param {string} awareness - Either 'aware' or 'unaware'
  1126. * @param {number} v - The new value.
  1127. * @returns {number|undefined} The new value or, if there was an error, undefined.
  1128. */
  1129. sexStatSet(key,awareness,v){
  1130. this._sexStats[key] ??= [0,0];
  1131. switch(awareness){
  1132. case 'aware':
  1133. this._sexStats[key][0] = v;
  1134. return v;
  1135. case 'unaware':
  1136. this._sexStats[key][1] = v;
  1137. return v;
  1138. default:
  1139. console.error('Invalid argument for PlayerCharacter.sexStatSet: awareness',awareness);
  1140. return undefined;
  1141. }
  1142. }
  1143. get isVirgin(){
  1144. return (this.sexStat('vaginal','both') == 0) ? true : false;
  1145. }
  1146. get thinksIsVirgin(){
  1147. return (this.sexStat('vaginal','aware') == 0) ? true : false;
  1148. }
  1149. _traits = {
  1150. nerd_status: 0
  1151. }
  1152. trait(key,def=0){
  1153. if(key in this._traits)
  1154. return this._traits[key];
  1155. return def;
  1156. }
  1157. traitDec(key,v){
  1158. this._traits[key] -= v;
  1159. }
  1160. traitInc(key,v){
  1161. this._traits[key] += v;
  1162. }
  1163. traitSet(key,v){
  1164. this._traits[key] = v;
  1165. }
  1166. // ----- Arousal -----
  1167. /*
  1168. For checking arousal and when applicable triggering orgasms.
  1169. action:
  1170. All acts are from Sveta''s perspective and in cases of both giving and receiving, receiving should be used.
  1171. It can be when receiving any of the following
  1172. 'clit_finger' - Clit being stimulated directly by a finger
  1173. 'clit_vibe' - Clit being stimulated directly by a vibrator (set low, use negative time and double/triple up for more power)
  1174. 'porn' - viewing pornographic material
  1175. 'voyeur_sex' - watching, usually as in spying on, other people have sex
  1176. 'voyeur' - watching, usually as in spying on, erotic acts of others
  1177. 'erotic' - being aroused by eroticism
  1178. 'erotic_nudity' - being aroused by nudity of others
  1179. 'trib' - rubbing pussy against another pussy
  1180. 'massage' - rubbing your body, back, feet, etc. with their hands/arms
  1181. 'cuni' - stimulation of your pussy by someones toungue
  1182. 'rimming' - stimulation of your anus by someones toungue
  1183. 'vaginal' 'vaginal_finger' 'vaginal_fist' 'vaginal_dildo' 'vaginal_strap' 'vaginal_vibe' - stimulation of your vagina with a penis and various others
  1184. 'self_fisting' - fisting your own vagina
  1185. 'anal' 'anal_finger' 'anal_fist' 'anal_dildo' 'anal_strap' 'anal_vibe' - stimulation of your anus with a penis and various others
  1186. 'self_fisting_anal' - fisting your own anus
  1187. 'kiss' - snogging, tonsil tennis, lip locking, etc.
  1188. 'BDSM' - receiving candle wax, flogging, leash play, bondage etc
  1189. 'pee' - being peed upon
  1190. also when giving any of the following:
  1191. 'flashlite' - flashing underwear
  1192. 'flash' - flashing naked breasts/arse/vagina
  1193. 'massage_give' - rubbing their body, back, feet, etc. with your hands/arms
  1194. 'cuni_give' - stimulating someones pussy with your toungue
  1195. 'rimming_give' - stimulating someones anus with your toungue
  1196. 'vaginal_finger_give' 'vaginal_fist_give' 'vaginal_dildo_give' 'vaginal_strap_give' 'vaginal_vibe_give' - stimulating someones vagina in various ways
  1197. 'clit_finger_give' - stimulating someones clit
  1198. 'anal_finger_give' 'anal_fist_give' 'anal_dildo_give' 'anal_vibe_give' 'anal_strap_give' - stimulating someones anus in various ways
  1199. 'hj' - jerking a guy off with your hand
  1200. 'bj' - sucking a guy off
  1201. 'dildo_suck' - simulating a bj on a dildo/strapon
  1202. 'titjob' - using boobs to jerk off a guy
  1203. 'footjob' - using feet to jerk off a guy
  1204. 'BDSM_give' - giving candle wax, flogging, leash play, bondage etc
  1205. 'pee_give' - peeing on somone
  1206. finally
  1207. 'foreplay' - receiving other stuff
  1208. 'foreplay_give' - giving other stuff
  1209. time
  1210. 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.
  1211. npcId
  1212. specify the involved npc
  1213. flags
  1214. Are optional but can be themes involved in the act and can be any of the following:
  1215. 'maso' 'bound' 'beast' 'exhibitionism' 'rough' 'prostitution' 'dom' 'sub' 'incest'
  1216. 'feet' 'lesbian' 'group' 'gangbang' 'humiliation' 'deepthroat' 'unknown' 'gloryhole' 'rape' 'futa' 'masturbate'
  1217. 'unaware'
  1218. */
  1219. _analCapacity = 0.5;
  1220. get analCapacity(){
  1221. return this._analCapacity;
  1222. }
  1223. _vaginalCapacity = 1;
  1224. get vaginalCapacity(){
  1225. return this._vaginalCapacity;
  1226. }
  1227. get vaginalLubrication(){
  1228. return this.horny / 100;
  1229. }
  1230. analCapacityAdaptTo(v){
  1231. if(v <= this._analCapacity)
  1232. return;
  1233. const sizeDifference = v - this._analCapacity;
  1234. if(sizeDifference <= 1)
  1235. this._analCapacity = Math.min(v, this._analCapacity + 0.1);
  1236. else
  1237. this._analCapacity = Math.min(v, this._analCapacity + sizeDifference / 10);
  1238. }
  1239. vaginalCapacityAdaptTo(v){
  1240. if(v <= this._vaginalCapacity)
  1241. return;
  1242. const sizeDifference = v - this._vaginalCapacity;
  1243. if(sizeDifference <= 1)
  1244. this._vaginalCapacity = Math.min(v, this._vaginalCapacity + 0.1);
  1245. else
  1246. this._vaginalCapacity = Math.min(v, this._vaginalCapacity + sizeDifference / 10);
  1247. }
  1248. current_arousal_scene_actions = [];
  1249. arouse(action,time,npcId=undefined,flags=undefined){
  1250. console.warn("PlayerCharacter.arouse() is deprecated. Use <<arouse>> instead",action,time,npcId,flags);
  1251. }
  1252. //TODO
  1253. arousalEnd(){
  1254. console.warn("PlayerCharacter.arousalEnd() is deprecated");
  1255. }
  1256. // ----- Cum -----
  1257. /*
  1258. Cum-Locations
  1259. 0 = 'In your Vagina'
  1260. 1 = 'On your labia'
  1261. 2 = 'On your panties over your vagina'
  1262. 3 = 'In your anus'
  1263. 4 = 'On your butt'
  1264. 5 = 'On your panties over your butt'
  1265. 6 = 'On your clothes in your groin area'
  1266. 7 = 'On your clothes'
  1267. 8 = 'On your back'
  1268. 9 = 'On your legs'
  1269. 10 = 'On your arms'
  1270. 11 = 'On your face'
  1271. 12 = 'Inside your mouth'
  1272. 13 = 'On your hands'
  1273. 14 = 'On your stomach'
  1274. 15 = 'On your breasts'
  1275. 16 = 'In your hair'
  1276. 17 = 'In a condom in your vagina
  1277. */
  1278. _cum:{ [key: string]: Array<any>} = {};
  1279. get cums(){
  1280. this._cumPurgeExpired();
  1281. return this._cum;
  1282. }
  1283. /**
  1284. * Removes cum-data on the inside of the PCs body.
  1285. * @date 10/8/2023 - 2:16:00 PM
  1286. */
  1287. _cumPurgeExpired(){
  1288. const bodyparts = setup.getBodyparts();
  1289. let updatedCumData = {};
  1290. const maxAgeInMinutes = 360; // 6 Hours
  1291. let oldestPossibleDateTime = State.variables.time.now;
  1292. oldestPossibleDateTime.setUTCMinutes(oldestPossibleDateTime.getUTCMinutes() - maxAgeInMinutes);
  1293. for (const [bodypart, cumData] of Object.entries(this._cum)) {
  1294. if(bodyparts[bodypart].inside)
  1295. updatedCumData[bodypart] = cumData.filter(cum => cum.time.getTime() >= oldestPossibleDateTime.getTime());
  1296. else
  1297. updatedCumData[bodypart] = cumData;
  1298. }
  1299. this._cum = updatedCumData;
  1300. }
  1301. cum(npcId,bodypartId){
  1302. let bodypartData = setup.getBodypart(bodypartId);
  1303. if(!bodypartData || bodypartData.cumDisabled)
  1304. console.warn('The following bodypart is not enabled for $pc.cum():',bodypartId);
  1305. bodypartId = bodypartData.id;
  1306. const time = State.variables.time;
  1307. const now = time.now;
  1308. const cumData = {
  1309. npc: npcId,
  1310. time: now
  1311. };
  1312. this._cum[bodypartId] ??= [];
  1313. this._cum[bodypartId].push(cumData);
  1314. }
  1315. cumCleanAll(){
  1316. this._cum={};
  1317. }
  1318. cumCleanByActivity(activityId){
  1319. const bodyparts = Object.entries(setup.getBodyparts());
  1320. const bodypartIdsToClean = bodyparts.filter(([id,bodypart]) => bodypart['clean'].includes(activityId)).map(([id,bodypart]) => id);
  1321. for(let bodypartIdToClean of bodypartIdsToClean)
  1322. this._cum[bodypartIdToClean] = [];
  1323. }
  1324. get cumBodypartIds(){
  1325. return Object.entries(this.cums).filter(([bodypartId,cumArray]) => cumArray.length > 0).map(([bodypartId,cumArray]) => bodypartId);
  1326. }
  1327. get cumVisibleBodypartIds(){
  1328. return this.cumBodypartIds.filter(bodypartId=>!this.bodyPartCovered(bodypartId));
  1329. }
  1330. /*_cumCleanByCumLocation(bodyLocation){
  1331. this._cum[bodyLocation] = [];
  1332. }
  1333. cumAtLocation(location){
  1334. if(this._cum[location].length > 0)
  1335. return 1;
  1336. return 0;
  1337. }*/
  1338. /*cumClean(location){
  1339. if(typeof location != "string"){
  1340. switch(location){
  1341. case 1: location = 'full external'; break;
  1342. case 2: location = 'mouth'; break;
  1343. case 3: location = 'interal'; break;
  1344. case 4: location = 'external'; break;
  1345. case 5: location = 'hair'; break;
  1346. case 6: location = 'clothes'; break;
  1347. case 7: location = 'enema'; break;
  1348. case 8: location = 'blank'; break;
  1349. case 9: location = 'hands'; break;
  1350. case 10: location = 'full external'; break;
  1351. case 11: location = 'face'; break;
  1352. case 12: location = 'face and hair'; break;
  1353. case 13: location = 'head'; break;
  1354. case 14: location = 'belly'; break;
  1355. case 15: location = 'wipe ass and pussy'; break;
  1356. case 16: location = 'breasts'; break;
  1357. case 17: location = 'arms and legs'; break;
  1358. case 18: location = 'full except internal'; break;
  1359. case 19: location = ''; break;
  1360. case 20: location = 'everything'; break;
  1361. default:
  1362. console.error("Unsupported value in cumClean:",location);
  1363. return;
  1364. }
  1365. }
  1366. switch(location){
  1367. case 'full external':
  1368. this._cumCleanByCumLocation(1);
  1369. this._cumCleanByCumLocation(2);
  1370. this._cumCleanByCumLocation(4);
  1371. this._cumCleanByCumLocation(5);
  1372. this._cumCleanByCumLocation(6);
  1373. this._cumCleanByCumLocation(7);
  1374. this._cumCleanByCumLocation(8);
  1375. this._cumCleanByCumLocation(9);
  1376. this._cumCleanByCumLocation(10);
  1377. this._cumCleanByCumLocation(11);
  1378. this._cumCleanByCumLocation(12);
  1379. this._cumCleanByCumLocation(13);
  1380. this._cumCleanByCumLocation(14);
  1381. this._cumCleanByCumLocation(15);
  1382. this._cumCleanByCumLocation(16);
  1383. break;
  1384. case 'mouth':
  1385. this._cumCleanByCumLocation(12);
  1386. break;
  1387. case 'interal':
  1388. this._cumCleanByCumLocation(0);
  1389. this._cumCleanByCumLocation(3);
  1390. this._cumCleanByCumLocation(17);
  1391. break;
  1392. case 'external':
  1393. this._cumCleanByCumLocation(1);
  1394. this._cumCleanByCumLocation(4);
  1395. this._cumCleanByCumLocation(8);
  1396. this._cumCleanByCumLocation(9);
  1397. this._cumCleanByCumLocation(10);
  1398. this._cumCleanByCumLocation(11);
  1399. this._cumCleanByCumLocation(13);
  1400. this._cumCleanByCumLocation(14);
  1401. this._cumCleanByCumLocation(15);
  1402. this._cumCleanByCumLocation(16);
  1403. break;
  1404. case 'hair':
  1405. this._cumCleanByCumLocation(16);
  1406. break;
  1407. case 'clothes':
  1408. this._cumCleanByCumLocation(2);
  1409. this._cumCleanByCumLocation(5);
  1410. this._cumCleanByCumLocation(6);
  1411. this._cumCleanByCumLocation(7);
  1412. break;
  1413. case 'enema':
  1414. case 'ass':
  1415. this._cumCleanByCumLocation(3);
  1416. break;
  1417. case 'bank':
  1418. case '':
  1419. break;
  1420. case 'hand':
  1421. case 'hands':
  1422. this._cumCleanByCumLocation(13);
  1423. break;
  1424. case 'face':
  1425. this._cumCleanByCumLocation(11);
  1426. break;
  1427. case 'face and hair':
  1428. this._cumCleanByCumLocation(11);
  1429. this._cumCleanByCumLocation(16);
  1430. break;
  1431. case 'head':
  1432. this._cumCleanByCumLocation(11);
  1433. this._cumCleanByCumLocation(12);
  1434. this._cumCleanByCumLocation(16);
  1435. break;
  1436. case 'belly':
  1437. this._cumCleanByCumLocation(14);
  1438. break;
  1439. case 'wipe ass and pussy':
  1440. this._cumCleanByCumLocation(1);
  1441. this._cumCleanByCumLocation(4);
  1442. break;
  1443. case 'breast':
  1444. case 'breasts':
  1445. this._cumCleanByCumLocation(15);
  1446. break;
  1447. case 'arms and legs':
  1448. this._cumCleanByCumLocation(9);
  1449. this._cumCleanByCumLocation(10);
  1450. break;
  1451. case 'full except internal':
  1452. this._cumCleanByCumLocation(1);
  1453. this._cumCleanByCumLocation(2);
  1454. this._cumCleanByCumLocation(4);
  1455. this._cumCleanByCumLocation(5);
  1456. this._cumCleanByCumLocation(6);
  1457. this._cumCleanByCumLocation(7);
  1458. this._cumCleanByCumLocation(8);
  1459. this._cumCleanByCumLocation(9);
  1460. this._cumCleanByCumLocation(10);
  1461. this._cumCleanByCumLocation(11);
  1462. this._cumCleanByCumLocation(12);
  1463. this._cumCleanByCumLocation(13);
  1464. this._cumCleanByCumLocation(14);
  1465. this._cumCleanByCumLocation(15);
  1466. this._cumCleanByCumLocation(16);
  1467. break;
  1468. case 'everything':
  1469. case 'reset':
  1470. /*
  1471. <<set $sparrage to null>>
  1472. <<set $sparrloc to null>>
  1473. <<set $sparrnam to null>>
  1474. <<set $sparrppt to null>>
  1475. <<set $sparridt to null>>
  1476. <<set $sparrvol to null>>
  1477. <<set $sparrslc to null>>
  1478. <<set $sparrcnt to null>>
  1479. *//*
  1480. break;
  1481. default:
  1482. console.error("Unsupported location in cumClean:",location);
  1483. return;
  1484. }
  1485. }*/
  1486. // ----- Pain -----
  1487. vgape = 0;
  1488. agape = 0;
  1489. spanked = 0;
  1490. _pain = new setup.Pain();
  1491. pain(bodypart){return this._pain.pain(bodypart)}
  1492. get painByBodyparts(){return this._pain.painByBodyparts}
  1493. painDeteriorate(timeToAdd){return this._pain.painDeteriorate(timeToAdd)}
  1494. get painRaw(){return this._pain.painRaw}
  1495. painInc(bodypart,v){return this._pain.painInc(bodypart,v)}
  1496. painIncAll(v){return this._pain.painIncAll(v)}
  1497. //get painRelief(){return this._pain.painRelief}
  1498. painSet(bodypart,v){return this._pain.painSet(bodypart,v)}
  1499. get painTotal(){return this._pain.painTotal}
  1500. get painOfActiveEffects(){
  1501. var painRaw = {};
  1502. const activeEffects = this.activeEffects;
  1503. for(const activeEffect of Object.values(activeEffects)){
  1504. for(const [bodypartId,painValue] of Object.entries(activeEffect.pain ?? {})){
  1505. painRaw[bodypartId] ??= [];
  1506. painRaw[bodypartId].push(painValue);
  1507. }
  1508. }
  1509. return painRaw;
  1510. }
  1511. // ----- Personality -----
  1512. _pcs_dom = 0
  1513. get pcs_dom(){return this._pcs_dom;}
  1514. set pcs_dom(v){
  1515. if(v < 0){
  1516. this.pcs_sub -= v;
  1517. this._pcs_dom = 0;
  1518. }else{
  1519. this._pcs_dom = Math.min(100,v);
  1520. this._balanceDomSub();
  1521. }
  1522. }
  1523. _pcs_sub = 0
  1524. get pcs_sub(){return this._pcs_sub;}
  1525. set pcs_sub(v){
  1526. if(v < 0){
  1527. this.pcs_dom -= v;
  1528. this._pcs_sub = 0;
  1529. }else{
  1530. this._pcs_sub = Math.min(100,v);
  1531. this._balanceDomSub();
  1532. }
  1533. }
  1534. _balanceDomSub(){
  1535. if(this._pcs_dom > 0 && this._pcs_sub > this._pcs_dom){
  1536. this._pcs_sub -= this._pcs_dom;
  1537. this._pcs_dom = 0;
  1538. }
  1539. if(this._pcs_sub > 0 && this._pcs_dom > this._pcs_sub){
  1540. this._pcs_dom -= this._pcs_sub;
  1541. this._pcs_sub = 0;
  1542. }
  1543. }
  1544. timeFactor = 1; //Used for cheating
  1545. // ----- Fame -----
  1546. _fame = {};
  1547. fame(location,type=null){
  1548. if(!type){
  1549. console.warn('Calling $pc.fame in deprecated format.',location);
  1550. let idParts = location.split('_');
  1551. location = idParts[0];
  1552. let type = idParts[1];
  1553. return this.fame(location,type);
  1554. }
  1555. if(!(location in this._fame))
  1556. return 0;
  1557. if(type == 'slut')
  1558. return this.fame(location,'sex') + this.fame(location,'prostitute');
  1559. return this._fame[location][type] || 0;
  1560. }
  1561. fameDec(location,type,amount,local=false){
  1562. this.fameInc(location,type,amount * -1,local);
  1563. }
  1564. fameInc(location,type,amount,local=false){
  1565. this._fame[location] ??= {};
  1566. this._fame[location][type] ??= 0;
  1567. let current = this._fame[location][type];
  1568. if(typeof amount == "string"){
  1569. if(current > 900){ //The original says 1000... how is this possible if 1000 is supposed to be the soft cap?
  1570. switch(amount){
  1571. case 'tiny':
  1572. case 'small':
  1573. return;
  1574. case 'medium': amount = rand(0,1);break;
  1575. case 'large': amount = rand(1,2);break;
  1576. case 'huge': amount = rand(1,4);break;
  1577. case 'BronzeMedal': amount = rand(15,25);break;
  1578. case 'SilverMedal': amount = rand(25,35);break;
  1579. case 'GoldMedal': amount = rand(35,45);break;
  1580. default:
  1581. console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
  1582. amount = 0;
  1583. }
  1584. }else if(current > 700){
  1585. switch(amount){
  1586. case 'tiny': amount = rand(0,1);break;
  1587. case 'small': amount = rand(1,2);break;
  1588. case 'medium': amount = rand(1,4);break;
  1589. case 'large': amount = rand(6,12);break;
  1590. case 'huge': amount = rand(10,24);break;
  1591. case 'BronzeMedal': amount = rand(25,60);break;
  1592. case 'SilverMedal': amount = rand(60,100);break;
  1593. case 'GoldMedal': amount = rand(100,150);break;
  1594. default:
  1595. console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
  1596. amount = 0;
  1597. }
  1598. }else if(current > 400){
  1599. switch(amount){
  1600. case 'tiny': amount = rand(1,2);break;
  1601. case 'small': amount = rand(1,4);break;
  1602. case 'medium': amount = rand(6,12);break;
  1603. case 'large': amount = rand(10,24);break;
  1604. case 'huge': amount = rand(20,50);break;
  1605. case 'BronzeMedal': amount = rand(50,100);break;
  1606. case 'SilverMedal': amount = rand(100,150);break;
  1607. case 'GoldMedal': amount = rand(150,200);break;
  1608. default:
  1609. console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
  1610. amount = 0;
  1611. }
  1612. }else{
  1613. switch(amount){
  1614. case 'tiny': amount = rand(1,4);break;
  1615. case 'small': amount = rand(6,12);break;
  1616. case 'medium': amount = rand(10,24);break;
  1617. case 'large': amount = rand(20,50);break;
  1618. case 'huge': amount = rand(40,70);break;
  1619. case 'BronzeMedal': amount = rand(150,250);break;
  1620. case 'SilverMedal': amount = rand(250,350);break;
  1621. case 'GoldMedal': amount = rand(350,450);break;
  1622. default:
  1623. console.warn('Argument for amount not reckognized in $pc.fameInc:',amount);
  1624. amount = 0;
  1625. }
  1626. }
  1627. }else{
  1628. if(current > 900)
  1629. amount /= 10;
  1630. else if(current > 700)
  1631. amount /= 6;
  1632. else if(current > 400)
  1633. amount /= 3;
  1634. }
  1635. if(amount == 0)
  1636. return;
  1637. let target = Math.clamp(current+amount,0,1000);
  1638. this._fame[location][type] = target;
  1639. if(type in fameAlwaysLocale)
  1640. local = true;
  1641. }
  1642. // ----- Effects -----
  1643. _effects:{ [key: string]: any } = {};
  1644. get activeEffects():{[key: string]:any}{
  1645. let result = {};
  1646. const time = State.variables.time;
  1647. for(const [effectid,effectData] of Object.entries(this._effects)){
  1648. if(effectData.expiration === undefined)
  1649. continue;
  1650. if(this._effects[effectid] === null || time.isFuture(this._effects[effectid]))
  1651. result[effectid] = effectData;
  1652. }
  1653. result = Object.assign({},this.drugsActiveEffects,result);
  1654. return result;
  1655. }
  1656. get activeEffectsMoodlets():{ [key: string]: ActiveMoodlet; }{
  1657. const time = State.variables.time;
  1658. return Object.fromEntries(
  1659. this.activeEffectsMoodletIDs.map(
  1660. (moodletId) =>
  1661. [
  1662. moodletId,
  1663. ActiveMoodlet.create(moodletId, {expiration: time.endTime})
  1664. ]
  1665. )
  1666. );
  1667. }
  1668. get activeEffectsMoodletIDs():Array<string>{
  1669. var result = [];
  1670. const activeEffects = this.activeEffects;
  1671. for(const activeEffect of Object.values(activeEffects)){
  1672. if(activeEffect.moodlet)
  1673. result.push(activeEffect.moodlet);
  1674. }
  1675. return result;
  1676. }
  1677. get activeEffectsSidebar(){
  1678. return Object.values(this.activeEffects).filter(effect=>effect.sidebar);
  1679. }
  1680. /**
  1681. * @param {string} key
  1682. * @param {undefined|'+'|'*'} [reduceMode='+']
  1683. * @returns {number}
  1684. */
  1685. #activeEffectValueByKey(key,reduceMode='+'){
  1686. const valueArray = this.#activeEffectValuesByKey(key);
  1687. switch (reduceMode) {
  1688. case '*':
  1689. return valueArray.reduce((accumulator, currentValue) => accumulator * currentValue,1);
  1690. case '+':
  1691. default:
  1692. return valueArray.reduce((accumulator, currentValue) => accumulator + currentValue,0);
  1693. }
  1694. }
  1695. /**
  1696. * @param {string} key
  1697. * @returns {{}}
  1698. */
  1699. #activeEffectValuesByKey(key){
  1700. var result =[];
  1701. const activeEffects = this.activeEffects;
  1702. for(const effect of Object.values(activeEffects)){
  1703. if(key in effect)
  1704. result.push(effect[key]);
  1705. }
  1706. return result;
  1707. }
  1708. /**
  1709. * Description placeholder
  1710. * @date 10/3/2023 - 1:07:07 PM
  1711. *
  1712. * @param {string} effectId
  1713. * @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.
  1714. */
  1715. effectAdd(effectId, expirationOrDuration, metadata={}){
  1716. const time = State.variables.time;
  1717. let expiration = expirationOrDuration;
  1718. if(typeof expirationOrDuration == 'number')
  1719. expiration = time.nowWithMinutesOffset(expirationOrDuration);
  1720. this._effects[effectId] = Object.assign({expiration:expiration},metadata) ;
  1721. }
  1722. /**
  1723. * @param {string} effectId
  1724. * @returns {boolean}
  1725. */
  1726. effectIsActive(effectId){
  1727. return (effectId in this.activeEffects);
  1728. }
  1729. // ----- Timed Actions -----
  1730. execute_every_15_minutes(){
  1731. const time = State.variables.time;
  1732. let timeMod = 0.25 * this.timeFactor; //We are doing some hourly calculations 4 times as often
  1733. let change = {
  1734. 'energy': 0,
  1735. 'hydra': 0
  1736. }
  1737. if(this.isSleeping){
  1738. change.energy = timed_stat_changes['energy']['sleep'] * timeMod;
  1739. change.hydra = timed_stat_changes['hydra']['sleep'] * timeMod;
  1740. }else{
  1741. change.energy = timed_stat_changes['energy']['default'] * timeMod;
  1742. change.hydra = timed_stat_changes['hydra']['default'] * timeMod;
  1743. }
  1744. this.pcs_energy -= change.energy;
  1745. this.pcs_hydra -= change.hydra;
  1746. this.energyDemand += change.energy;
  1747. if(this.isSleeping){
  1748. this.pcs_sleep -= timed_stat_changes['sleep']['sleep'] * timeMod ;
  1749. this.pcs_stam += this.stammax / 10;
  1750. this.pcs_willpwr += this.willpowermax / 100;
  1751. this.painIncAll(-1);
  1752. }else{
  1753. this.pcs_sleep -= timed_stat_changes['sleep']['default'] * timeMod ;
  1754. this.pcs_stam += this.stammax / 20;
  1755. this.pcs_willpwr += this.willpowermax / 300;
  1756. this.painIncAll(-0.5);
  1757. }
  1758. // ----- Dying -----
  1759. //TODO
  1760. /*
  1761. for (const [riskId, riskData] of Object.entries(dieRisks)){
  1762. this._death[riskId] ??= {stage:0};
  1763. if(this[riskData['variable']] == 0){
  1764. if(this._death[riskId].stage == 0 || !time.isFuture(this._death[riskId].nextStageDate)){
  1765. if((this._death[riskId].stage ?? 0) >= riskData.durations.length){
  1766. this.gameover = riskId;
  1767. console.warn("GAMEOVER set",riskId);
  1768. }else{
  1769. this._death[riskId].nextStageDate = time.nowWithMinutesOffset(riskData.durations[this._death[riskId].stage]);
  1770. this._death[riskId].stage += 1;
  1771. this.moodletApplyById(riskId+'_'+this._death[riskId].stage);
  1772. }
  1773. }
  1774. }
  1775. }*/
  1776. }
  1777. execute_every_1_hour(){
  1778. let timeFactor = this.timeFactor;
  1779. if(this.mood <= 20)
  1780. this.will_counter -= 2 * timeFactor;
  1781. if(this.willpowermax > 100){
  1782. if(this.pcs_willpwr < 25)
  1783. this.will_counter -= 1 * timeFactor;
  1784. }else{
  1785. if(this.pcs_willpwr < this.willpowermax / 4)
  1786. this.will_counter -= 1 * timeFactor;
  1787. }
  1788. if(this.will_counter <= -10){
  1789. this.willpowermax -= 1;
  1790. this.will_counter = 0;
  1791. }
  1792. this.pcs_lipbalm -= 1 * timeFactor;
  1793. }
  1794. execute_every_1_day(){
  1795. this.bodyDailyUpdate();
  1796. this.#skill_dayly();
  1797. this.#moodletsClean();
  1798. }
  1799. execute_every_timeUpdate(timeToAdd){
  1800. let timestamp = State.variables.time.minutesTimestamp;
  1801. if(this.deodorant_on == 1 && timestamp > this.deodorant_time)
  1802. this.deodorant_on = 0;
  1803. this.performShoePainAndExperience(timeToAdd);
  1804. this.performInhibitionExperience(timeToAdd);
  1805. this.horny -= timeToAdd * this.hornyDeteriorationRate;
  1806. this.drugsDeteriorate(timeToAdd);
  1807. this.painDeteriorate(timeToAdd);
  1808. }
  1809. performInhibitionExperience(minutes){
  1810. if(this.isSleeping)
  1811. return;
  1812. let balancingFactorExp = 0.5;
  1813. let outsideFactor = 2;
  1814. let location = State.variables.location;
  1815. // You don't get any inhibition progress while being at home
  1816. if(location.isHome)
  1817. return;
  1818. let totalExpFactor = balancingFactorExp * (location.isOutdoors ? outsideFactor : 1);
  1819. let wardrobe = State.variables.wardrobe;
  1820. let clothesInhib = wardrobe.PCloInhibit;
  1821. let playerInhib = this.pcs_inhib;
  1822. if(playerInhib > clothesInhib)
  1823. return;
  1824. let inhibDiff = clothesInhib - playerInhib;
  1825. let expRaw = Math.ceil(inhibDiff / 10) * 10; // Round up to 10, 20, etc.
  1826. let experienceGained = expRaw * minutes / 60 * totalExpFactor;
  1827. this.skillExperienceGain("inhibition",experienceGained);
  1828. }
  1829. performShoePainAndExperience(minutes){
  1830. if(this.isSleeping)
  1831. return;
  1832. let balancingFactorPain = 2;
  1833. let balancingFactorExp = 0.5;
  1834. let wardrobe = State.variables.wardrobe;
  1835. if(!wardrobe.isWearingShoes)
  1836. return;
  1837. let shoeQuality = wardrobe.PShoQuaility;
  1838. let shoeHeight = wardrobe.shoesHeight;
  1839. let painOfShoe = shoeHeight / 10 - shoeQuality;
  1840. let heelSkill = this.skillLevel("highHeels");
  1841. let painReductionDueToSkill = heelSkill / 5;
  1842. let painOfWalkingInShoes = Math.max(0,painOfShoe-painReductionDueToSkill) * balancingFactorPain;
  1843. painOfWalkingInShoes = painOfWalkingInShoes / 60 * minutes;
  1844. this.painInc('feet',painOfWalkingInShoes);
  1845. // ----- Experience -----
  1846. let experienceLevelRequiredForShoes = Math.clamp((shoeHeight / 10) * 3,0,100);
  1847. let experienceLevelDifference = experienceLevelRequiredForShoes - heelSkill;
  1848. let experienceGained = 0;
  1849. if(experienceLevelDifference > 0)
  1850. experienceGained = shoeHeight;
  1851. else{
  1852. //Results in gaining half as many experience for being 10 points over target, a quarter fo 20 over target, and so on.
  1853. const basis = 2;
  1854. const exponent = experienceLevelDifference / -10;
  1855. experienceGained = shoeHeight / Math.pow(2,exponent);
  1856. }
  1857. experienceGained = Math.max(0,experienceGained) * balancingFactorExp;
  1858. experienceGained = experienceGained / 60 * minutes;
  1859. this.skillExperienceGain("highHeels",experienceGained);
  1860. }
  1861. // ----- Deco -----
  1862. // Deco includes tattoos, piercings, glasses, etc.
  1863. _deco = {}
  1864. _decoOwned = {}
  1865. decoAdd(type,position,index){
  1866. if(!this._decoOwned[type])
  1867. this._decoOwned[type] = {};
  1868. if(!this._decoOwned[type][position])
  1869. this._decoOwned[type][position] = [];
  1870. if(!this._decoOwned[type][position].includes(index))
  1871. this._decoOwned[type][position].push(index);
  1872. }
  1873. decoHas(type,position,index){
  1874. if(!(this._decoOwned[type]?.[position]))
  1875. return false;
  1876. return this._decoOwned[type][position].includes(index);
  1877. }
  1878. decoGet(type,position='default'){
  1879. return this._deco[type]?.[position] ?? 0;
  1880. }
  1881. get decoOwned(){return this._decoOwned;}
  1882. decoRemove(type,position='default'){
  1883. this.decoSet(type,position,null);
  1884. }
  1885. decoSet(type,position='default',v){
  1886. if(!(type in this._deco))
  1887. this._deco[type] = {};
  1888. this._deco[type][position] = v;
  1889. }
  1890. decoWear(type,position='default',v){
  1891. this.decoSet(type,position,v);
  1892. if(v && v != -1)
  1893. this.decoAdd(type,position,v);
  1894. }
  1895. // ----- Bodyparts -----
  1896. bodyPartCovered(bodypartId){
  1897. let bodypartData = setup.getBodypart(bodypartId);
  1898. if(!bodypartData){
  1899. console.error('Data not found by $pc.bodyPartCovered for bodypart',bodypartId);
  1900. return false;
  1901. }
  1902. if(typeof bodypartData.covered === "undefined")
  1903. return false;
  1904. if(typeof bodypartData.covered === "boolean")
  1905. return bodypartData.covered;
  1906. if(typeof bodypartData.covered === "string")
  1907. return Scripting.evalTwineScript(bodypartData.covered);
  1908. console.error('Unsupported type of covered of bodypart in $pc.bodyPartCovered',bodypartId,bodypartData);
  1909. return false;
  1910. }
  1911. // ----- Init -----
  1912. init_final(){
  1913. for (const [skillId, skillData] of Object.entries(this._skills)){
  1914. this._skills[skillId].experienceHistory = [skillData.experience];
  1915. }
  1916. }
  1917. // ----- Occupation -----
  1918. get isSchoolStudent(){
  1919. return State.variables.q.questIsActive("school");
  1920. }
  1921. // ----- Vehicles -----
  1922. _vehicleInUse = null;
  1923. get vehicleInUse(){return this._vehicleInUse;}
  1924. set vehicleInUse(v){this._vehicleInUse = v;}
  1925. get vehicleData(){
  1926. let inventory = State.variables.inventory;
  1927. let vehicleData = inventory.metadata(this.vehicleInUse,'vehicles');
  1928. return vehicleData;
  1929. }
  1930. vehicleCanEnterPassage(passage){
  1931. var vehicleType = this.vehicleType;
  1932. if(vehicleType != 'car')
  1933. return true;
  1934. var passageTags = tags(passage);
  1935. return passageTags.includes('car');
  1936. }
  1937. get vehicleSpeed(){
  1938. return this.vehicleData.speed ?? 1;
  1939. }
  1940. get vehicleType(){
  1941. if(!this.vehicleInUse)
  1942. return 'walk';
  1943. let inventory = State.variables.inventory;
  1944. let vehicleData = inventory.metadata(this.vehicleInUse,'vehicles');
  1945. return vehicleData.type;
  1946. }
  1947. // ----- Menstruation Cycle -----
  1948. _cycleStart = -7;
  1949. _cycleLength = 28;
  1950. _cycleLengthLast = 28;
  1951. get cycleDay(){
  1952. const time = State.variables.time;
  1953. const daystart = time.daystart;
  1954. return daystart - this.cycleStart;
  1955. }
  1956. get cycleLength(){return this._cycleLength;} // the length of the current cycle
  1957. set cycleLength(v){this._cycleLength = v;}
  1958. get cycleLengthLast(){return this._cycleLengthLast;}
  1959. set cycleLengthLast(v){this._cycleLengthLast = v;}
  1960. get cycleStart(){return this._cycleStart;} // the daystart of the current cycle
  1961. set cycleStart(v){this._cycleStart = v;}
  1962. cycleStartMessageSent = true;
  1963. cycleStartHour = 17;
  1964. get menstruationLength(){return 3;}
  1965. get ovulationDay(){return this.cycleStart + this.cycleLength - 14;}
  1966. ovulationHour = 19;
  1967. get cyclePhase(){
  1968. if(this.effectIsActive('cycleBlocked'))
  1969. return 'blocked';
  1970. const today = State.variables.time.daystart;
  1971. if(today - this.cycleStart - 1 > this.cycleLength || (today - this.cycleStart > this.cycleLength && State.variables.time.hour >= this.cycleStartHour))
  1972. this.cycleStartNew();
  1973. if(today == this.cycleStart){
  1974. //if(State.variables.time.hour < this.cycleStartHour)
  1975. // return 'end';
  1976. return 'menses';
  1977. }else if(this.cycleStart + this.menstruationLength >= today){
  1978. return 'menses';
  1979. }else if(today < this.ovulationDay){
  1980. return 'start';
  1981. }else if(today == this.ovulationDay && State.variables.time.hour < this.ovulationHour){
  1982. return 'start';
  1983. }else{
  1984. return 'end';
  1985. }
  1986. }
  1987. cycleStartNew(){
  1988. const today = State.variables.time.daystart;
  1989. this.cycleStart = today;
  1990. this.cycleStartMessageSent = false;
  1991. this.cycleLengthLast = this.cycleLength;
  1992. if(this.cycleLength >= 35)
  1993. this.cycleLength += rand(-3,0);
  1994. else if(this.cycleLength >= 33)
  1995. this.cycleLength += rand(-2,1);
  1996. else if(this.cycleLength >= 26)
  1997. this.cycleLength += rand(-1,1);
  1998. else if(this.cycleLength >= 24)
  1999. this.cycleLength += rand(-1,2);
  2000. else
  2001. this.cycleLength += rand(0,3);
  2002. this.cycleStartHour = rand(0,23);
  2003. this.ovulationHour = rand(0,23);
  2004. }
  2005. get cycleArousalModificator(){
  2006. switch(this.cyclePhase){
  2007. case 'end':
  2008. return {factor: 0.5, target: 0};
  2009. case 'menses':
  2010. return {factor: 1, target: 0};
  2011. case 'start':
  2012. default:
  2013. const ovulationDay = this.ovulationDay;
  2014. const daystart = State.variables.time.daystart;
  2015. const daysTilOvolationDay = ovulationDay - daystart;
  2016. if(daysTilOvolationDay <= 1)
  2017. return {factor: 2, target: 30};
  2018. if(daysTilOvolationDay <= 2)
  2019. return {factor: 1.5, target: 10};
  2020. return {factor: 1, target: 0};
  2021. }
  2022. }
  2023. get cycleProductsEnabled(){
  2024. const today = State.variables.time.daystart;
  2025. if(this.cyclePhase == 'menses')
  2026. return true;
  2027. if(today - this.cycleStart >= this.cycleLength)
  2028. return true;
  2029. return false;
  2030. }
  2031. _cycleProduct = null;
  2032. get cycleProduct() {
  2033. return this._cycleProduct;
  2034. }
  2035. set cycleProduct(value) {
  2036. this._cycleProduct = value;
  2037. }
  2038. cycleProductSince = null;
  2039. get cycleProductsUsed(){
  2040. if(State.variables.time.secondsSinceDate(this.cycleProductSince) > 43200)
  2041. return false;
  2042. return !(!(this._cycleProduct));
  2043. }
  2044. get indecencies(){
  2045. let result = [];
  2046. if(this.cyclePhase == 'menses' && !this.cycleProductsUsed)
  2047. result.push('menses_blood');
  2048. if(State.variables.wardrobe.clothingIsNude){
  2049. if(!State.variables.wardrobe.isWearingBra)
  2050. result.push('naked_breast');
  2051. if(!State.variables.wardrobe.isWearingPanties)
  2052. result.push('naked_pussy');
  2053. }
  2054. if(this.cumVisibleBodypartIds.length > 0)
  2055. result.push('cum');
  2056. return result;
  2057. }
  2058. get isIndecent(){
  2059. return (!(this.indecencies.length == 0));
  2060. }
  2061. constructor(){}
  2062. _init(playerCharacter){
  2063. Object.keys(playerCharacter).forEach(function (pn) {
  2064. this[pn] = clone(playerCharacter[pn]);
  2065. }, this);
  2066. return this;
  2067. }
  2068. clone = function () {
  2069. return (new PlayerCharacter())._init(this);
  2070. };
  2071. toJSON = function () {
  2072. var ownData = {};
  2073. Object.keys(this).forEach(function (pn) {
  2074. if(typeof this[pn] !== "function")
  2075. ownData[pn] = clone(this[pn]);
  2076. }, this);
  2077. return JSON.reviveWrapper('(new PlayerCharacter())._init($ReviveData$)', ownData);
  2078. };
  2079. }
  2080. setup.PlayerCharacter = PlayerCharacter;