123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- //How should storylines be formatted?
- /*Hook object
- {
- hook://certain spots prechecking in code
- //round_start
- //
- params://List of parameters set by hook
- //ex. mainCamp (roundstart, player is in their 'initial' camp)
- conditions:[condition]//
- {name:name of condition
- val,minV,maxV
- */
- const comparisons={
- "<":(x,y)=>x<y,
- '<=':(x,y)=>x<=y,
- '==':(x,y)=>x==y,
- '===':(x,y)=>x===y,
- '!=':(x,y)=>x!=y,
- '!==':(x,y)=>x!==y,
- '>=':(x,y)=>x>=y,
- '>':(x,y)=>x>y,
- 'isChar':function(x,y){
- return x.name==y;
- },
- 'notChar':function(x,y){
- return x.name!=y;
- }
- };
- const changes={
- "=":(p,n,v)=>p[n]=v,
- '+':(p,n,v)=>p[n]=p[n]?p[n]+v:v,
- '-':(p,n,v)=>p[n]=p[n]?p[n]-v:-v,
- '*':(p,n,v)=>p[n]=p[n]?p[n]*v:v,
- '/':(p,n,v)=>p[n]=p[n]?p[n]/v:1/v
- };
- class StoryNode{
- constructor(def,id,story){
- this.hasRun=false;
- this.id=id;
- this.story=story;
- this.definition=def;
- const commonMistakes = {
- condition:"conditions",
- effect:"effects"
- }
- for(let m in commonMistakes){
- if(def[m]){
- console.warn("Story node ",id,"has a property named",m, ". Did you mean",commonMistakes[m],"instead?");
- }
- }
- for(var p of Object.keys(def)){
- this[p]=def[p];
- }
- if(this.title){
- this.title=Handlebars.compile(this.title);
- }
- if(this.text){
- this.text=Handlebars.compile(this.text);
- }
- }
- static checkVariable(v,def){
- if((v===undefined)&&(def!==undefined)){
- return false;
- }
- console.log("Checking variable",v,def);
- if(def instanceof Object){
- for(c in def){
- if(!comparisons[c](v,def[c])){
- return false;
- }
- }
- }else{
- return (def===v);
- }
- return true;
- }
- static alterVariable(parent,v,def){
- if(def instanceof Object){
- for(var o in def){//done in whatever order JS gives us.
- changes[o](parent,v,def[o]);
- }
- }else{
- if(typeof parent[v] == "number"){
- changes['+'](parent,v,def);
- }else{
- parent[v]=def;
- }
- }
- }
- static getCharacters(){
- return {
- player:player,
- thoth:thoth,
- rival:rival,
- leader:leader
- };
- }
- deserialize(def){
- var barred = new Set(["definition","title","text","story"]);
- for(var i in def){
- if(!barred.has(i)){
- this[i]=def[i];
- }
- }
- }
- serialize(){
- var barred = new Set(["definition","title","text","story"]);
- var ret={};
- for(var d in this){
- if((typeof this[d] == "function")&&(!barred.has(d))){
- ret[d]=this[d];
- }
- }
- return ret;
- }
- checkCharacterCondition(chr,cond){
- console.log("Char check",chr,cond);
- if(chr===undefined){
- return false;
- }
- for(var c in cond){
- console.log("Character condition",c);
- if(c=="status"){
- let status = chr===leader?"leader":"follower"
- if(cond.status!=status){
- return false;
- }
- }else if(c=="camp"){
- if(cond.camp!=activeCamp){
- return false;
- }
- }else{
- if(!StoryNode.checkVariable(chr[c],cond[c])){
- return false;
- }
- }
- }
- console.log("Character condition true");
- return true;
- }
- checkCondition(){
- var ret = true;
- const chars=StoryNode.getCharacters();
- const charKeys = Object.keys(chars);
- console.log("Check Conditions for",this.id);
- console.log(chars);
- if(this.conditions){
- for(var c in this.conditions){
- console.log("condition",c,ret);
- if(charKeys.includes(c)){
- console.log("checking character");
- ret = ret&&this.checkCharacterCondition(chars[c],this.conditions[c]);
- }else if(c=="story"){
- if(this.conditions.story.current){
- ret = ret && storyNode.checkVariable(this.story.current,this.conditions.story.current);
- }
- if(this.conditions.story.visited){
- let vis = this.conditions.story.visited;
- if(!(vis instanceof Array)){
- vis=[vis];
- }
- for(let v in vis){
- ret = ret && this.story.history.includes(vis[v]);
- }
- }
- }else{//otherwise treat as the story's variable.
- ret = ret && storyNode.checkVariable(this.story.variables[c],this.conditions[c]);
- }
- }
- }
- console.log("Final status",ret);
- return ret;
- }
- _handleEffects(effects){//need to pass effects to handle choice windows later
- const chars = StoryNode.getCharacters();
- const charKeys = Object.keys(chars);
- console.log("Called effects",effects);
- for(var e in effects){
- if(charKeys.includes(e)){
- console.log("Setting variable for",e);
- for(var v in effects[e]){
- if(v==="impregnate"){
- if(chars[e]&&chars[effects[e][v]]){
- chars[e].maybeImpregnate(chars[effects[e][v]]);
- }
- }else{
- StoryNode.alterVariable(chars[e],v,effects[e][v]);
- if(StoryNode.attributes.has(v)){
- StoryNode.alterVariable(chars[e].natural,v,effects[e][v]);
- }
- }
- }
- }else{
- for(var v in effects[e]){
- StoryNode.alterVariable(this.story.variables,v,effects[e][v]);
- }
- }
- }
- }
- handleHook(hook,interrupt){
- if(this.checkCondition()){
- var node = this;
- var chrs = StoryNode.getCharacters();
- var hbContext = {...chrs, ...this.story.variables};
- function callBack(){
- node._handleEffects(node.effects)
- node.story.history.push(node.id);
- if(!interrupt){
- node.story.unhook(hook,node.id);
- }else if(this.once){
- node.story.unhook(hook,node.id);
- }
- if(node.next){//will change for interrupts that set this, as well.
- node.story.current=node.next;
- node.story.hook(hook,node.next);
- }
- NextWindow();
- }
- var msg="";
- if (this.title){
- msg+=`<h1>${this.title(hbContext)}</h1>`;
- }
- if(this.text){
- msg+=this.text(hbContext);
- }
- Message(callBack,msg,false,chrs[this.showCharacter]);
- return true;
- }else{
- return false;
- }
- }
- }
- StoryNode.attributes = new Set(["orientation","submissiveness","domesticity","allure","maternalism"]);
- Object.freeze(StoryNode.attributes);
- class Story{
- constructor(def,id,initial,hooks){
- this.hooks={};
- this.id=id;
- this.nodes={};
- this.variables={};
- this.interrupts={};
- this.current=initial;
- this.history=[];
- for(var h in hooks){
- this.hooks[h]={
- name:h,
- hook:hooks[h],
- active:(new Set())
- }
- }
- for(var n in def){
- let node = new StoryNode(def[n],n,this);
- this.nodes[n]=node;
- if(def[n].interrupt){
- this.interrupts[n]=node;
- this.hook(this.hooks[node.hook],node.id);
- }
- }
- if(this.nodes[initial]){
- //console.log(this.nodes[initial].hook,this.hooks,this.hooks[this.nodes[initial].hook],initial);
- this.hook(this.hooks[this.nodes[initial].hook],initial);
- }
- }
- serialize(){
- var ret={}
- const barred = new Set(["hooks","id","nodes","interrupts"]);
- for(var i in this){
- if(((typeof this[i] !=="function")) && (!barred.has(i))){
- ret[i]=this[i];
- }
- }
- ret.nodes={};
- for(var n in this.nodes){
- ret.nodes[n]=this.nodes[n].serialize();
- }
- ret.interrupts=[];
- for(var i in this.interrupts){
- ret.interrupts.push(this.interrupts[i].id);
- }
- ret.hooks={};
- for(var h in this.hooks){
- ret.hooks[h]=Array.from(this.hooks[h].active);
- }
- return ret;
- }
- deserialize(def){
- console.log("deserializing",def);
- const barred = new Set(["hooks","id","nodes","interrupts"]);
- for(var i in def){
- if(!barred.has(i)){
- this[i]=def[i];
- }
- }
- for(var n in def.nodes){
- this.nodes[n].deserialize(def.nodes[n]);
- }
- this.interrupts = def.interrupts.map( (x)=>this.nodes[x]);
- this.unhookall();
- for(var h in def.hooks){
- for(var n in def.hooks[h]){
- this.hook(this.hooks[h],def.hooks[h][n]);
- }
- }
- }
- handleHook(hookName){
- console.log("Handling hook ",hookName);
- 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
- var currentHook = this.hooks[hookName];
- var hookNodes = this.hooks[hookName].active;
- if(hookNodes.has(this.current)){//always to "main" node first if it runs
- ranOnce = ranOnce || this.nodes[this.current].handleHook(currentHook);
- }
- for(var n of hookNodes){
- if((n != this.current) && ((!ranOnce) || (!this.nodes[n].sole))){
- ranOnce = ranOnce || this.nodes[n].handleHook(currentHook);
- }
- }
- }
- unhookall(){
- for(var h in this.hooks){
- this.hooks[h].hook.delete(this);
- this.hooks[h].active=new Set();
- }
- }
- hook(hook,node){
- hook.active.add(node);
- hook.hook.add(this);
- console.log("adding",node,hook.hook.size);
- }
- unhook(hook,node){
- var did = hook.active.delete(node);
- console.log("deleting",node.toString(),node,hook.active.size,did,hook.active);
- if(hook.active.size==0){
- hook.hook.delete(this);
- console.log("deleting",node,hook.hook.size);
- }
- }
- }
- var activeStories={};
- const storyHooks = {
- "round":new Set(),
- "gameover":new Set(),
- }
- Object.freeze(storyHooks);
- function loadStory(storyDef,id,initial){
- var story = new Story(storyDef,id,initial,storyHooks);
- activeStories[id]=story;
- }
- function triggerStoryHook(hookName){
- for(var s of storyHooks[hookName]){
- s.handleHook(hookName);
- }
- }
|