PlayerCharacter.ts 74 KB

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