NPCsDict.ts 22 KB

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