var MAP_MAX_ALTITUDE:number = 3; class PerceptionBufferRecord { constructor(action:string, subjectID:string, subjectSort:Sort, objectID:string, objectSort:Sort, objectSymbol:string, indirectID:string, indirectSort:Sort, x0:number, y0:number, x1:number, y1:number) { this.action = action; this.subjectID = subjectID; this.subjectSort = subjectSort; this.directObjectID = objectID; this.directObjectSort = objectSort; this.directObjectSymbol = objectSymbol; this.indirectObjectID = indirectID; this.indirectObjectSort = indirectSort; this.x0 = x0; this.y0 = y0; this.x1 = x1; this.y1 = y1; } action:string; subjectID:string; directObjectID:string = null; indirectObjectID:string = null; subjectSort:Sort = null; directObjectSort:Sort = null; indirectObjectSort:Sort = null; directObjectSymbol:string = null; x0:number; y0:number; x1:number; y1:number; time:number; } class PerceptionBufferObjectWarpedRecord { constructor(ID:string, sort:Sort, map:string, x0:number, y0:number, x1:number, y1:number) { this.ID = ID; this.sort = sort; this.targetMap = map; this.x0 = x0; this.y0 = y0; this.x1 = x1; this.y1 = y1; } ID:string; sort:Sort; targetMap:string; x0:number; y0:number; x1:number; y1:number; time:number; }; class A4Map { constructor(xml:Element, game:A4Game, objectsToRevisit_xml:Element[], objectsToRevisit_object:A4Object[]) { this.xml = xml; this.width = Number(xml.getAttribute("width")); this.height = Number(xml.getAttribute("height")); let properties_xml:Element = getFirstElementChildByTag(xml, "properties"); let properties_xmls:Element[] = getElementChildrenByTag(properties_xml, "property"); for(let i:number = 0;i " + gf); gfs.push(gf); } } this.tileWidth = gfs[0].tileWidth; this.tileHeight = gfs[0].tileHeight; // load tile layers: let layers_xmls:Element[] = getElementChildrenByTag(xml, "layer"); this.layers = []; for(let i:number = 0;i= 0) { console.error("Cannot find mapTile with ID: " + (Number(values[j]) - 1)); } idx++; } } } else { let tile_xmls:Element[] = getElementChildrenByTag(data_xml, "tile"); for(let j:number = 0;j= A4Object.s_nextID) A4Object.s_nextID = Number(o_ID)+1; } return null; } else if (o_class == "BridgeDestination") { let mb:A4MapBridge = new A4MapBridge(object_xml, this); mb.loadObjectAdditionalContent(object_xml, game, of, objectsToRevisit_xml, objectsToRevisit_object); this.bridgeDestinations.push(mb); if (o_ID != null) { mb.ID = o_ID; if (!isNaN(Number(o_ID)) && Number(o_ID) >= A4Object.s_nextID) A4Object.s_nextID = Number(o_ID)+1; } return null; } else if (o_class == "Trigger") { o = new A4Trigger(game.ontology.getSort("Trigger"), Number(object_xml.getAttribute("width")), Number(object_xml.getAttribute("height"))); o.loadObjectAdditionalContent(object_xml, game, of, objectsToRevisit_xml, objectsToRevisit_object); let once:boolean = true; if (object_xml.getAttribute("repeat") == "true") once = false; let scripts_xmls:Element[] = getElementChildrenByTag(object_xml, "script"); if (scripts_xmls != null && scripts_xmls.length>0) { let tmp:HTMLCollection = scripts_xmls[0].children; for(let i:number = 0;i= A4Object.s_nextID) A4Object.s_nextID = Number(o_ID)+1; } return o; } saveToXML(game:A4Game) : string { let xmlString:string = ""; xmlString += "\n"; xmlString += "\n"; xmlString += "\n"; xmlString += "\n"; xmlString += "\n"; let firstID:number = 1; for(let gf of game.graphicFiles) { xmlString += "\n"; xmlString += "\n"; xmlString += "\n"; firstID += gf.n_tiles; } // tile layers: for(let i:number = 0;i\n"; xmlString += "\n"; xmlString += "\n"; xmlString += "\n"; /* xmlString += "\n"; for(let y:number = 0;y\n"; } } */ // xmlString += "\n"; for(let y:number = 0;y\n"; for(let b of this.bridges) xmlString += b.saveToXML(game,1,true) + "\n"; for(let b of this.bridgeDestinations) xmlString += b.saveToXML(game,2,true) + "\n"; for(let o of this.objects) { if (!o.isPlayer()) xmlString += o.saveToXML(game,0,true) + "\n"; } /* for(let i:number = 0;i\n"; } if (onStarttagOpen) xmlString += "\n"; // each execution queue goes to its own "onStart" block: for(let seq of this.scriptQueues) { xmlString += "\n"; for(let s of seq.scripts) xmlString += s.saveToXML() + "\n"; xmlString += "\n"; } // rules: for(let i:number = 0;i (row+1)*this.tileHeight) break; let tx:number = Math.floor(o.x/this.tileWidth); let ty:number = Math.floor(o.y/this.tileHeight); let draw:boolean = false; let dark:boolean = true; for(let i:number = 0;i=0 && xx=0 && yy0 && this.visibilityRegions[offset-1]==visibilityRegion) || (xx0 && this.visibilityRegions[offset-this.width]==visibilityRegion) || (yy0 && yy>0 && this.visibilityRegions[offset-(1+this.width)]==visibilityRegion) || (xx>0 && yy0 && this.visibilityRegions[offset+1-this.width]==visibilityRegion) || (xx=0 && tx+j=0 && ty+io).drawTextBubbles(-offsetx,-offsety, SCREEN_X/zoom, SCREEN_Y/zoom, game); } let y:number = 0; for(let sb of this.textBubbles) { sb[0].drawNoArrow(Math.floor(SCREEN_X/zoom/2) - (sb[0].width)/2, y, false, 1); y += sb[0].height; } ctx.restore(); } drawTextBubblesRegion(offsetx:number, offsety:number, zoom:number, SCREEN_X:number, SCREEN_Y:number, visibilityRegion:number, game:A4Game) { ctx.save(); ctx.scale(zoom, zoom); let xx:number; let yy:number; let offset:number; for(let o of this.objects) { if (o.burrowed) continue; if (!o.isCharacter()) continue; let tx:number = Math.floor(o.x/this.tileWidth); let ty:number = Math.floor(o.y/this.tileHeight); let draw:boolean = false; for(let i:number = 0;i=0 && xx=0 && yy0 && this.visibilityRegions[offset-1]==visibilityRegion) || (xx0 && this.visibilityRegions[offset-this.width]==visibilityRegion) || (yy0 && yy>0 && this.visibilityRegions[offset-(1+this.width)]==visibilityRegion) || (xx>0 && yy0 && this.visibilityRegions[offset+1-this.width]==visibilityRegion) || (xxo).drawTextBubbles(-offsetx,-offsety, SCREEN_X/zoom, SCREEN_Y/zoom, game); } let y:number = 0; for(let sb of this.textBubbles) { sb[0].drawNoArrow(Math.floor(SCREEN_X/zoom/2) - (sb[0].width)/2, y, false, 1); y += sb[0].height; } ctx.restore(); } getNeighborMaps() : A4Map[] { let l:A4Map[] = []; for(let mb of this.bridges) { if (mb.linkedTo != null) { if (l.indexOf(mb.linkedTo.map)==-1) { l.push(mb.linkedTo.map); } } } return l; } executeScriptQueues(game:A4Game) { let toDelete:A4ScriptExecutionQueue[] = []; for(let seb of this.scriptQueues) { while(true) { let s:A4Script = seb.scripts[0]; let retval:number = s.execute(seb.object, (seb.map == null ? this:seb.map), (seb.game == null ? game:seb.game), seb.otherCharacter); if (retval==SCRIPT_FINISHED) { seb.scripts.splice(0,1); if (seb.scripts.length == 0) { toDelete.push(seb); break; } } else if (retval==SCRIPT_NOT_FINISHED) { break; } else if (retval==SCRIPT_FAILED) { toDelete.push(seb); break; } } } for(let seb of toDelete) { let idx:number = this.scriptQueues.indexOf(seb); this.scriptQueues.splice(idx, 1); } } addScriptQueue(seq: A4ScriptExecutionQueue) { this.scriptQueues.push(seq); } setStoryStateVariable(variable:string, value:string, game:A4Game) { this.storyState[variable] = value; this.lastTimeStoryStateChanged = game.cycle; } getStoryStateVariable(variable:string) : string { return this.storyState[variable]; } removeObject(o:A4Object) : boolean { let idx:number = this.objects.indexOf(o); if (idx>=0) { this.objects.splice(idx, 1); return true; } return false; } addObject(o:A4Object)//, layer:number) { this.objects.push(o); o.map = this; } contains(o:A4Object) : boolean { if (this.objects.indexOf(o)!=-1) return true; return false; } // This function returns a list with the hierarchy of objects necessary to find the desired object // For example, if an object is directly in a map, the list with be length 1, but if // the obeject is in the inventory of a character, then we will get a list with the character and then the object findObjectByName(name:string) : A4Object[] { for(let o of this.objects) { if (o.name == name) return [o]; let o2:A4Object[] = o.findObjectByName(name); if (o2!=null) return [o].concat(o2); } return null; } // This function returns a list with the hierarchy of objects necessary to find the desired object // For example, if an object is directly in a map, the list with be length 1, but if // the obeject is in the inventory of a character, then we will get a list with the character and then the object findObjectByID(ID:string) : A4Object[] { for(let o of this.objects) { if (o.ID == ID) return [o]; let o2:A4Object[] = o.findObjectByID(ID); if (o2!=null) return [o].concat(o2); } return null; } objectRemoved(o:A4Object) { for(let o2 of this.objects) { o2.objectRemoved(o); } } checkIfDoorGroupStateCanBeChanged(doorGroup:string, state:boolean, character:A4Character, map:A4Map, game:A4Game) { for(let o of this.objects) { if (o.isDoor()) { let d:A4Door = o; if (d.doorGroupID == doorGroup) { if (!d.checkForBlockages(state, character, map, game, [])) return false; } } } return true; } setDoorGroupState(doorGroup:string, state:boolean, character:A4Character, map:A4Map, game:A4Game) { for(let o of this.objects) { if (o.isDoor()) { let d:A4Door = o; if (d.doorGroupID == doorGroup) { d.changeStateRecursively(state, character, map, game); } } } } getTileWidth() : number { return this.layers[0].tileWidth; } getTileHeight() : number { return this.layers[0].tileHeight; } addPerceptionBufferRecord(pbr:PerceptionBufferRecord) { pbr.time = this.cycle; this.perceptionBuffer.push(pbr); } addPerceptionBufferObjectWarpedRecord(pbr:PerceptionBufferObjectWarpedRecord) { pbr.time = this.cycle; this.warpPerceptionBuffer.push(pbr); } walkableOnlyBackground(x:number, y:number, dx:number, dy:number, subject:A4Object) : boolean { for(let i:number = 0;io).isEmpty()) continue; // } if (!o.isWalkable()) { if (o.collision(x,y,dx,dy)) return false; } } } return true; } walkableOnlyObjectsIgnoringObject(x:number, y:number, dx:number, dy:number, subject:A4Object, toIgnore:A4Object) : boolean { for(let o of this.objects) { if (o == toIgnore) continue; if (o != subject) { // if (subject.isCharacter() && o.isVehicle()) { // // characters can always walk onto empty vehicles: // if ((o).isEmpty()) continue; // } if (!o.isWalkable()) { if (o.collision(x,y,dx,dy)) return false; } } } return true; } walkable(x:number, y:number, dx:number, dy:number, subject:A4Object) : boolean { if (!this.walkableOnlyBackground(x, y, dx, dy, subject)) return false; return this.walkableOnlyObjects(x, y, dx, dy, subject); } walkableIgnoringObject(x:number, y:number, dx:number, dy:number, subject:A4Object, toIgnore:A4Object) : boolean { if (!this.walkableOnlyBackground(x,y,dx,dy,subject)) return false; return this.walkableOnlyObjectsIgnoringObject(x,y, dx, dy, subject, toIgnore); } walkableConsideringVehicles(x:number, y:number, dx:number, dy:number, subject:A4Object) : boolean { let rettiles:boolean = true; let retobjects:boolean = true; for(let i:number = 0;i x && b.y < y && b.y+b.height > y) { return b; } } return null; } getTakeableObject(x:number, y:number, dx:number, dy:number) : A4Object { for(let o of this.objects) { if (o.takeable && !o.burrowed && o.collision(x,y,dx,dy)) return o; } return null; } getBurrowedObject(x:number, y:number, dx:number, dy:number) : A4Object { for(let o of this.objects) { if (o.burrowed && o.collision(x,y,dx,dy)) return o; } return null; } getUsableObject(x:number, y:number, dx:number, dy:number) : A4Object { for(let o of this.objects) { if (o.usable && !o.burrowed && o.collision(x,y,dx,dy)) return o; } return null; } getVehicleObject(x:number, y:number, dx:number, dy:number) : A4Object { for(let o of this.objects) { if (o.isVehicle() && o.collision(x,y,dx,dy)) return o; } return null; } getAllObjectCollisions(o:A4Object) : A4Object[] { return this.getAllObjectCollisionsWithOffset(o, 0, 0); } getAllObjectCollisionsWithOffset(o:A4Object, xoffs:number, yoffs:number) : A4Object[] { let l:A4Object[] = []; for(let o2 of this.objects) { if (o2!=o && o.collisionObjectOffset(xoffs, yoffs, o2)) l.push(o2); } return l; } getAllObjectCollisionsOnlyWithOffset(o:A4Object, xoffs:number, yoffs:number) : A4Object[] { let l:A4Object[] = []; for(let o2 of this.objects) { if (o2!=o && o.collisionObjectOffset(xoffs, yoffs, o2) && !o.collisionObjectOffset(0, 0, o2)) l.push(o2); } return l; } getAllObjects(x:number, y:number, dx:number, dy:number) : A4Object[] { let l:A4Object[] = []; for(let o of this.objects) { if (o.collision(x,y,dx,dy)) l.push(o); } return l; } getAllObjectsInRegion(x:number, y:number, dx:number, dy:number, region:number) : A4Object[] { let l:A4Object[] = []; for(let o of this.objects) { if (o.collision(x,y,dx,dy)) { let tx:number = Math.floor(o.x/this.tileWidth); let ty:number = Math.floor(o.y/this.tileHeight); let region2:number = this.visibilityRegion(tx,ty); if (region == region2) l.push(o); } } return l; } getAllObjectsInRegionPlusDoorsAndObstacles(x:number, y:number, dx:number, dy:number, region:number) : A4Object[] { let l:A4Object[] = []; for(let o of this.objects) { if (o.collision(x,y,dx,dy)) { let tx:number = Math.floor(o.x/this.tileWidth); let ty:number = Math.floor(o.y/this.tileHeight); let region2:number = this.visibilityRegion(tx,ty); if (region == region2 || (o instanceof A4Door) || (o instanceof A4Obstacle) || (o instanceof A4ObstacleContainer) || (o instanceof A4PushableWall)) l.push(o); } } return l; } triggerObjectsEvent(event:number, otherCharacter:A4Character, map:A4Map, game:A4Game) { for(let o of this.objects) { o.event(event,otherCharacter,map,game); } } triggerObjectsEventWithID(event:number, ID:string, otherCharacter:A4Character, map:A4Map, game:A4Game) { for(let o of this.objects) { o.eventWithID(event,ID,otherCharacter,map,game); } } reevaluateVisibilityRequest() { this.visibilityReevaluationRequested = true; } reevaluateVisibility() { let x:number; let y:number; let nextRegion:number = 1; let inOpen:boolean[] = new Array(this.width * this.height); for(let i:number = 0;i0) { let tmp:number = stack[0]; stack.splice(0,1); x = tmp%this.width; y = Math.floor(tmp/this.width); if (this.seeThrough(x,y)) { this.visibilityRegions[tmp] = nextRegion; if (x>0 && this.visibilityRegions[x+y*this.width-1]==0 && !inOpen[x+y*this.width-1]) { stack.push(x+y*this.width-1); inOpen[x+y*this.width-1] = true; } if (y>0 && this.visibilityRegions[x+(y-1)*this.width]==0 && !inOpen[x+(y-1)*this.width]) { stack.push(x+(y-1)*this.width); inOpen[x+(y-1)*this.width] = true; } if (x<(this.width-1) && this.visibilityRegions[x+y*this.width+1]==0 && !inOpen[x+y*this.width+1]) { stack.push(x+y*this.width+1); inOpen[x+y*this.width+1] = true; } if (y<(this.height-1) && this.visibilityRegions[x+(y+1)*this.width]==0 && !inOpen[x+(y+1)*this.width]) { stack.push(x+(y+1)*this.width); inOpen[x+(y+1)*this.width] = true; } if (x>0 && y>0 && this.visibilityRegions[x+(y-1)*this.width-1]==0 && !inOpen[x+(y-1)*this.width-1]) { stack.push(x+(y-1)*this.width-1); inOpen[x+(y-1)*this.width-1] = true; } if (x<(this.width-1) && y>0 && this.visibilityRegions[x+(y-1)*this.width+1]==0 && !inOpen[x+(y-1)*this.width+1]) { stack.push(x+(y-1)*this.width+1); inOpen[x+(y-1)*this.width+1] = true; } if (x>0 && y<(this.height-1) && this.visibilityRegions[x+(y+1)*this.width-1]==0 && !inOpen[x+(y+1)*this.width-1]) { stack.push(x+(y+1)*this.width-1); inOpen[x+(y+1)*this.width-1] = true; } if (x<(this.width-1) && y<(this.height-1) && this.visibilityRegions[x+(y+1)*this.width+1]==0 && !inOpen[x+(y+1)*this.width+1]) { stack.push(x+(y+1)*this.width+1); inOpen[x+(y+1)*this.width+1] = true; } } } nextRegion++; } } } //console.log("Regions: " + this.visibilityRegions); /* console.log("ReevaluateVisibility called on " + this.name); let debugstr:string = ""; for(let i:number = 0;i=(tilex+1)*this.tileWidth && o.y<=tiley*this.tileHeight && o.y+o.getPixelHeight()>=(tiley+1)*this.tileHeight) { if (!o.seeThrough()) return false; } } return true; } visible(tilex:number, tiley:number, region:number) : boolean { return this.visibilityRegions[tilex + tiley*this.width] == region; } visibilityRegion(tilex:number, tiley:number) : number { return this.visibilityRegions[tilex + tiley*this.width]; } // By default, all the lights are on, but this function can be used to make any region of the map // dark. The regionsWithLights/regionsWithLightsOn variables should be handled by each specific // game, and are used to tell the engine which parts of the maps are lit and which are not. // By default, if a region is not in the "regionsWithLights" list, it is assumed it is lit. recalculateLightsOnStatus(regionsWithLights:string[], regionsWithLightsOn:string[], regionNames:string[]) { for(let i:number = 0;i yi2 || // A4Characters have preference over other objects: (yi1 == yi2 && this.objects[i] instanceof A4Character && !(this.objects[i+1] instanceof A4Character))) { tmp = this.objects[i]; this.objects[i] = this.objects[i+1]; this.objects[i+1] = tmp; change = true; } } // going down: if (change) { change = false; for(let i:number = this.objects.length-2;i>=0;i--) { let yi1:number = this.objects[i].y + this.objects[i].getPixelHeight(); let yi2:number = this.objects[i+1].y + this.objects[i+1].getPixelHeight(); if (yi1 > yi2 || // A4Characters have preference over other objects: (yi1 == yi2 && this.objects[i] instanceof A4Character && !(this.objects[i+1] instanceof A4Character))) { tmp = this.objects[i]; this.objects[i] = this.objects[i+1]; this.objects[i+1] = tmp; change = true; } } } } } xml:Element = null; name:string = null; width:number; height:number; tileWidth:number = 8; tileHeight:number = 8; pixelsPerMeter:number = 8; layers:A4MapLayer[] = []; bridges:A4MapBridge[] = []; bridgeDestinations:A4MapBridge[] = []; objects:A4Object[] = []; cycle:number = 0; visibilityReevaluationRequested:boolean = true; visibilityRegions:number[] = null; lightOnStatus:number[] = null; textBubbles:[A4TextBubble,number][] = []; // the second number is the timer // scripts: eventScripts:A4EventRule[][] = new Array(A4_NEVENTS); // script excution queues (these contain scripts that are pending execution, will be executed in the next "cycle"): scriptQueues: A4ScriptExecutionQueue[] = []; // story state: storyState:{ [id: string] : string; } = {}; lastTimeStoryStateChanged:number = 0; // perception buffers: perceptionBuffer:PerceptionBufferRecord[] = []; warpPerceptionBuffer:PerceptionBufferObjectWarpedRecord[] = []; }