storylinehandler.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. //How should storylines be formatted?
  2. /*Hook object
  3. {
  4. hook://certain spots prechecking in code
  5. //round_start
  6. //
  7. params://List of parameters set by hook
  8. //ex. mainCamp (roundstart, player is in their 'initial' camp)
  9. conditions:[condition]//
  10. {name:name of condition
  11. val,minV,maxV
  12. */
  13. const comparisons={
  14. "<":(x,y)=>x<y,
  15. '<=':(x,y)=>x<=y,
  16. '==':(x,y)=>x==y,
  17. '===':(x,y)=>x===y,
  18. '!=':(x,y)=>x!=y,
  19. '!==':(x,y)=>x!==y,
  20. '>=':(x,y)=>x>=y,
  21. '>':(x,y)=>x>y,
  22. 'isChar':function(x,y){
  23. return x.name==y;
  24. },
  25. 'notChar':function(x,y){
  26. return x.name!=y;
  27. }
  28. };
  29. const changes={
  30. "=":(p,n,v)=>p[n]=v,
  31. '+':(p,n,v)=>p[n]=p[n]?p[n]+v:v,
  32. '-':(p,n,v)=>p[n]=p[n]?p[n]-v:-v,
  33. '*':(p,n,v)=>p[n]=p[n]?p[n]*v:v,
  34. '/':(p,n,v)=>p[n]=p[n]?p[n]/v:1/v
  35. };
  36. class StoryNode{
  37. constructor(def,id,story){
  38. this.hasRun=false;
  39. this.id=id;
  40. this.story=story;
  41. this.definition=def;
  42. const commonMistakes = {
  43. condition:"conditions",
  44. effect:"effects"
  45. }
  46. for(let m in commonMistakes){
  47. if(def[m]){
  48. console.warn("Story node ",id,"has a property named",m, ". Did you mean",commonMistakes[m],"instead?");
  49. }
  50. }
  51. for(var p of Object.keys(def)){
  52. this[p]=def[p];
  53. }
  54. if(this.title){
  55. this.title=Handlebars.compile(this.title);
  56. }
  57. if(this.text){
  58. this.text=Handlebars.compile(this.text);
  59. }
  60. }
  61. static checkVariable(v,def){
  62. if((v===undefined)&&(def!==undefined)){
  63. return false;
  64. }
  65. console.log("Checking variable",v,def);
  66. if(def instanceof Object){
  67. for(c in def){
  68. if(!comparisons[c](v,def[c])){
  69. return false;
  70. }
  71. }
  72. }else{
  73. return (def===v);
  74. }
  75. return true;
  76. }
  77. static alterVariable(parent,v,def){
  78. if(def instanceof Object){
  79. for(var o in def){//done in whatever order JS gives us.
  80. changes[o](parent,v,def[o]);
  81. }
  82. }else{
  83. if(typeof parent[v] == "number"){
  84. changes['+'](parent,v,def);
  85. }else{
  86. parent[v]=def;
  87. }
  88. }
  89. }
  90. static getCharacters(){
  91. return {
  92. player:player,
  93. thoth:thoth,
  94. rival:rival,
  95. leader:leader
  96. };
  97. }
  98. deserialize(def){
  99. var barred = new Set(["definition","title","text","story"]);
  100. for(var i in def){
  101. if(!barred.has(i)){
  102. this[i]=def[i];
  103. }
  104. }
  105. }
  106. serialize(){
  107. var barred = new Set(["definition","title","text","story"]);
  108. var ret={};
  109. for(var d in this){
  110. if((typeof this[d] == "function")&&(!barred.has(d))){
  111. ret[d]=this[d];
  112. }
  113. }
  114. return ret;
  115. }
  116. checkCharacterCondition(chr,cond){
  117. console.log("Char check",chr,cond);
  118. if(chr===undefined){
  119. return false;
  120. }
  121. for(var c in cond){
  122. console.log("Character condition",c);
  123. if(c=="status"){
  124. let status = chr===leader?"leader":"follower"
  125. if(cond.status!=status){
  126. return false;
  127. }
  128. }else if(c=="camp"){
  129. if(cond.camp!=activeCamp){
  130. return false;
  131. }
  132. }else{
  133. if(!StoryNode.checkVariable(chr[c],cond[c])){
  134. return false;
  135. }
  136. }
  137. }
  138. console.log("Character condition true");
  139. return true;
  140. }
  141. checkCondition(){
  142. var ret = true;
  143. const chars=StoryNode.getCharacters();
  144. const charKeys = Object.keys(chars);
  145. console.log("Check Conditions for",this.id);
  146. console.log(chars);
  147. if(this.conditions){
  148. for(var c in this.conditions){
  149. console.log("condition",c,ret);
  150. if(charKeys.includes(c)){
  151. console.log("checking character");
  152. ret = ret&&this.checkCharacterCondition(chars[c],this.conditions[c]);
  153. }else if(c=="story"){
  154. if(this.conditions.story.current){
  155. ret = ret && storyNode.checkVariable(this.story.current,this.conditions.story.current);
  156. }
  157. if(this.conditions.story.visited){
  158. let vis = this.conditions.story.visited;
  159. if(!(vis instanceof Array)){
  160. vis=[vis];
  161. }
  162. for(let v in vis){
  163. ret = ret && this.story.history.includes(vis[v]);
  164. }
  165. }
  166. }else{//otherwise treat as the story's variable.
  167. ret = ret && storyNode.checkVariable(this.story.variables[c],this.conditions[c]);
  168. }
  169. }
  170. }
  171. console.log("Final status",ret);
  172. return ret;
  173. }
  174. _handleEffects(effects){//need to pass effects to handle choice windows later
  175. const chars = StoryNode.getCharacters();
  176. const charKeys = Object.keys(chars);
  177. console.log("Called effects",effects);
  178. for(var e in effects){
  179. if(charKeys.includes(e)){
  180. console.log("Setting variable for",e);
  181. for(var v in effects[e]){
  182. if(v==="impregnate"){
  183. if(chars[e]&&chars[effects[e][v]]){
  184. chars[e].maybeImpregnate(chars[effects[e][v]]);
  185. }
  186. }else{
  187. StoryNode.alterVariable(chars[e],v,effects[e][v]);
  188. if(StoryNode.attributes.has(v)){
  189. StoryNode.alterVariable(chars[e].natural,v,effects[e][v]);
  190. }
  191. }
  192. }
  193. }else{
  194. for(var v in effects[e]){
  195. StoryNode.alterVariable(this.story.variables,v,effects[e][v]);
  196. }
  197. }
  198. }
  199. }
  200. handleHook(hook,interrupt){
  201. if(this.checkCondition()){
  202. var node = this;
  203. var chrs = StoryNode.getCharacters();
  204. var hbContext = {...chrs, ...this.story.variables};
  205. function callBack(){
  206. node._handleEffects(node.effects)
  207. node.story.history.push(node.id);
  208. if(!interrupt){
  209. node.story.unhook(hook,node.id);
  210. }else if(this.once){
  211. node.story.unhook(hook,node.id);
  212. }
  213. if(node.next){//will change for interrupts that set this, as well.
  214. node.story.current=node.next;
  215. node.story.hook(hook,node.next);
  216. }
  217. NextWindow();
  218. }
  219. var msg="";
  220. if (this.title){
  221. msg+=`<h1>${this.title(hbContext)}</h1>`;
  222. }
  223. if(this.text){
  224. msg+=this.text(hbContext);
  225. }
  226. Message(callBack,msg,false,chrs[this.showCharacter]);
  227. return true;
  228. }else{
  229. return false;
  230. }
  231. }
  232. }
  233. StoryNode.attributes = new Set(["orientation","submissiveness","domesticity","allure","maternalism"]);
  234. Object.freeze(StoryNode.attributes);
  235. class Story{
  236. constructor(def,id,initial,hooks){
  237. this.hooks={};
  238. this.id=id;
  239. this.nodes={};
  240. this.variables={};
  241. this.interrupts={};
  242. this.current=initial;
  243. this.history=[];
  244. for(var h in hooks){
  245. this.hooks[h]={
  246. name:h,
  247. hook:hooks[h],
  248. active:(new Set())
  249. }
  250. }
  251. for(var n in def){
  252. let node = new StoryNode(def[n],n,this);
  253. this.nodes[n]=node;
  254. if(def[n].interrupt){
  255. this.interrupts[n]=node;
  256. this.hook(this.hooks[node.hook],node.id);
  257. }
  258. }
  259. if(this.nodes[initial]){
  260. //console.log(this.nodes[initial].hook,this.hooks,this.hooks[this.nodes[initial].hook],initial);
  261. this.hook(this.hooks[this.nodes[initial].hook],initial);
  262. }
  263. }
  264. serialize(){
  265. var ret={}
  266. const barred = new Set(["hooks","id","nodes","interrupts"]);
  267. for(var i in this){
  268. if(((typeof this[i] !=="function")) && (!barred.has(i))){
  269. ret[i]=this[i];
  270. }
  271. }
  272. ret.nodes={};
  273. for(var n in this.nodes){
  274. ret.nodes[n]=this.nodes[n].serialize();
  275. }
  276. ret.interrupts=[];
  277. for(var i in this.interrupts){
  278. ret.interrupts.push(this.interrupts[i].id);
  279. }
  280. ret.hooks={};
  281. for(var h in this.hooks){
  282. ret.hooks[h]=Array.from(this.hooks[h].active);
  283. }
  284. return ret;
  285. }
  286. deserialize(def){
  287. console.log("deserializing",def);
  288. const barred = new Set(["hooks","id","nodes","interrupts"]);
  289. for(var i in def){
  290. if(!barred.has(i)){
  291. this[i]=def[i];
  292. }
  293. }
  294. for(var n in def.nodes){
  295. this.nodes[n].deserialize(def.nodes[n]);
  296. }
  297. this.interrupts = def.interrupts.map( (x)=>this.nodes[x]);
  298. this.unhookall();
  299. for(var h in def.hooks){
  300. for(var n in def.hooks[h]){
  301. this.hook(this.hooks[h],def.hooks[h][n]);
  302. }
  303. }
  304. }
  305. handleHook(hookName){
  306. console.log("Handling hook ",hookName);
  307. var ranOnce=false;//if a node is marked as "sole", it will only run if it is the only node IN THIS STORYLINE to pop
  308. var currentHook = this.hooks[hookName];
  309. var hookNodes = this.hooks[hookName].active;
  310. if(hookNodes.has(this.current)){//always to "main" node first if it runs
  311. ranOnce = ranOnce || this.nodes[this.current].handleHook(currentHook);
  312. }
  313. for(var n of hookNodes){
  314. if((n != this.current) && ((!ranOnce) || (!this.nodes[n].sole))){
  315. ranOnce = ranOnce || this.nodes[n].handleHook(currentHook);
  316. }
  317. }
  318. }
  319. unhookall(){
  320. for(var h in this.hooks){
  321. this.hooks[h].hook.delete(this);
  322. this.hooks[h].active=new Set();
  323. }
  324. }
  325. hook(hook,node){
  326. hook.active.add(node);
  327. hook.hook.add(this);
  328. console.log("adding",node,hook.hook.size);
  329. }
  330. unhook(hook,node){
  331. var did = hook.active.delete(node);
  332. console.log("deleting",node.toString(),node,hook.active.size,did,hook.active);
  333. if(hook.active.size==0){
  334. hook.hook.delete(this);
  335. console.log("deleting",node,hook.hook.size);
  336. }
  337. }
  338. }
  339. var activeStories={};
  340. const storyHooks = {
  341. "round":new Set(),
  342. "gameover":new Set(),
  343. }
  344. Object.freeze(storyHooks);
  345. function loadStory(storyDef,id,initial){
  346. var story = new Story(storyDef,id,initial,storyHooks);
  347. activeStories[id]=story;
  348. }
  349. function triggerStoryHook(hookName){
  350. for(var s of storyHooks[hookName]){
  351. s.handleHook(hookName);
  352. }
  353. }