NPCsDict.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. /// <reference path="NPCAccessor.ts" />
  2. const npcFieldBoundaries = {
  3. rel:{min:0,max:100}
  4. }
  5. let pronounDictLowerCase = {
  6. 'he': ['he','she'],
  7. 'him': ['him','her'],
  8. 'his': ['his','her'],
  9. 'himself': ['himself','herself'],
  10. }
  11. let pronounDictUpperFirst = {};
  12. let pronounDictUpperCase = {};
  13. for (const [key, value] of Object.entries(pronounDictLowerCase)) {
  14. pronounDictUpperFirst[key.toUpperFirst()] = [value[0].toUpperFirst(),value[1].toUpperFirst()];
  15. pronounDictUpperCase[key.toUpperCase()] = [value[0].toUpperCase(),value[1].toUpperCase()];
  16. }
  17. const pronounDict = Object.assign({},pronounDictLowerCase,pronounDictUpperFirst,pronounDictUpperCase);
  18. /*
  19. NPC-Fields:
  20. date:
  21. date.count:
  22. n: Number of dates one had with the NPC.
  23. During a date, it indicates that it is the nth date
  24. date.kissed:
  25. n: Number of times one has kissed the NPC.
  26. date.nextDateExpected:
  27. -1: does not expect a date and won't call asking for one
  28. most commonly set to -1 because a date is already scheduled
  29. n: will start calling at day n and ask for a date
  30. rel: Relationship Value / how much the NPC likes you
  31. familiarity:
  32. 0 - stranger
  33. 1000 - knows everything about each other
  34. */
  35. class NPCsDict{
  36. _dynamicData = {}
  37. _generatedIds = [];
  38. _generatedIdsCounter = 0;
  39. _boyfriends = [];
  40. dec(npcId,field,v,min=-Infinity,max=Infinity){
  41. this.inc(npcId,field,v * -1,min,max);
  42. }
  43. /**
  44. * Creates a new NPC and returns his ID.
  45. * @param {string} purpose the reason the NPC exists. If all purposes are removed, it is flagged for removal.
  46. * @returns {string}
  47. */
  48. create(purpose:string='static'):string{
  49. this._generatedIdsCounter += 1;
  50. let id = 'C'+this._generatedIdsCounter;
  51. this._generatedIds.push(id);
  52. this.set(id,'_purposes',[purpose]);
  53. return id;
  54. }
  55. createNPC(purpose:string='static'):NPC{
  56. return this.npc(this.create(purpose));
  57. }
  58. delete(npcID:string):void{
  59. delete this._dynamicData[npcID];
  60. this._generatedIds.delete(npcID);
  61. }
  62. generate(purpose:string,flags:NPCGenerateOptions):NPC{
  63. return setup.generateNPC(this,purpose,flags);
  64. }
  65. /**
  66. * Get a value of a field for a NPC.
  67. *
  68. * Will use dynamic data, if present. Otherwise, will use constant data. If constant data is not defined, will try passage data.
  69. *
  70. * @param {string} npcId
  71. * @param {string} field
  72. * @param {*} [def=undefined]
  73. * @returns {*}
  74. */
  75. get(npcId:string,field:string,def:any=undefined):any{
  76. let data = this.npcData(npcId);
  77. if(field in data)
  78. return data[field];
  79. if("passage" in data){
  80. let resultFromPassage = setup.func(data.passage,'vars',field);
  81. if(resultFromPassage)
  82. return resultFromPassage;
  83. }
  84. return def;
  85. }
  86. getDynamicData(npcId){
  87. if(npcId in this._dynamicData)
  88. return this._dynamicData[npcId];
  89. return {};
  90. }
  91. getStaticData(npcId){
  92. if(npcId in setup.npcs)
  93. return setup.npcs[npcId];
  94. return {};
  95. }
  96. inc(npcId,field,v:string|number,min=-Infinity,max=Infinity){
  97. let d:string|number = '';
  98. if(typeof v === "number")
  99. d = 0;
  100. let current = this.get(npcId,field,d);
  101. let newValue = current + v;
  102. newValue = Math.min(Math.max(current,max),newValue);
  103. newValue = Math.max(Math.min(current,min),newValue);
  104. this.set(npcId,field,newValue);
  105. }
  106. incBulk(npcIds,field,v,min=-Infinity,max=Infinity){
  107. for(let npcId of npcIds){
  108. this.inc(npcId,field,v,min,max);
  109. }
  110. }
  111. incBulkByFilter(filter={},field,v,min=-Infinity,max=Infinity){
  112. let npcsIds = this.ids(filter);
  113. this.incBulk(npcsIds,field,v,min,max);
  114. }
  115. ids(filter={}){
  116. let keysStatic = Object.keys(setup.npcs);
  117. let keysDynamic= Object.keys(this._dynamicData);
  118. let keys = keysStatic.concatUnique(keysDynamic);
  119. if(jQuery.isEmptyObject(filter))
  120. return keys;
  121. let keysMatchingFilter = [];
  122. for(let key of keys){
  123. if(this.npcFitsFilter(key,filter))
  124. keysMatchingFilter.push(key);
  125. }
  126. return keysMatchingFilter;
  127. }
  128. makeBoyfriend(npcId){
  129. if(this._boyfriends.includes(npcId)){
  130. console.warn('Already boyfriend:',npcId);
  131. return;
  132. }
  133. this.set(npcId,'keepAlive',true);
  134. let defaults = this.get(npcId,'defaults',[]);
  135. defaults.push('boyfriend');
  136. this.set(npcId,'defaults',defaults);
  137. this._boyfriends.push(npcId);
  138. }
  139. memoryUpdate(npcId){
  140. let rememberedFields = this.get(npcId,'memoryVars',[]);
  141. if(!rememberedFields.length)
  142. return;
  143. let now = State.variables.time.now;
  144. let memory = {time:now,vars:{}};
  145. for(let rememberedField of rememberedFields){
  146. memory.vars[rememberedField] = State.getVar(rememberedField);
  147. }
  148. this.set(npcId,'memory',memory);
  149. console.log('NPC memory updated',npcId,memory);
  150. }
  151. memoryUpdateAll(){
  152. const npcIds = this.ids();
  153. for(const npcId of npcIds){
  154. this.memoryUpdate(npcId);
  155. }
  156. }
  157. npc(npcId:string):NPC{
  158. return new Proxy(new NPCAccessor(npcId),NPCAccessor.handler);
  159. }
  160. npcData(npcId){
  161. let staticData = this.getStaticData(npcId);
  162. let dynamicData = this.getDynamicData(npcId);
  163. let data = Object.assign({},staticData,dynamicData);
  164. if("gender" in data)
  165. data = Object.assign({},setup.npcDefaults.gender[data.gender],data); //Apply the gender-defaults first.
  166. if("defaults" in data){
  167. for(let defaultId of data.defaults){
  168. let def = setup.npcDefaults[defaultId];
  169. data = Object.assign({},def,data); //Apply the defaults first. They could be overwritten by static or dynamic data
  170. }
  171. }
  172. return data;
  173. }
  174. npcFitsFilter(npcId,filters:{[key:string]:any}={}){
  175. outerLoop:for (const [filterKey, filterValue] of Object.entries(filters)) {
  176. let npcValue = undefined;
  177. if(typeof filterValue === "object" && filterValue.filter){
  178. switch(filterValue.filter){
  179. case 'minMax':
  180. let min = filterValue.min ?? -Infinity;
  181. let max = filterValue.max ?? Infinity;
  182. npcValue = this.get(npcId,filterKey,0);
  183. if(npcValue > max || npcValue < min)
  184. return false;
  185. continue outerLoop;
  186. }
  187. }
  188. else if(typeof filterValue === "number")
  189. npcValue = this.get(npcId,filterKey,0);
  190. else
  191. npcValue = this.get(npcId,filterKey,'');
  192. if(npcValue != filterValue){
  193. return false;
  194. }
  195. }
  196. return true;
  197. }
  198. npcLikesAppearance(npcId):boolean{
  199. let appearanceDemand = this.get(npcId,'appearanceDemand',[null,null,5]);
  200. let hotcat = State.variables.pc.appearance;
  201. if(appearanceDemand[0] !== null && hotcat.geneticValue < appearanceDemand[0]) return false;
  202. if(appearanceDemand[1] !== null && hotcat.lastingValue < appearanceDemand[1]) return false;
  203. if(appearanceDemand[2] !== null && hotcat.currentValue < appearanceDemand[2]) return false;
  204. let preferenceBalance = this.preferenceBalance(npcId);
  205. return (preferenceBalance >= 0);
  206. }
  207. /**
  208. * Hof much the NPC likes / dislikes the PCs current appearance based on preferences.
  209. * @date 8/30/2023 - 9:43:26 AM
  210. *
  211. * @param {string} npcId
  212. * @returns {number}
  213. */
  214. preferenceBalance(npcId):number{
  215. let preferenceValues = this.preferencesCompare(npcId);
  216. return Object.values(preferenceValues).reduce((a,x) => a+x, 0);
  217. }
  218. preferencesComment(npcId,positives=[],negatives=[],today=undefined){
  219. today ??= State.variables.time.daystart;
  220. if(positives === null)
  221. positives = [];
  222. else if(!Array.isArray(positives))
  223. positives = [positives];
  224. if(negatives === null)
  225. negatives = [];
  226. else if(!Array.isArray(negatives))
  227. negatives = [negatives];
  228. const positiveValue = today;
  229. const negativeValue = today * -1;
  230. let comments = this.get(npcId,'preferenceComments',{});
  231. positives.forEach(e => {comments[e] = positiveValue;});
  232. negatives.forEach(e => {comments[e] = negativeValue;});
  233. this.set(npcId,'preferenceComments',comments);
  234. console.log('NPC Preferences Comments updated',npcId,comments);
  235. }
  236. /**
  237. * Returns an object with all the preferenceIds of the requested NPC as keys and the days since they were commented on as values (negative if the comment was a negative one). The value is null for each preference which was never commented.
  238. * @date 8/21/2023 - 9:01:37 AM
  239. *
  240. * @param {*} npcId The Id of the NPC you want to get the result for.
  241. * @returns {{}}
  242. */
  243. preferenceCommentOffset(npcId,today=undefined){
  244. today ??= State.variables.time.daystart;
  245. let result = {};
  246. let comments = this.get(npcId,'preferenceComments',{});
  247. let preferences = this.get(npcId,'preference');
  248. for (const preferenceId of Object.keys(preferences)) {
  249. if(!(preferenceId in comments)){
  250. result[preferenceId] = null;
  251. continue;
  252. }
  253. // dayRaw is negative if the NPCs last comment on this issue was a negative one
  254. let dayRaw = comments[preferenceId];
  255. let day = Math.abs(dayRaw);
  256. let commentDirection = Math.sign(dayRaw);
  257. let dayOffset = today - day;
  258. result[preferenceId] = (dayOffset + 1) * commentDirection; //+1 so the value isn't 0 for today, which would make it impossible to distinguish between a positive and a negative comment
  259. }
  260. return result;
  261. }
  262. preferenceCommentWeighted(npcId:string):any{
  263. let preferenceCommentOffset = this.preferenceCommentOffset(npcId);
  264. let preferencesCompare = this.preferencesCompare(npcId);
  265. let preferences = {
  266. positive:{
  267. currentMatches: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v>0))
  268. },
  269. negative:{
  270. currentMatches: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v<0))
  271. }
  272. }
  273. for(let positive_negative of ['positive','negative']){
  274. preferences[positive_negative].switched = {}; // Priority 1: The NPC made a negative comment before and his opinion is now positive or vice versa.
  275. preferences[positive_negative].neverCommented = {}; // Priority 2: The NPC never commented this specific aspect.
  276. preferences[positive_negative].repeat = {}; // Priority 3: The NPC has the same opinion as before and states it again.
  277. for (const [preferenceId, value] of Object.entries(preferences[positive_negative].currentMatches)) {
  278. // Value is the number that represents how negative or positive the opinion is
  279. if(typeof value != "number")
  280. continue;
  281. let opinionMagnitude = Math.abs(value);
  282. let opinionIsPositive = Math.sign(value); //-1: Negative, 1: Positive
  283. let commentOffsetRaw = preferenceCommentOffset[preferenceId];
  284. if(commentOffsetRaw === null)
  285. preferences[positive_negative].neverCommented[preferenceId] = opinionMagnitude;
  286. else{
  287. let daysSinceLastCommend = Math.abs(commentOffsetRaw);
  288. let lastCommentWasPositive = Math.sign(commentOffsetRaw);
  289. if(opinionIsPositive != lastCommentWasPositive)
  290. preferences[positive_negative].switched[preferenceId] = opinionMagnitude * daysSinceLastCommend;
  291. else
  292. preferences[positive_negative].repeat[preferenceId] = opinionMagnitude * daysSinceLastCommend;
  293. }
  294. }
  295. }
  296. return preferences;
  297. /*let preferences = {
  298. positive: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v>0)),
  299. negative: Object.fromEntries(Object.entries(preferencesCompare).filter(([k,v])=>v<0))
  300. }
  301. let switchedTo = {positive:{},negative:{}};
  302. let neverCommentedOn = {positive:{},negative:{}};
  303. let other = {positive:{},negative:{}};
  304. for(let pn of ['positive','negative']){
  305. for (const [preferenceId, value] of Object.entries(preferences[pn])) {
  306. let commentOffset = preferenceCommentOffset[preferenceId];
  307. if(commentOffset === null)
  308. neverCommentedOn[pn][preferenceId] = value;
  309. else{
  310. let daysSinceComment = Math.abs(commentOffset);
  311. if(Math.sign(commentOffset) == Math.sign(value))
  312. switchedTo[pn][preferenceId] = value * daysSinceComment;
  313. else{
  314. other[pn][preferenceId] = value * daysSinceComment;
  315. }
  316. }
  317. }
  318. }*/
  319. //return [preferencesCompare,preferencesPositive,preferencesNegative];
  320. }
  321. preferencesToCommentNow(npcId){
  322. /*let preferenceCommentWeighted = this.preferenceCommentWeighted(npcId);
  323. let positive = null;
  324. let negative = null;
  325. let positiveResult = null;
  326. let negativeResult = null;
  327. let positiveResultType = null;
  328. let negativeResultType = null;
  329. if(Object.keys(preferenceCommentWeighted.positive.neverCommented).length > 0){
  330. positive = preferenceCommentWeighted.positive.neverCommented;
  331. positiveResultType = 'n';
  332. }else if(Object.keys(preferenceCommentWeighted.positive.switched).length > 0){
  333. positive = preferenceCommentWeighted.positive.switched;
  334. positiveResultType = 's';
  335. }else if(Object.keys(preferenceCommentWeighted.positive.repeat).length > 0){
  336. positive = preferenceCommentWeighted.positive.repeat;
  337. positiveResultType = 'r';
  338. }
  339. if(Object.keys(preferenceCommentWeighted.negative.switched).length > 0){ // Switch has a higher priority than uncommented for negative. It's the opposite fpr positive
  340. negative = preferenceCommentWeighted.negative.switched;
  341. negativeResultType = 's';
  342. }else if(Object.keys(preferenceCommentWeighted.negative.neverCommented).length > 0){
  343. negative = preferenceCommentWeighted.negative.neverCommented;
  344. negativeResultType = 'n';
  345. }else if(Object.keys(preferenceCommentWeighted.negative.repeat).length > 0){
  346. negative = preferenceCommentWeighted.negative.repeat;
  347. negativeResultType = 'r';
  348. }
  349. // https://stackoverflow.com/a/1069840
  350. if(positive){
  351. const positiveSorted = Object.entries(positive)
  352. .sort(([,a],[,b]) => b-a);
  353. positiveResult = positiveSorted.first()[0];
  354. }
  355. if(negative){
  356. const negativeSorted = Object.entries(negative)
  357. .sort(([,a],[,b]) => b-a);
  358. negativeResult = negativeSorted.first()[0];
  359. }
  360. let balance = Object.values(preferenceCommentWeighted.positive.currentMatches).reduce((a, b) => {return a + b;},0)
  361. + Object.values(preferenceCommentWeighted.negative.currentMatches).reduce((a, b) => {return a + b;},0);
  362. return {
  363. positive: positiveResult,
  364. positiveResultType: positiveResultType,
  365. negative: negativeResult,
  366. negativeResultType:negativeResultType,
  367. balance: balance
  368. }*/
  369. //TODO
  370. }
  371. /**
  372. * Checks Preferences of a NPC and returns an object with key: preference id, value: magnitued of success (positive) or failure (negative).
  373. * @date 8/30/2023 - 7:20:46 AM
  374. *
  375. * @example
  376. * //Returns {"short_skirt_d": 1}
  377. * @param {*} npcId
  378. * @returns {{}}
  379. */
  380. preferencesCompare(npcId):{[key: string]:number}{
  381. /*let result = {};
  382. let preferences = this.get(npcId,'preference');
  383. for (const [preferenceId, value] of Object.entries(preferences)) {
  384. let preferenceData = setup.npcPreferences[preferenceId];
  385. if(!preferenceData){
  386. console.warn('Preference not found:',preferenceId);
  387. continue;
  388. }
  389. let valueSucceeded = value;
  390. let valueFailed = -value;
  391. let valueNeutral = 0;
  392. if(Array.isArray(value)){
  393. valueSucceeded = value[0];
  394. valueFailed = value[1];
  395. }
  396. if(!preferenceData.s)
  397. result[preferenceId] = valueSucceeded;
  398. else if(Scripting.evalTwineScript(preferenceData.s))
  399. result[preferenceId] = valueSucceeded;
  400. else if(preferenceData.f && Scripting.evalTwineScript(preferenceData.f))
  401. result[preferenceId] = valueFailed;
  402. else
  403. result[preferenceId] = valueNeutral;
  404. }
  405. return result;*/
  406. //TODO
  407. return {};
  408. }
  409. prerencesKnown(npcId){
  410. let result = {};
  411. let preference = this.get(npcId,'preference',{});
  412. let preferenceComments = this.get(npcId,'preferenceComments',{});
  413. for(var preferenceKey of Object.keys(preferenceComments)){
  414. result[preferenceKey] = preference[preferenceKey];
  415. }
  416. return result;
  417. }
  418. pronoun(npcId,pronoun){
  419. let gender = this.get(npcId,'gender',0);
  420. if(pronoun in pronounDict)
  421. return pronounDict[pronoun][gender];
  422. return 'GENDER KEY MISSING: '+pronoun+'['+gender+']';
  423. }
  424. rel(npcId){
  425. this.relUpdate(npcId);
  426. return this.get(npcId,'rel');
  427. }
  428. /**
  429. * Increases the relationships of a npc by inc. Diminishing return applied.
  430. * @deprecated
  431. * @param {string} npcId The ID of the NPC
  432. * @param {number} inc The amount by which to increase. Negative numbers decrease the relationship
  433. * @param {Array} boundaries Won't increase/decrease past these values. Format: [min,max]
  434. * @param {number} today Daystart of the current day. Used for diminishing return algorithm.
  435. */
  436. relInc(npcId,inc,boundaries=[0,100],today=undefined){
  437. console.warn('Use of $npcs.relInc is deprecated. Use <<relationship>> instead');
  438. let relGrace = 2; //The change that won't be diminished
  439. let boundaryLower = boundaries[0];
  440. let boundaryUpper = boundaries[1];
  441. today ??= State.variables.time.daystart;
  442. let rel_current = this.get(npcId,'rel',50);
  443. if(!inc)
  444. return;
  445. if(inc > 0 && rel_current >= boundaryUpper)
  446. return;
  447. if(inc < 0 && rel_current <= boundaryLower)
  448. return;
  449. let rel_date = this.get(npcId,'rel_date',undefined);
  450. let rel_base = undefined;
  451. let rel_change_raw = 0;
  452. if(!rel_date || rel_date != today){
  453. this.set(npcId,'rel_date',today);
  454. this.set(npcId,'rel_base',rel_current);
  455. rel_base = rel_current;
  456. }else{
  457. rel_base = this.get(npcId,'rel_base');
  458. rel_change_raw = this.get(npcId,'rel_change_raw');
  459. }
  460. rel_change_raw += inc;
  461. this.set(npcId,'rel_change_raw',rel_change_raw);
  462. let rel_new = 0;//TODO: setup.diminishingReturn(rel_base,rel_base + rel_change_raw,relGrace,'sqrt');
  463. if(inc > 0)
  464. rel_new = Math.min(rel_new,boundaryUpper);
  465. else if(inc < 0)
  466. rel_new = Math.max(rel_new,boundaryLower);
  467. this.set(npcId,'rel',rel_new);
  468. }
  469. relUpdate(npcId){
  470. const time = State.variables.time;
  471. const dayStart = time.daystart;
  472. const lastRelUpdate = this.get(npcId,'relu',-1);
  473. let currentRel = this.get(npcId,'rel');
  474. let lastingRel = this.get(npcId,'rell',currentRel);
  475. if(lastRelUpdate == -1){
  476. this.set(npcId,'rell',lastingRel);
  477. this.set(npcId,'relu',dayStart);
  478. return;
  479. }
  480. if(dayStart <= lastRelUpdate)
  481. return;
  482. for(let updateDay = lastRelUpdate; updateDay < dayStart; updateDay++){
  483. let relDifference = currentRel - lastingRel;
  484. let changeInLastingRel = Math.ceilAbs(relDifference / 10);
  485. lastingRel += changeInLastingRel;
  486. relDifference = lastingRel - currentRel;//note opposite order
  487. let changeInCurrentRel = Math.ceilAbs(relDifference / 4);
  488. currentRel += changeInCurrentRel;
  489. }
  490. this.set(npcId,'rel',currentRel);
  491. this.set(npcId,'rell',lastingRel);
  492. this.set(npcId,'relu',dayStart);
  493. }
  494. set(npcId,field,v){
  495. if(!(npcId in this._dynamicData))
  496. this._dynamicData[npcId] = {};
  497. if(field in npcFieldBoundaries){
  498. let fieldRules = npcFieldBoundaries[field];
  499. if("min" in fieldRules)
  500. v = Math.max(v, fieldRules.min);
  501. if("max" in fieldRules)
  502. v = Math.min(v, fieldRules.max);
  503. }
  504. this._dynamicData[npcId][field] = v;
  505. console.log("NPC value set",npcId,field,v);
  506. }
  507. //npcIds: Array of ids
  508. setBulk(npcIds,field,v){
  509. for(let npcId of npcIds){
  510. this.set(npcId,field,v);
  511. }
  512. }
  513. setBulkByFilter(filter={},field,v){
  514. let npcsIds = this.ids(filter);
  515. this.setBulk(npcsIds,field,v);
  516. }
  517. // ----- Dating -----
  518. _datingNPCs = [];
  519. get datingNPCs(){return this._datingNPCs;}
  520. // Only sets the NPC to dating. Does not make him call you at any point. Use <<datingStart>> for this.
  521. datingStart(npcID){
  522. console.log("Start Dating:",npcID);
  523. this._datingNPCs.push(npcID);
  524. this.set(npcID,'dating',true);
  525. }
  526. //#region Activity
  527. activity(npcId:string):NPCActivity{
  528. let activityInformationCached:ExpiringVar<NPCActivity>|null = this.get(npcId,'activity',null);
  529. if(!State.variables.time.isExpired(activityInformationCached))
  530. return activityInformationCached.val;
  531. return this.#updateActivity(npcId).val;
  532. }
  533. /**
  534. * Returns whether the given NPC is at the given activity.
  535. *
  536. * @param {string} npcId
  537. * @param {string|string[]} activity
  538. * @param {string} [location=undefined]
  539. * @returns {boolean}
  540. */
  541. atActivity(npcId:string,activity:string|string[],location=undefined){
  542. const locationInformation = this.activity(npcId);
  543. if(!locationInformation.activity)
  544. return false;
  545. if(location && locationInformation.location != location)
  546. return false;
  547. if(Array.isArray(activity))
  548. return locationInformation.activity.includesAll(activity);
  549. return locationInformation.activity.includes(activity);
  550. }
  551. /**
  552. * Ends the current activity and starts the next one.
  553. * @param {string} npcId
  554. * @param {string} [activity=undefined]
  555. * @returns {NPCActivity}
  556. */
  557. endActivity(npcId:string,activity:string|string[]=undefined):NPCActivity{
  558. if(activity && !this.atActivity(npcId,activity))
  559. return this.activity(npcId);
  560. let activityInformationCached:ExpiringVar<NPCActivity>|null = this.get(npcId,'activity',null);
  561. if(!activityInformationCached)
  562. return this.#updateActivity(npcId).val;
  563. const expirationOfCurrentActivity = activityInformationCached.til;
  564. return (this.#updateActivity(npcId,expirationOfCurrentActivity)).val;
  565. /*const timeWhenNextActivityStarts = new Date(currentActivity.end.getTime()+1);
  566. this._updateLocation(npcId,timeWhenNextActivityStarts);
  567. return this.location(npcId);*/
  568. }
  569. /**
  570. * @deprecated
  571. * @param {*} npcId
  572. * @returns {NPCActivity}
  573. */
  574. location(npcId){
  575. return this.activity(npcId);
  576. }
  577. locationGroup(npcIds:string[]):{[locationId:string]:{[npcId:string]:NPCActivity}}{
  578. let locations:{[key:string]:any} = {};
  579. for(let npcId of npcIds){
  580. let activity = this.activity(npcId);
  581. locations[activity.location] ??= {};
  582. locations[activity.location][npcId] = activity;
  583. }
  584. return locations;
  585. }
  586. #updateActivity(npcId,dateTime:Date=undefined):ExpiringVar<NPCActivity>{
  587. let newActivity:NPCActivity = {location:'',activity:[]};
  588. let newExpiration = new Date(0);
  589. const time = State.variables.time;
  590. dateTime ??= time.now;
  591. const timeTable:Timetable<NPCActivity> = this.get(npcId,'timetable',null);
  592. const twoResultsFromTimeTable = time.timetableLookupMultiple(timeTable,dateTime,2);
  593. const currentActivity = twoResultsFromTimeTable[0];
  594. const nextActivity = twoResultsFromTimeTable[1];
  595. let result = {
  596. til: nextActivity.beginDate,
  597. val: currentActivity.val
  598. };
  599. this.set(npcId,'activity',result);
  600. return result;
  601. }
  602. //#endregion
  603. //#region System
  604. constructor(){}
  605. _init(nPCsDict){
  606. Object.keys(nPCsDict).forEach(function (pn) {
  607. this[pn] = clone(nPCsDict[pn]);
  608. }, this);
  609. return this;
  610. }
  611. clone = function () {
  612. return (new NPCsDict())._init(this);
  613. };
  614. toJSON = function () {
  615. var ownData = {};
  616. Object.keys(this).forEach(function (pn) {
  617. if(typeof this[pn] !== "function")
  618. ownData[pn] = clone(this[pn]);
  619. }, this);
  620. return JSON.reviveWrapper('(new NPCsDict())._init($ReviveData$)', ownData);
  621. };
  622. //#endregion
  623. }
  624. window.NPCsDict = NPCsDict;