A Pen by Lovro Selic on CodePen.
Created
October 17, 2019 13:42
-
-
Save lovroselic/6ea73654fd73d782e4d8c7faedf97d83 to your computer and use it in GitHub Desktop.
Engine LS v2.20.A
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //////////////////engine.js///////////////////////// | |
| // // | |
| // ENGINE version 2.20 by LS // | |
| // // | |
| // // | |
| //////////////////////////////////////////////////// | |
| //COUPLED | |
| //MAP | |
| //GAME.level | |
| //AI | |
| var ENGINE = { | |
| VERSION: "2.20.A", | |
| CSS: "color: #0FA", | |
| INI: { | |
| ANIMATION_INTERVAL: 17, | |
| SPRITESHEET_HEIGHT: 48, | |
| SPRITESHEET_DEFAULT_WIDTH: 48, | |
| SPRITESHEET_DEFAULT_HEIGHT: 48, | |
| sprite_maxW: 300, | |
| sprite_maxH: 100, | |
| GRIDPIX: 48, | |
| FADE_FRAMES: 50, | |
| COLLISION_SAFE: 48, | |
| PATH_ROUNDS: 999, | |
| MAX_PATH: 999 | |
| }, | |
| readyCall: null, | |
| SOURCE: "https://www.c00lsch00l.eu/Games/AA/", | |
| WASM_SOURCE: "https://www.c00lsch00l.eu/WASM/", | |
| AUDIO_SOURCE: "https://www.c00lsch00l.eu/Mp3/", | |
| FONT_SOURCE: "https://www.c00lsch00l.eu/Fonts/", | |
| checkIntersection: false, //use linear intersection collision method after pixelperfect collision; set to false to exclude | |
| checkProximity: true, //check proximity before pixel perfect evaluation of collision to background | |
| pixelPerfectCollision: false, //false by default | |
| LOAD_W: 160, | |
| LOAD_H: 22, | |
| gameWindowId: "#game", | |
| gameWIDTH: 960, | |
| gameHEIGHT: 768, | |
| sideWIDTH: 960, | |
| sideHEIGHT: 768, | |
| titleHEIGHT: 120, | |
| titleWIDTH: 960, | |
| bottomHEIGHT: 40, | |
| bottomWIDTH: 960, | |
| mapWIDTH: 512, | |
| statusWIDTH: 312, | |
| currentTOP: 0, | |
| currentLEFT: 0, | |
| directions: [LEFT, UP, RIGHT, DOWN], | |
| corners: [ | |
| new Vector(-1, -1), | |
| new Vector(1, -1), | |
| new Vector(-1, 1), | |
| new Vector(1, 1) | |
| ], | |
| layersToClear: new Set(), | |
| disableKey: function(key) { | |
| $(document).keydown(function(event) { | |
| if (event.which === ENGINE.KEY.map[key]) { | |
| event.preventDefault(); | |
| } | |
| }); | |
| $(document).keyup(function(event) { | |
| if (event.which === ENGINE.KEY.map[key]) { | |
| event.preventDefault(); | |
| } | |
| }); | |
| $(document).keypress(function(event) { | |
| if (event.which === ENGINE.KEY.map[key]) { | |
| event.preventDefault(); | |
| } | |
| }); | |
| }, | |
| disableArrows: function() { | |
| ENGINE.disableKey("up"); | |
| ENGINE.disableKey("down"); | |
| }, | |
| init: function() { | |
| console.log(`%cInitializing ENGINE V${String(ENGINE.VERSION)}`, ENGINE.CSS); | |
| $("#temp").append( | |
| "<canvas id ='temp_canvas' width='" + | |
| ENGINE.INI.sprite_maxW + | |
| "' height='" + | |
| ENGINE.INI.sprite_maxH + | |
| "'></canvas>" | |
| ); | |
| $("#temp2").append( | |
| "<canvas id ='temp_canvas2' width='" + | |
| ENGINE.INI.sprite_maxW + | |
| "' height='" + | |
| ENGINE.INI.sprite_maxH + | |
| "'></canvas>" | |
| ); | |
| LAYER.temp = $("#temp_canvas")[0].getContext("2d"); | |
| LAYER.temp2 = $("#temp_canvas2")[0].getContext("2d"); | |
| VIEW.init(); | |
| }, | |
| fill: function(ctx, pattern) { | |
| let CTX = ctx; | |
| let pat = CTX.createPattern(pattern, "repeat"); | |
| CTX.fillStyle = pat; | |
| CTX.fillRect(0, 0, CTX.canvas.width, CTX.canvas.height); | |
| }, | |
| clearLayer: function(layer) { | |
| let CTX = LAYER[layer]; | |
| CTX.clearRect(0, 0, CTX.canvas.width, CTX.canvas.height); | |
| }, | |
| clearLayerStack: function() { | |
| let CLR = ENGINE.layersToClear.length; | |
| if (CLR === 0) return; | |
| ENGINE.layersToClear.forEach(ENGINE.clearLayer); | |
| ENGINE.layersToClear.clear(); | |
| }, | |
| fillLayer: function(layer, colour) { | |
| let CTX = LAYER[layer]; | |
| CTX.fillStyle = colour; | |
| CTX.fillRect(0, 0, CTX.canvas.width, CTX.canvas.height); | |
| }, | |
| resizeBOX: function(id, width, height) { | |
| width = width || ENGINE.gameWIDTH; | |
| height = height || ENGINE.gameHEIGHT; | |
| let box = $("#" + id).children(); | |
| for (let a = 0; a < box.length; a++) { | |
| box[a].width = width; | |
| box[a].height = height; | |
| } | |
| }, | |
| addBOX: function(id, width, height, alias, type) { | |
| //types null, side, fside | |
| if (id === null) return; | |
| if (width === null) return; | |
| if (height === null) return; | |
| let layers = alias.length; | |
| $(ENGINE.gameWindowId).append( | |
| `<div id ='${id}' style='position: relative'></div>` | |
| ); | |
| if (type === "side" || type === "fside") { | |
| $(`#${id}`).addClass("gw"); //adds gw class: side by side windows | |
| } else { | |
| $(`#${id}`).addClass("gh"); //adds gh class: windows below | |
| } | |
| let prop; | |
| let canvasElement; | |
| for (let x = 0; x < layers; x++) { | |
| canvasElement = `<canvas class='layer' id='${id}_canvas_${x}' width='${width}' height='${height}' style='z-index:${x}; top:${ | |
| ENGINE.currentTOP | |
| }px; left:${ENGINE.currentLEFT}px'></canvas>`; | |
| $(`#${id}`).append(canvasElement); | |
| prop = alias.shift(); | |
| LAYER[prop] = $(`#${id}_canvas_${x}`)[0].getContext("2d"); | |
| } | |
| if (type === "side") { | |
| ENGINE.currentLEFT += width; | |
| } else if (type === "fside") { | |
| ENGINE.currentTOP += height; | |
| ENGINE.currentLEFT = 0; | |
| } else { | |
| ENGINE.currentTOP += height; | |
| ENGINE.currentLEFT = 0; | |
| } | |
| }, | |
| addCanvas: function(id, w, h) { | |
| //adds canvas to div | |
| let canvas = `<canvas id="c_${id}" width="${w}" height="${h}"></canvas>`; | |
| $(`#${id}`).append(canvas); | |
| LAYER[id] = $(`#c_${id}`)[0].getContext("2d"); | |
| }, | |
| copyLayer: function(copyFrom, copyTo, orX, orY, orW, orH) { | |
| let CTX = LAYER[copyTo]; | |
| CTX.drawImage(LAYER[copyFrom].canvas, orX, orY, orW, orH, 0, 0, orW, orH); | |
| }, | |
| flattenLayers: function(src, dest) { | |
| let W = LAYER[dest].canvas.width; | |
| let H = LAYER[dest].canvas.height; | |
| ENGINE.copyLayer(src, dest, 0, 0, W, H, 0, 0, W, H); | |
| }, | |
| spriteDraw: function(layer, X, Y, image) { | |
| let CX = Math.floor(X - image.width / 2); | |
| let CY = Math.floor(Y - image.height / 2); | |
| let CTX = LAYER[layer]; | |
| CTX.drawImage(image, CX, CY); | |
| }, | |
| drawToGrid: function(layer, grid, image) { | |
| let p = GRID.gridToCoord(grid); | |
| ENGINE.draw(layer, p.x, p.y, image); | |
| }, | |
| spriteToGrid: function(layer, grid, image) { | |
| let p = GRID.gridToCenterPX(grid); | |
| ENGINE.spriteDraw(layer, p.x, p.y, image); | |
| }, | |
| draw: function(layer, X, Y, image) { | |
| let CTX = LAYER[layer]; | |
| CTX.drawImage(image, X, Y); | |
| }, | |
| drawPart: function(layer, X, Y, image, line) { | |
| let CTX = LAYER[layer]; | |
| CTX.drawImage( | |
| image, | |
| 0, | |
| line, | |
| image.width, | |
| image.height - line, | |
| X, | |
| Y, | |
| image.width, | |
| image.height - line | |
| ); | |
| }, | |
| drawPool: function(layer, pool, sprite) { | |
| let CTX = LAYER[layer]; | |
| let PL = pool.length; | |
| if (PL === 0) return; | |
| for (let i = 0; i < PL; i++) { | |
| ENGINE.spriteDraw(layer, pool[i].x, pool[i].y, sprite); | |
| } | |
| }, | |
| trimCanvas: function(data) { | |
| var top = 0, | |
| bottom = data.height, | |
| left = 0, | |
| right = data.width; | |
| var width = data.width; | |
| while (top < bottom && rowBlank(data, width, top)) ++top; | |
| while (bottom - 1 > top && rowBlank(data, width, bottom - 1)) --bottom; | |
| while (left < right && columnBlank(data, width, left, top, bottom)) ++left; | |
| while (right - 1 > left && columnBlank(data, width, right - 1, top, bottom)) | |
| --right; | |
| return { left: left, top: top, right: right, bottom: bottom }; | |
| function rowBlank(data, width, y) { | |
| for (let x = 0; x < width; ++x) { | |
| if (data.data[y * width * 4 + x * 4 + 3] !== 0) return false; | |
| } | |
| return true; | |
| } | |
| function columnBlank(data, width, x, top, bottom) { | |
| for (let y = top; y < bottom; ++y) { | |
| if (data.data[y * width * 4 + x * 4 + 3] !== 0) return false; | |
| } | |
| return true; | |
| } | |
| }, | |
| rotateImage: function(image, degree, newName) { | |
| let CTX = LAYER.temp; | |
| let CW = image.width; | |
| let CH = image.height; | |
| let max = Math.max(CW, CH); | |
| let min = Math.max(CW, CH); | |
| CTX.canvas.width = max * 2; | |
| CTX.canvas.height = max * 2; | |
| CTX.save(); | |
| CTX.translate(max, max); | |
| CTX.rotate(degree * Math.PI / 180); | |
| CTX.drawImage(image, -min / 2, -min / 2); | |
| CTX.restore(); | |
| let imgDATA = CTX.getImageData(0, 0, CTX.canvas.width, CTX.canvas.height); | |
| let TRIM = ENGINE.trimCanvas(imgDATA); | |
| let trimmed = CTX.getImageData( | |
| TRIM.left, | |
| TRIM.top, | |
| TRIM.right - TRIM.left, | |
| TRIM.bottom - TRIM.top | |
| ); | |
| CTX.canvas.width = TRIM.right - TRIM.left; | |
| CTX.canvas.height = TRIM.bottom - TRIM.top; | |
| CTX.putImageData(trimmed, 0, 0); | |
| SPRITE[newName] = new Image(); | |
| SPRITE[newName].onload = ENGINE.creationSpriteCount; | |
| SPRITE[newName].crossOrigin = "Anonymous"; | |
| SPRITE[newName].src = CTX.canvas.toDataURL("image/png"); | |
| SPRITE[newName].width = CTX.canvas.width; | |
| SPRITE[newName].height = CTX.canvas.height; | |
| }, | |
| setCollisionsafe: function(safe) { | |
| if (safe !== undefined) { | |
| ENGINE.INI.COLLISION_SAFE = safe; | |
| } else { | |
| for (let sprite in SPRITE) { | |
| if (SPRITE[sprite].width > ENGINE.INI.COLLISION_SAFE) { | |
| ENGINE.INI.COLLISION_SAFE = SPRITE[sprite].width; | |
| } | |
| if (SPRITE[sprite].height > ENGINE.INI.COLLISION_SAFE) { | |
| ENGINE.INI.COLLISION_SAFE = SPRITE[sprite].height; | |
| } | |
| } | |
| ENGINE.INI.COLLISION_SAFE++; | |
| } | |
| console.log( | |
| `%cENGINE.INI.COLLISION_SAFE set to: ${ENGINE.INI.COLLISION_SAFE}`, | |
| ENGINE.CSS | |
| ); | |
| }, | |
| ready: function() { | |
| ENGINE.setCollisionsafe(); | |
| console.log("%cENGINE ready!", ENGINE.CSS); | |
| $("#buttons").prepend("<input type='button' id='startGame' value='START'>"); | |
| $("#load").addClass("hidden"); | |
| $("#startGame").on("click", PRG.start); | |
| ENGINE.readyCall.call(); | |
| }, | |
| intersectionCollision: function(actor1, actor2) { | |
| if (actor1.class !== "bullet" && actor2.class !== "bullet") return; | |
| if (actor1.prevX === null || actor2.prevX === null) return; | |
| let AL = arguments.length; | |
| let line1 = {}; | |
| let line2 = {}; | |
| for (let q = 0; q < AL; q++) { | |
| switch (arguments[q].class) { | |
| case "bullet": | |
| // for 5px*5px bullet | |
| line1.x1 = arguments[q].prevX; | |
| line1.y1 = arguments[q].prevY + 3; | |
| line1.x2 = arguments[q].x; | |
| line1.y2 = arguments[q].y - 3; | |
| break; | |
| default: | |
| //linear representation of object, angle not considered | |
| line2.x1 = parseInt( | |
| (arguments[q].prevX + arguments[q].x) / 2 + arguments[q].width / 2, | |
| 10 | |
| ); | |
| line2.y1 = parseInt((arguments[q].prevY + arguments[q].y) / 2, 10); | |
| line2.x2 = parseInt( | |
| (arguments[q].prevX + arguments[q].x) / 2 - arguments[q].width / 2, | |
| 10 | |
| ); | |
| line2.y2 = line2.y1; | |
| break; | |
| } | |
| } | |
| return ENGINE.lineIntersects( | |
| line1.x1, | |
| line1.y1, | |
| line1.x2, | |
| line1.y2, | |
| line2.x1, | |
| line2.y1, | |
| line2.x2, | |
| line2.y2 | |
| ); | |
| }, | |
| lineIntersects: function(a, b, c, d, p, q, r, s) { | |
| //https://stackoverflow.com/a/24392281/4154250 | |
| var det, gamma, lambda; | |
| det = (c - a) * (s - q) - (r - p) * (d - b); | |
| if (det === 0) { | |
| return false; | |
| } else { | |
| lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det; | |
| gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det; | |
| return 0 < lambda && lambda < 1 && (0 < gamma && gamma < 1); | |
| } | |
| }, | |
| pixPerfectCollision: function(actor1, actor2) { | |
| var w1 = parseInt(actor1.width / 2, 10); | |
| var w2 = parseInt(actor2.width / 2, 10); | |
| var h1 = parseInt(actor1.height / 2, 10); | |
| var h2 = parseInt(actor2.height / 2, 10); | |
| var act1 = new Vector(actor1.x, actor1.y); | |
| var act2 = new Vector(actor2.x, actor2.y); | |
| var SQ1 = new Square(act1.x - w1, act1.y - h1, w1 * 2, h1 * 2); | |
| var SQ2 = new Square(act2.x - w2, act2.y - h2, w2 * 2, h2 * 2); | |
| var x = parseInt(Math.max(SQ1.x, SQ2.x), 10) - 1; | |
| var y = parseInt(Math.max(SQ1.y, SQ2.y), 10) - 1; | |
| var w = parseInt(Math.min(SQ1.x + SQ1.w - x, SQ2.x + SQ2.w - x), 10) + 1; | |
| var h = parseInt(Math.min(SQ1.y + SQ1.h - y, SQ2.y + SQ2.h - y), 10) + 1; | |
| if (w === 0 || h === 0) return false; | |
| var area = new Square(x, y, w, h); | |
| var area1 = new Square(area.x - SQ1.x, area.y - SQ1.y, area.w, area.h); | |
| var area2 = new Square(area.x - SQ2.x, area.y - SQ2.y, area.w, area.h); | |
| var CTX1 = LAYER.temp; | |
| var CTX2 = LAYER.temp2; | |
| CTX1.canvas.width = ENGINE.INI.sprite_maxW; | |
| CTX1.canvas.height = ENGINE.INI.sprite_maxH; | |
| CTX2.canvas.width = ENGINE.INI.sprite_maxW; | |
| CTX2.canvas.height = ENGINE.INI.sprite_maxH; | |
| ENGINE.draw("temp", 0, 0, SPRITE[actor1.name]); | |
| ENGINE.draw("temp2", 0, 0, SPRITE[actor2.name]); | |
| var data1 = CTX1.getImageData(area1.x, area1.y, area1.w, area1.h); | |
| var data2 = CTX2.getImageData(area2.x, area2.y, area2.w, area2.h); | |
| var DL = data1.data.length; | |
| var index; | |
| for (index = 3; index < DL; index += 4) { | |
| if (data1.data[index] > 0 && data2.data[index] > 0) { | |
| return true; | |
| } | |
| } | |
| //intersectionCollision check | |
| if (ENGINE.checkIntersection) { | |
| return ENGINE.intersectionCollision(actor1, actor2); | |
| } else return false; | |
| }, | |
| collision: function(actor1, actor2) { | |
| var X = Math.abs(actor1.x - actor2.x); | |
| var Y = Math.abs(actor1.y - actor2.y); | |
| if (Y >= ENGINE.INI.COLLISION_SAFE) return false; | |
| if (X >= ENGINE.INI.COLLISION_SAFE) return false; | |
| var w1 = parseInt(actor1.width / 2, 10); | |
| var w2 = parseInt(actor2.width / 2, 10); | |
| var h1 = parseInt(actor1.height / 2, 10); | |
| var h2 = parseInt(actor2.height / 2, 10); | |
| if (X >= w1 + w2 || Y >= h1 + h2) return false; | |
| if (ENGINE.pixelPerfectCollision) { | |
| return ENGINE.pixPerfectCollision(actor1, actor2); | |
| } else return true; | |
| }, | |
| collisionToBackground: function(actor, layer) { | |
| var CTX = layer; | |
| var maxSq = Math.max(actor.width, actor.height); | |
| var R = Math.ceil(0.5 * Math.sqrt(2 * Math.pow(maxSq, 2))); | |
| var X = actor.x; | |
| var Y = actor.y; | |
| var proximity = false; | |
| if (ENGINE.checkProximity) { | |
| var imgDATA = CTX.getImageData(X - R, Y - R, 2 * R, 2 * R); | |
| var check = 1; | |
| var left, right, down, top; | |
| while (check < R) { | |
| left = imgDATA.data[toIndex(X - check, Y)]; | |
| right = imgDATA.data[toIndex(X + check, Y)]; | |
| down = imgDATA.data[toIndex(X, Y + check)]; | |
| top = imgDATA.data[toIndex(X, Y - check)]; | |
| if (left || right || down || top) { | |
| proximity = true; | |
| break; | |
| } | |
| check++; | |
| } | |
| } else proximity = true; | |
| if (!proximity) { | |
| return false; | |
| } else { | |
| var CX = Math.floor(X - actor.width / 2); | |
| var CY = Math.floor(Y - actor.height / 2); | |
| var CTX1 = LAYER.temp; | |
| CTX1.canvas.width = actor.width; | |
| CTX1.canvas.height = actor.height; | |
| ENGINE.draw("temp", 0, 0, SPRITE[actor.name]); | |
| var data1 = CTX1.getImageData(0, 0, actor.width, actor.height); //actor data | |
| var data2 = CTX.getImageData(CX, CY, actor.width, actor.height); //layer data | |
| var DL = data1.data.length; | |
| var index; | |
| for (index = 3; index < DL; index += 4) { | |
| if (data1.data[index] > 0 && data2.data[index] > 0) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function toIndex(x, y) { | |
| var index = (y - Y) * 4 * (2 * R) + (x - (X - R)) * 4 + 3; | |
| return index; | |
| } | |
| }, | |
| drawLoadingGraph: function(counter) { | |
| var count = ENGINE.LOAD[counter]; | |
| var HMI = ENGINE.LOAD["HM" + counter]; | |
| var text = counter; | |
| var percent = Math.floor(count / HMI * 100); | |
| var CTX = LAYER.PRELOAD[counter]; | |
| CTX.clearRect(0, 0, ENGINE.LOAD_W, ENGINE.LOAD_H); | |
| CTX.beginPath(); | |
| CTX.lineWidth = "1"; | |
| CTX.strokeStyle = "black"; | |
| CTX.rect(0, 0, ENGINE.LOAD_W, ENGINE.LOAD_H); | |
| CTX.closePath(); | |
| CTX.stroke(); | |
| CTX.fillStyle = "#999"; | |
| CTX.fillRect( | |
| 1, | |
| 1, | |
| Math.floor((ENGINE.LOAD_W - 2) * (percent / 100)), | |
| ENGINE.LOAD_H - 2 | |
| ); | |
| CTX.fillStyle = "black"; | |
| CTX.font = "10px Verdana"; | |
| CTX.fillText( | |
| text + ": " + percent + "%", | |
| ENGINE.LOAD_W * 0.1, | |
| ENGINE.LOAD_H * 0.62 | |
| ); | |
| return; | |
| }, | |
| statusBar: function(CTX, x, y, w, h, value, max, color) { | |
| CTX.save(); | |
| ENGINE.resetShadow(CTX); | |
| let fs = h / 2; | |
| CTX.font = `${fs}px Verdana`; | |
| CTX.strokeStyle = color; | |
| CTX.fillStyle = color; | |
| CTX.beginPath(); | |
| CTX.lineWidth = "1"; | |
| CTX.rect(x, y, w, h); | |
| CTX.closePath(); | |
| CTX.stroke(); | |
| let fraction = value / max; | |
| CTX.fillRect(x, y, Math.round(fraction * w), h); | |
| CTX.fillStyle = "#FFF"; | |
| CTX.textAlign = "center"; | |
| let tx = x + w / 2 + fs / 2; | |
| let ty = y + h / 2 + fs / 2; | |
| CTX.fillText(`${value}/${max}`, tx, ty); | |
| CTX.restore(); | |
| }, | |
| resetShadow: function(CTX) { | |
| CTX.shadowColor = "#000"; | |
| CTX.shadowOffsetX = 0; | |
| CTX.shadowOffsetY = 0; | |
| CTX.shadowBlur = 0; | |
| }, | |
| spriteDump: function(layer, spriteList) { | |
| console.log("%c********* SPRITE DUMP *********", ENGINE.CSS); | |
| console.log(SPRITE); | |
| var x = 0; | |
| var y = 0; | |
| var dy = 0; | |
| if (spriteList === undefined) { | |
| var keys = Object.keys(SPRITE); | |
| spriteList = keys.map(x => SPRITE[x]); | |
| } | |
| spriteList.forEach(function(q) { | |
| ENGINE.draw(layer, x, y, q); | |
| x += q.width; | |
| if (q.height > dy) dy = q.height; | |
| if (x > LAYER[layer].canvas.width - 64) { | |
| y += dy; | |
| x = 0; | |
| } | |
| }); | |
| }, | |
| window: function( | |
| width = ENGINE.gameWIDTH / 2, | |
| height = ENGINE.gameHEIGHT / 2, | |
| CTX = LAYER.text, | |
| x = Math.floor((ENGINE.gameWIDTH - width) / 2), | |
| y = Math.floor((ENGINE.gameHEIGHT - height) / 2) | |
| ) { | |
| CTX.save(); | |
| CTX.fillStyle = "#000"; | |
| CTX.shadowColor = "#000"; | |
| CTX.shadowOffsetX = 0; | |
| CTX.shadowOffsetY = 0; | |
| CTX.shadowBlur = 0; | |
| CTX.globalAlpha = 0.8; | |
| CTX.roundRect( | |
| x, | |
| y, | |
| width, | |
| height, | |
| { | |
| upperLeft: 15, | |
| upperRight: 15, | |
| lowerLeft: 15, | |
| lowerRight: 15 | |
| }, | |
| true, | |
| true | |
| ); | |
| CTX.restore(); | |
| return new Point(x, y); | |
| }, | |
| mouseOverOK: function(event, cname) { | |
| var canvasOffset = $(cname).offset(); | |
| var offsetX = canvasOffset.left; | |
| var offsetY = canvasOffset.top; | |
| var mouseX = parseInt(event.pageX - offsetX - ENGINE.alertButton.okX, 10); | |
| var mouseY = parseInt(event.pageY - offsetY - ENGINE.alertButton.okY, 10); | |
| if ( | |
| mouseX >= 0 && | |
| mouseX < ENGINE.alertButton.width && | |
| mouseY >= 0 && | |
| mouseY < ENGINE.alertButton.heigth | |
| ) { | |
| $(cname).css("cursor", "pointer"); | |
| } else { | |
| $(cname).css("cursor", "auto"); | |
| } | |
| }, | |
| mouseOver: function(event, cname) { | |
| $(cname).css("cursor", "pointer"); | |
| }, | |
| mouseClick: function(event, cname, func) { | |
| var canvasOffset = $(cname).offset(); | |
| var offsetX = canvasOffset.left; | |
| var offsetY = canvasOffset.top; | |
| var mouseX = parseInt(event.pageX - offsetX, 10); | |
| var mouseY = parseInt(event.pageY - offsetY, 10); | |
| ENGINE.mouseX = mouseX; | |
| ENGINE.mouseY = mouseY; | |
| func.call(); | |
| return; | |
| }, | |
| mouseClickOK: function(event, cname) { | |
| var canvasOffset = $(cname).offset(); | |
| var offsetX = canvasOffset.left; | |
| var offsetY = canvasOffset.top; | |
| var mouseX = parseInt(event.pageX - offsetX - ENGINE.alertButton.okX, 10); | |
| var mouseY = parseInt(event.pageY - offsetY - ENGINE.alertButton.okY, 10); | |
| if ( | |
| mouseX >= 0 && | |
| mouseX < ENGINE.alertButton.width && | |
| mouseY >= 0 && | |
| mouseY < ENGINE.alertButton.heigth | |
| ) { | |
| $(cname).css("cursor", "auto"); | |
| $(cname).off(); | |
| ENGINE.alertMode = false; | |
| ENGINE.clearLayer("alert"); | |
| $(document).keydown(ENGINE.GAME.checkKey); | |
| $(document).keyup(ENGINE.GAME.clearKey); | |
| GAME.continue(); //not DECOUPLED!! | |
| } | |
| }, | |
| getCanvasNumber: function(id) { | |
| var cnv = $("#" + id + " .layer"); | |
| return cnv.length; | |
| }, | |
| getCanvasName: function(id) { | |
| var CL = ENGINE.getCanvasNumber("ROOM"); | |
| var cname = "#ROOM_canvas_" + --CL; | |
| return cname; | |
| }, | |
| cutGrid: function(layer, point) { | |
| var CTX = layer; | |
| CTX.clearRect(point.x, point.y, ENGINE.INI.GRIDPIX, ENGINE.INI.GRIDPIX); | |
| return; | |
| }, | |
| cutManyGrids: function(layer, point, N) { | |
| var CTX = layer; | |
| CTX.clearRect( | |
| point.x, | |
| point.y, | |
| N * ENGINE.INI.GRIDPIX, | |
| N * ENGINE.INI.GRIDPIX | |
| ); | |
| return; | |
| }, | |
| spreadAroundCenter: function(toDo, x, delta) { | |
| var xS = []; | |
| var odd = toDo % 2; | |
| var n = 2; | |
| if (odd) { | |
| xS.push(x); | |
| toDo--; | |
| while (toDo > 0) { | |
| xS.push(x + (n - 1) * delta); | |
| xS.push(x - (n - 1) * delta); | |
| toDo -= 2; | |
| n++; | |
| } | |
| } else { | |
| while (toDo > 0) { | |
| xS.push(x + (n - 1) * delta / 2); | |
| xS.push(x - (n - 1) * delta / 2); | |
| toDo -= 2; | |
| n += 2; | |
| } | |
| } | |
| xS.sort((a, b) => a - b); | |
| return xS; | |
| }, | |
| imgToTexture: function(obj) { | |
| TEXTURE[obj.name] = obj.img; | |
| }, | |
| imgToSprite: function(obj) { | |
| SPRITE[obj.name] = obj.img; | |
| SPRITE[obj.name].width = obj.img.width; | |
| SPRITE[obj.name].height = obj.img.height; | |
| }, | |
| imgToSeqSprite: function(obj) { | |
| SPRITE[obj.name] = obj.img; | |
| SPRITE[obj.name].width = obj.img.width; | |
| SPRITE[obj.name].height = obj.img.height; | |
| }, | |
| extractImg: function(x, y, CTX) { | |
| var data, imgDATA; | |
| var NTX = LAYER.temp2; | |
| data = CTX.getImageData( | |
| x, | |
| y, | |
| ENGINE.INI.SPRITESHEET_DEFAULT_WIDTH, | |
| ENGINE.INI.SPRITESHEET_DEFAULT_HEIGHT | |
| ); | |
| NTX.canvas.width = ENGINE.INI.SPRITESHEET_DEFAULT_WIDTH; | |
| NTX.canvas.height = ENGINE.INI.SPRITESHEET_DEFAULT_HEIGHT; | |
| NTX.putImageData(data, 0, 0); | |
| imgDATA = NTX.getImageData( | |
| 0, | |
| 0, | |
| ENGINE.INI.SPRITESHEET_DEFAULT_WIDTH, | |
| ENGINE.INI.SPRITESHEET_DEFAULT_HEIGHT | |
| ); | |
| var TRIM = ENGINE.trimCanvas(imgDATA); | |
| var trimmed = NTX.getImageData( | |
| TRIM.left, | |
| TRIM.top, | |
| TRIM.right - TRIM.left, | |
| TRIM.bottom - TRIM.top | |
| ); | |
| NTX.canvas.width = TRIM.right - TRIM.left; | |
| NTX.canvas.height = TRIM.bottom - TRIM.top; | |
| NTX.putImageData(trimmed, 0, 0); | |
| return NTX; | |
| }, | |
| drawSheet: function(spriteSheet) { | |
| var CTX = LAYER.temp; | |
| CTX.canvas.width = spriteSheet.width; | |
| CTX.canvas.height = spriteSheet.height; | |
| ENGINE.draw("temp", 0, 0, spriteSheet); | |
| return CTX; | |
| }, | |
| contextToSprite: function(newName, NTX) { | |
| SPRITE[newName] = new Image(); | |
| SPRITE[newName].crossOrigin = "Anonymous"; | |
| SPRITE[newName].src = NTX.canvas.toDataURL("image/png"); | |
| SPRITE[newName].width = NTX.canvas.width; | |
| SPRITE[newName].height = NTX.canvas.height; | |
| return SPRITE[newName]; | |
| }, | |
| packToSprite: function(obj) { | |
| var tag = ["left", "right", "front", "back"]; | |
| var CTX = ENGINE.drawSheet(obj.img); | |
| let x, y; | |
| let newName; | |
| for (var W = 0, LN = tag.length; W < LN; W++) { | |
| for (var q = 0; q < obj.count; q++) { | |
| x = q * ENGINE.INI.SPRITESHEET_DEFAULT_WIDTH; | |
| y = W * ENGINE.INI.SPRITESHEET_DEFAULT_HEIGHT; | |
| let NTX = ENGINE.extractImg(x, y, CTX); | |
| newName = obj.name + "_" + tag[W] + "_" + q; | |
| ASSET[obj.name][tag[W]].push(ENGINE.contextToSprite(newName, NTX)); | |
| } | |
| } | |
| }, | |
| seqToSprite: function(obj) { | |
| var CTX = ENGINE.drawSheet(obj.img); | |
| let x; | |
| let newName; | |
| for (var q = 0; q < obj.count; q++) { | |
| x = q * ENGINE.INI.SPRITESHEET_DEFAULT_WIDTH; | |
| let NTX = ENGINE.extractImg(x, 0, CTX); | |
| newName = obj.name + "_" + q.toString().padStart(2, "0"); | |
| ASSET[obj.name].linear.push(ENGINE.contextToSprite(newName, NTX)); | |
| } | |
| }, | |
| sheetToSprite: function(obj) { | |
| var CTX = ENGINE.drawSheet(obj.img); | |
| let x; | |
| let newName; | |
| for (var q = 0; q < obj.count; q++) { | |
| x = q * ENGINE.INI.SPRITESHEET_DEFAULT_WIDTH; | |
| let NTX = ENGINE.extractImg(x, 0, CTX); | |
| newName = obj.name + "_" + q; | |
| ASSET[obj.parent][obj.tag].push(ENGINE.contextToSprite(newName, NTX)); | |
| } | |
| }, | |
| audioToAudio: function(obj) { | |
| AUDIO[obj.name] = obj.audio; | |
| }, | |
| linkToWasm: function(obj) { | |
| var bin = obj.exports; | |
| for (var fn in bin) { | |
| if (typeof bin[fn] === "function") { | |
| WASM[fn] = bin[fn]; | |
| } | |
| } | |
| }, | |
| KEY: { | |
| on: function() { | |
| $(document).keydown(ENGINE.GAME.checkKey); | |
| $(document).keyup(ENGINE.GAME.clearKey); | |
| }, | |
| off: function() { | |
| $(document).off("keyup", ENGINE.GAME.clearKey); | |
| $(document).off("keydown", ENGINE.GAME.checkKey); | |
| }, | |
| map: { | |
| ctrl: 17, | |
| back: 8, | |
| tab: 9, | |
| alt: 18, | |
| left: 37, | |
| up: 38, | |
| right: 39, | |
| down: 40, | |
| space: 32, | |
| enter: 13, | |
| F9: 120, | |
| F8: 119, | |
| A: 65, | |
| D: 68, | |
| C: 67, | |
| H: 72, | |
| M: 77 | |
| }, | |
| waitFor: function(func, key = "enter") { | |
| if (ENGINE.GAME.stopAnimation) return; | |
| let map = ENGINE.GAME.keymap; | |
| if (map[ENGINE.KEY.map[key]]) { | |
| ENGINE.GAME.ANIMATION.stop(); | |
| func.call(); | |
| ENGINE.GAME.keymap[ENGINE.KEY.map[key]] = false; | |
| return; | |
| } | |
| } | |
| }, | |
| GAME: { | |
| running: false, | |
| keymap: { | |
| 17: false, //CTRL | |
| 37: false, //LEFT | |
| 38: false, //UP | |
| 39: false, //RIGHT | |
| 40: false, //Down | |
| 32: false, //SPACE | |
| 13: false, //ENTER | |
| 120: false, //F9 | |
| 119: false, //F8 | |
| 65: false, //A | |
| 68: false, //D | |
| 67: false, //C | |
| 8: false, //back | |
| 9: false, //tab | |
| 72: false, //h | |
| 77: false //m | |
| }, | |
| clearAllKeys: function() { | |
| for (var key in ENGINE.GAME.keymap) { | |
| ENGINE.GAME.keymap[key] = false; | |
| } | |
| }, | |
| clearKey: function(e) { | |
| e = e || window.event; | |
| if (e.keyCode in ENGINE.GAME.keymap) { | |
| ENGINE.GAME.keymap[e.keyCode] = false; | |
| } | |
| }, | |
| checkKey: function(e) { | |
| e = e || window.event; | |
| if (e.keyCode in ENGINE.GAME.keymap) { | |
| ENGINE.GAME.keymap[e.keyCode] = true; | |
| e.preventDefault(); | |
| } | |
| }, | |
| run: function(func, nextFunct) { | |
| ENGINE.GAME.running = true; | |
| if (!ENGINE.GAME.frame.start) ENGINE.GAME.frame.start = performance.now(); | |
| ENGINE.GAME.frame.delta = performance.now() - ENGINE.GAME.frame.start; | |
| if (ENGINE.GAME.frame.delta >= ENGINE.INI.ANIMATION_INTERVAL) { | |
| if (ENGINE.GAME.stopAnimation) { | |
| if (nextFunct) nextFunct.call(); | |
| console.log( | |
| `%cAnimation stopped BEFORE execution ${func.name}`, | |
| "color: #f00" | |
| ); | |
| ENGINE.GAME.running = false; | |
| return; | |
| } | |
| func.call(); | |
| ENGINE.GAME.frame.start = null; | |
| } | |
| if (!ENGINE.GAME.stopAnimation) { | |
| requestAnimationFrame(ENGINE.GAME.run.bind(null, func, nextFunct)); | |
| } else { | |
| if (nextFunct) nextFunct.call(); | |
| console.log( | |
| `%cAnimation stopped AFTER execution ${func.name}`, | |
| "color: #f00" | |
| ); | |
| ENGINE.GAME.running = false; | |
| return; | |
| } | |
| }, | |
| start: function() { | |
| $("#DOWN")[0].scrollIntoView(); | |
| ENGINE.GAME.stopAnimation = false; | |
| ENGINE.GAME.started = Date.now(); | |
| ENGINE.GAME.frame = {}; | |
| ENGINE.GAME.frame.start = null; | |
| }, | |
| ANIMATION: { | |
| start: function(wrapper) { | |
| console.log( | |
| `%cENGINE.GAME.ANIMATION.start --> ${wrapper.name}`, | |
| "color: #0F0" | |
| ); | |
| ENGINE.GAME.stopAnimation = false; | |
| ENGINE.GAME.run(wrapper, ENGINE.GAME.ANIMATION.queue); | |
| }, | |
| stop: function() { | |
| ENGINE.GAME.stopAnimation = true; | |
| }, | |
| queue: function() { | |
| ENGINE.GAME.ANIMATION.stop(); | |
| setTimeout(() => { | |
| console.log(`%cGame running? ${ENGINE.GAME.running}`, ENGINE.CSS); | |
| if (ENGINE.GAME.ANIMATION.STACK.length > 0) { | |
| let next = ENGINE.GAME.ANIMATION.STACK.shift(); | |
| console.log(`%c..... animation queue: ${next.name}`, ENGINE.CSS); | |
| ENGINE.GAME.ANIMATION.start(next); | |
| } else { | |
| console.log( | |
| "%cAnimation STACK EMPTY! Game stopped running.", | |
| ENGINE.CSS | |
| ); | |
| } | |
| }, ENGINE.INI.ANIMATION_INTERVAL); | |
| }, | |
| waitThen: function(func, n = 1) { | |
| setTimeout(func, ENGINE.INI.ANIMATION_INTERVAL * n); | |
| }, | |
| STACK: [] | |
| } | |
| }, | |
| VIEWPORT: { | |
| max: { | |
| x: 0, | |
| y: 0 | |
| }, | |
| setMax: function(max) { | |
| ENGINE.VIEWPORT.max.x = max.x; | |
| ENGINE.VIEWPORT.max.y = max.y; | |
| }, | |
| changed: false, | |
| reset: function() { | |
| ENGINE.VIEWPORT.vx = 0; | |
| ENGINE.VIEWPORT.vy = 0; | |
| }, | |
| vx: 0, | |
| vy: 0, | |
| change: function(from, to) { | |
| ENGINE.copyLayer( | |
| from, | |
| to, | |
| ENGINE.VIEWPORT.vx, | |
| ENGINE.VIEWPORT.vy, | |
| ENGINE.gameWIDTH, | |
| ENGINE.gameHEIGHT | |
| ); | |
| }, | |
| check: function(actor, max = ENGINE.VIEWPORT.max) { | |
| var vx = actor.x - ENGINE.gameWIDTH / 2; | |
| var vy = actor.y - ENGINE.gameHEIGHT / 2; | |
| if (vx < 0) vx = 0; | |
| if (vy < 0) vy = 0; | |
| if (vx > max.x - ENGINE.gameWIDTH) vx = max.x - ENGINE.gameWIDTH; | |
| if (vy > max.y - ENGINE.gameHEIGHT) vy = max.y - ENGINE.gameHEIGHT; | |
| if (vx != ENGINE.VIEWPORT.vx || vy != ENGINE.VIEWPORT.vy) { | |
| ENGINE.VIEWPORT.vx = vx; | |
| ENGINE.VIEWPORT.vy = vy; | |
| ENGINE.VIEWPORT.changed = true; | |
| } | |
| }, | |
| alignTo: function(actor) { | |
| actor.vx = actor.x - ENGINE.VIEWPORT.vx; | |
| actor.vy = actor.y - ENGINE.VIEWPORT.vy; | |
| } | |
| }, | |
| TEXT: { | |
| font: "Arcade", | |
| fs: "40", | |
| setFS: function(fs) { | |
| if (isNaN(fs)) fs = 40; | |
| ENGINE.TEXT.fs = fs.toString(); | |
| }, | |
| text: function( | |
| text, | |
| x, | |
| y, | |
| layer = "text", | |
| fs = ENGINE.TEXT.fs, | |
| font = ENGINE.TEXT.font | |
| ) { | |
| var CTX = LAYER[layer]; | |
| CTX.fillStyle = "#FFF"; | |
| CTX.font = fs + "px " + font; | |
| CTX.shadowColor = "#333333"; | |
| CTX.shadowOffsetX = 3; | |
| CTX.shadowOffsetY = 3; | |
| CTX.shadowBlur = 3; | |
| CTX.textAlign = "center"; | |
| CTX.fillText(text, x, y); | |
| }, | |
| centeredText: function( | |
| text, | |
| y, | |
| layer = "text", | |
| fs = ENGINE.TEXT.fs, | |
| font = ENGINE.TEXT.font | |
| ) { | |
| var x = ENGINE.gameWIDTH / 2; | |
| ENGINE.TEXT.text(text, x, y, layer, fs, font); | |
| } | |
| }, | |
| LOAD: { | |
| Textures: 0, | |
| Sprites: 0, | |
| Sequences: 0, | |
| Sheets: 0, | |
| Rotated: 0, | |
| WASM: 0, | |
| Sounds: 0, | |
| Fonts: 0, | |
| Packs: 0, | |
| SheetSequences: 0, | |
| HMSheetSequences: null, | |
| HMFonts: null, | |
| HMSequences: null, | |
| HMTextures: null, | |
| HMSprites: null, | |
| HMSheets: null, | |
| HMRotated: null, | |
| HMWASM: null, | |
| HMSounds: null, | |
| HMPacks: null, | |
| preload: function() { | |
| console.log("%cPreloading ...", ENGINE.CSS); | |
| var AllLoaded = Promise.all([ | |
| loadTextures(), | |
| loadSprites(), | |
| loadSequences(), | |
| loadSheets(), | |
| loadPacks(), | |
| loadSheetSequences(), | |
| loadRotated(), | |
| loadingSounds(), | |
| loadWASM(), | |
| loadAllFonts() | |
| ]).then(function() { | |
| console.log("%cAll assets loaded and ready!", ENGINE.CSS); | |
| console.log("%c****************************", ENGINE.CSS); | |
| //console.log("SPRITE", SPRITE); | |
| ENGINE.ready(); | |
| }); | |
| return; | |
| function appendCanvas(name) { | |
| let id = "preload_" + name; | |
| $("#load").append( | |
| "<canvas id ='" + | |
| id + | |
| "' width='" + | |
| ENGINE.LOAD_W + | |
| "' height='" + | |
| ENGINE.LOAD_H + | |
| "'></canvas>" | |
| ); | |
| LAYER.PRELOAD[name] = $("#" + id)[0].getContext("2d"); | |
| } | |
| function loadTextures(arrPath = LoadTextures) { | |
| console.log(`%c ...loading ${arrPath.length} textures`, ENGINE.CSS); | |
| ENGINE.LOAD.HMTextures = arrPath.length; | |
| if (ENGINE.LOAD.HMTextures) appendCanvas("Textures"); | |
| const temp = Promise.all( | |
| arrPath.map(img => loadImage(img, "Textures")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.imgToTexture(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadSprites(arrPath = LoadSprites) { | |
| console.log(`%c ...loading ${arrPath.length} sprites`, ENGINE.CSS); | |
| ENGINE.LOAD.HMSprites = arrPath.length; | |
| if (ENGINE.LOAD.HMSprites) appendCanvas("Sprites"); | |
| const temp = Promise.all( | |
| arrPath.map(img => loadImage(img, "Sprites")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.imgToSprite(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadSequences(arrPath = LoadSequences) { | |
| console.log(`%c ...loading ${arrPath.length} sequences`, ENGINE.CSS); | |
| var toLoad = []; | |
| arrPath.forEach(function(el) { | |
| for (let i = 1; i <= el.count; i++) { | |
| toLoad.push({ | |
| srcName: | |
| el.srcName + | |
| "_" + | |
| i.toString().padStart(2, "0") + | |
| "." + | |
| el.type, | |
| name: el.name + (i - 1).toString().padStart(2, "0") | |
| }); | |
| } | |
| }); | |
| ENGINE.LOAD.HMSequences = toLoad.length; | |
| if (ENGINE.LOAD.HMSequences) appendCanvas("Sequences"); | |
| const temp = Promise.all( | |
| toLoad.map(img => loadImage(img, "Sequences")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.imgToSeqSprite(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadPacks(arrPath = LoadPacks) { | |
| console.log(`%c ...loading ${arrPath.length} packs`, ENGINE.CSS); | |
| var toLoad = []; | |
| arrPath.forEach(function(el) { | |
| ASSET[el.name] = new LiveSPRITE("4D", [], [], [], []); | |
| toLoad.push({ | |
| srcName: el.srcName, | |
| name: el.name, | |
| count: el.count | |
| }); | |
| }); | |
| ENGINE.LOAD.HMPacks = toLoad.length; | |
| if (ENGINE.LOAD.HMPacks) appendCanvas("Packs"); | |
| const temp = Promise.all( | |
| toLoad.map(img => loadImage(img, "Packs")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.packToSprite(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadSheets(arrPath = LoadSheets, addTag = ExtendSheetTag) { | |
| console.log(`%c ...loading ${arrPath.length} sheets`, ENGINE.CSS); | |
| var toLoad = []; | |
| var tag = ["left", "right", "front", "back", ...addTag]; | |
| arrPath.forEach(function(el) { | |
| ASSET[el.name] = new LiveSPRITE("4D", [], [], [], []); | |
| for (let q = 0, TL = tag.length; q < TL; q++) { | |
| toLoad.push({ | |
| srcName: el.srcName + "_" + tag[q] + "." + el.type, | |
| name: el.name + "_" + tag[q], | |
| count: el.count, | |
| tag: tag[q], | |
| parent: el.name | |
| }); | |
| } | |
| }); | |
| ENGINE.LOAD.HMSheets = toLoad.length; | |
| if (ENGINE.LOAD.HMSheets) appendCanvas("Sheets"); | |
| const temp = Promise.all( | |
| toLoad.map(img => loadImage(img, "Sheets")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.sheetToSprite(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadSheetSequences(arrPath = LoadSheetSequences) { | |
| console.log( | |
| `%c ...loading ${arrPath.length} sheet sequences`, | |
| ENGINE.CSS | |
| ); | |
| var toLoad = []; | |
| arrPath.forEach(function(el) { | |
| ASSET[el.name] = new LiveSPRITE("1D", []); | |
| toLoad.push({ | |
| srcName: el.srcName, | |
| name: el.name, | |
| count: el.count | |
| }); | |
| }); | |
| ENGINE.LOAD.HMSheetSequences = toLoad.length; | |
| if (ENGINE.LOAD.HMSheetSequences) appendCanvas("SheetSequences"); | |
| const temp = Promise.all( | |
| toLoad.map(img => loadImage(img, "SheetSequences")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.seqToSprite(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadRotated(arrPath = LoadRotated) { | |
| console.log( | |
| `%c ...loading ${arrPath.length} rotated sprites`, | |
| ENGINE.CSS | |
| ); | |
| ENGINE.LOAD.HMRotated = arrPath.length; | |
| if (ENGINE.LOAD.HMRotated) appendCanvas("Rotated"); | |
| const temp = Promise.all( | |
| arrPath.map(img => loadImage(img, "Rotated")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| for ( | |
| let q = el.rotate.first; | |
| q <= el.rotate.last; | |
| q += el.rotate.step | |
| ) { | |
| ENGINE.rotateImage(el.img, q, el.name + "_" + q); | |
| } | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadWASM(arrPath = LoadExtWasm) { | |
| var LoadIntWasm = []; //internal hard coded ENGINE requirements | |
| var toLoad = [...arrPath, ...LoadIntWasm]; | |
| console.log(`%c ...loading ${toLoad.length} WASM files`, ENGINE.CSS); | |
| ENGINE.LOAD.HMWASM = toLoad.length; | |
| if (ENGINE.LOAD.HMWASM) appendCanvas("WASM"); | |
| const temp = Promise.all( | |
| toLoad.map(wasm => loadWebAssembly(wasm, "WASM")) | |
| ).then(instance => { | |
| instance.forEach(function(el) { | |
| ENGINE.linkToWasm(el); | |
| }); | |
| }); | |
| return temp; | |
| } | |
| function loadingSounds(arrPath = LoadAudio) { | |
| console.log(`%c ...loading ${arrPath.length} sounds`, ENGINE.CSS); | |
| ENGINE.LOAD.HMSounds = arrPath.length; | |
| if (ENGINE.LOAD.HMSounds) appendCanvas("Sounds"); | |
| const temp = Promise.all( | |
| arrPath.map(audio => loadAudio(audio, "Sounds")) | |
| ).then(function(obj) { | |
| obj.forEach(function(el) { | |
| ENGINE.audioToAudio(el); | |
| }); | |
| }); | |
| } | |
| function loadAllFonts(arrPath = LoadFonts) { | |
| console.log(`%c ...loading ${arrPath.length} fonts`, ENGINE.CSS); | |
| ENGINE.LOAD.HMFonts = arrPath.length; | |
| if (ENGINE.LOAD.HMFonts) appendCanvas("Fonts"); | |
| const temp = Promise.all(arrPath.map(font => loadFont(font))).then( | |
| function(obj) { | |
| obj.map(x => document.fonts.add(x)); | |
| ENGINE.LOAD.Fonts = ENGINE.LOAD.HMFonts; | |
| ENGINE.drawLoadingGraph("Fonts"); | |
| } | |
| ); | |
| } | |
| function loadImage(srcData, counter, dir = ENGINE.SOURCE) { | |
| var srcName, name, count, tag, parent, rotate; | |
| switch (typeof srcData) { | |
| case "string": | |
| srcName = srcData; | |
| name = srcName.substr(0, srcName.indexOf(".")); | |
| break; | |
| case "object": | |
| srcName = srcData.srcName; | |
| name = srcData.name; | |
| count = srcData.count || null; | |
| tag = srcData.tag || null; | |
| parent = srcData.parent || null; | |
| rotate = srcData.rotate || null; | |
| break; | |
| default: | |
| console.error(`ENGINE: loadImage srcData ERROR: ${typeof srcData}`); | |
| } | |
| var src = dir + srcName; | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| var obj = { | |
| img: img, | |
| name: name, | |
| count: count, | |
| tag: tag, | |
| parent: parent, | |
| rotate: rotate | |
| }; | |
| img.onload = function() { | |
| ENGINE.LOAD[counter]++; | |
| ENGINE.drawLoadingGraph(counter); | |
| resolve(obj); | |
| }; | |
| img.onerror = err => resolve(err); | |
| img.crossOrigin = "Anonymous"; | |
| img.src = src; | |
| }); | |
| } | |
| function loadAudio(srcData, counter, dir = ENGINE.AUDIO_SOURCE) { | |
| var srcName, name; | |
| switch (typeof srcData) { | |
| case "string": | |
| srcName = srcData; | |
| name = srcName.substr(0, srcName.indexOf(".")); | |
| break; | |
| case "object": | |
| srcName = srcData.srcName; | |
| name = srcData.name; | |
| break; | |
| default: | |
| console.error(`ENGINE: loadAudio srcData ERROR: ${typeof srcData}`); | |
| } | |
| var src = dir + srcName; | |
| return new Promise((resolve, reject) => { | |
| const audio = new Audio(); | |
| var obj = { | |
| audio: audio, | |
| name: name | |
| }; | |
| audio.oncanplaythrough = function() { | |
| ENGINE.LOAD[counter]++; | |
| ENGINE.drawLoadingGraph(counter); | |
| resolve(obj); | |
| }; | |
| audio.onerror = err => resolve(err); | |
| audio.src = src; | |
| audio.load(); | |
| }); | |
| } | |
| function loadWebAssembly(fileName, counter) { | |
| fileName = ENGINE.WASM_SOURCE + fileName; | |
| return fetch(fileName) | |
| .then(response => response.arrayBuffer()) | |
| .then(bits => WebAssembly.compile(bits)) | |
| .then(module => { | |
| ENGINE.LOAD[counter]++; | |
| ENGINE.drawLoadingGraph(counter); | |
| return new WebAssembly.Instance(module); | |
| }); | |
| } | |
| function loadFont(srcData, dir = ENGINE.FONT_SOURCE) { | |
| const fontSource = dir + srcData.srcName; | |
| const url = `url(${fontSource})`; | |
| const temp = new FontFace(srcData.name, url); | |
| return temp.load(); | |
| } | |
| } | |
| } | |
| }; | |
| var TEXTURE = {}; | |
| var LAYER = { | |
| PRELOAD: {} | |
| }; | |
| var SPRITE = {}; | |
| var AUDIO = {}; | |
| var ASSET = {}; | |
| var WASM = {}; | |
| var PATTERN = { | |
| create: function(which) { | |
| var image = TEXTURE[which]; | |
| var CTX = LAYER.temp; | |
| PATTERN[which] = CTX.createPattern(image, "repeat"); | |
| } | |
| }; | |
| var AnimationSPRITE = function(x, y, type, howmany) { | |
| this.x = x; | |
| this.y = y; | |
| this.pool = []; | |
| for (var i = 0; i < howmany; i++) { | |
| this.pool.push(type + i.toString().padStart(2, "0")); | |
| } | |
| }; | |
| class TextSprite { | |
| constructor(text, point, color, frame) { | |
| this.text = text; | |
| this.point = point; | |
| this.color = color || "#FFF"; | |
| this.frame = frame || ENGINE.INI.FADE_FRAMES; //magic number | |
| } | |
| } | |
| var TEXTPOOL = { | |
| pool: [], | |
| draw: function(layer) { | |
| var TPL = TEXTPOOL.pool.length; | |
| if (TPL === 0) return; | |
| ENGINE.layersToClear.add(layer); | |
| var CTX = LAYER[layer]; | |
| CTX.font = "10px Consolas"; | |
| CTX.textAlign = "center"; | |
| var vx, vy; | |
| for (let q = TPL - 1; q >= 0; q--) { | |
| CTX.fillStyle = TEXTPOOL.pool[q].color; | |
| vx = | |
| TEXTPOOL.pool[q].point.x - ENGINE.VIEWPORT.vx + ENGINE.INI.GRIDPIX / 2; | |
| vy = | |
| TEXTPOOL.pool[q].point.y - ENGINE.VIEWPORT.vy + ENGINE.INI.GRIDPIX / 2; | |
| CTX.save(); | |
| CTX.globalAlpha = | |
| (1000 - | |
| (ENGINE.INI.FADE_FRAMES - TEXTPOOL.pool[q].frame) * | |
| (1000 / ENGINE.INI.FADE_FRAMES)) / | |
| 1000; | |
| CTX.fillText(TEXTPOOL.pool[q].text, vx, vy); | |
| CTX.restore(); | |
| TEXTPOOL.pool[q].frame--; | |
| if (TEXTPOOL.pool[q].frame <= 0) { | |
| TEXTPOOL.pool.splice(q, 1); | |
| } | |
| } | |
| } | |
| }; | |
| class PartSprite { | |
| constructor(point, sprite, line, speed) { | |
| this.point = point; | |
| this.sprite = sprite; | |
| this.line = line; | |
| this.speed = speed; | |
| } | |
| } | |
| var SpritePOOL = { | |
| pool: [], | |
| draw: function(layer) { | |
| var SPL = SpritePOOL.pool.length; | |
| if (SPL === 0) return; | |
| ENGINE.layersToClear.add(layer); | |
| var vx, vy, line; | |
| for (var q = SPL - 1; q >= 0; q--) { | |
| vx = SpritePOOL.pool[q].point.x - ENGINE.VIEWPORT.vx; | |
| vy = SpritePOOL.pool[q].point.y - ENGINE.VIEWPORT.vy; | |
| line = SpritePOOL.pool[q].sprite.height - SpritePOOL.pool[q].line; | |
| ENGINE.drawPart(layer, vx, vy, SpritePOOL.pool[q].sprite, line); | |
| SpritePOOL.pool[q].line--; | |
| if (SpritePOOL.pool[q].line <= 0) { | |
| SpritePOOL.pool.splice(q, 1); | |
| } | |
| } | |
| } | |
| }; | |
| var EXPLOSIONS = { | |
| pool: [], | |
| draw: function(layer) { | |
| // draws AnimationSPRITE(x, y, type, howmany) from EXPLOSIONS.pool | |
| // example new AnimationSPRITE(actor.x, actor.y, "AlienExp", 6) | |
| layer = layer || "explosion"; | |
| var PL = EXPLOSIONS.pool.length; | |
| if (PL === 0) return; | |
| ENGINE.layersToClear.add(layer); | |
| for (var instance = PL - 1; instance >= 0; instance--) { | |
| var sprite = EXPLOSIONS.pool[instance].pool.shift(); | |
| ENGINE.spriteDraw( | |
| layer, | |
| EXPLOSIONS.pool[instance].x - ENGINE.VIEWPORT.vx, | |
| EXPLOSIONS.pool[instance].y - ENGINE.VIEWPORT.vy, | |
| SPRITE[sprite] | |
| ); | |
| if (EXPLOSIONS.pool[instance].pool.length === 0) { | |
| EXPLOSIONS.pool.splice(instance, 1); | |
| } | |
| } | |
| } | |
| }; | |
| class LiveSPRITE { | |
| constructor(type, left, right, front, back) { | |
| this.type = type || null; | |
| switch (type) { | |
| case "1D": | |
| this.linear = left; | |
| break; | |
| case "4D": | |
| this.left = left || null; | |
| this.right = right || null; | |
| this.front = front || null; | |
| this.back = back || null; | |
| break; | |
| default: | |
| throw "LiveSPRITE type ERROR"; | |
| break; | |
| } | |
| } | |
| } | |
| class ACTOR { | |
| constructor(sprite_class, x, y, orientation, asset) { | |
| this.class = sprite_class; | |
| this.x = x || 0; | |
| this.y = y || 0; | |
| this.orientation = orientation || null; | |
| this.asset = asset || null; | |
| this.vx = 0; | |
| this.vy = 0; | |
| this.resetIndexes(); | |
| if (this.class !== null) this.refresh(); | |
| } | |
| resetIndexes() { | |
| this.left_index = 0; | |
| this.right_index = 0; | |
| this.front_index = 0; | |
| this.back_index = 0; | |
| this.linear_index = 0; | |
| } | |
| refresh() { | |
| if (this.orientation === null) { | |
| this.name = this.class; | |
| return; | |
| } | |
| switch (this.asset.type) { | |
| case "4D": | |
| this.name = `${this.class}_${this.orientation}_${ | |
| this[this.orientation + "_index"] | |
| }`; | |
| break; | |
| case "1D": | |
| this.name = `${this.class}_${this.linear_index | |
| .toString() | |
| .padStart(2, "0")}`; | |
| break; | |
| default: | |
| throw "actor.refresh asset type ERRoR"; | |
| } | |
| this.width = SPRITE[this.name].width; | |
| this.height = SPRITE[this.name].height; | |
| } | |
| sprite() { | |
| return SPRITE[this.name]; | |
| } | |
| getOrientation(dir) { | |
| var orientation; | |
| if (this.asset.type === "1D") { | |
| orientation = "linear"; | |
| return orientation; | |
| } | |
| switch (dir.x) { | |
| case 1: | |
| orientation = "right"; | |
| break; | |
| case -1: | |
| orientation = "left"; | |
| break; | |
| case 0: | |
| switch (dir.y) { | |
| case 1: | |
| orientation = "front"; | |
| break; | |
| case -1: | |
| orientation = "back"; | |
| break; | |
| case 0: | |
| orientation = "front"; | |
| break; | |
| } | |
| break; | |
| } | |
| return orientation; | |
| } | |
| animateMove(orientation) { | |
| this[orientation + "_index"]++; | |
| if (this[orientation + "_index"] >= this.asset[orientation].length) | |
| this[orientation + "_index"] = 0; | |
| this.refresh(); | |
| } | |
| static gridToClass(grid, sprite_class) { | |
| var p = GRID.gridToCenterPX(grid); | |
| return new ACTOR(sprite_class, p.x, p.y); | |
| } | |
| } | |
| var GRID = { | |
| collision: function(actor, grid) { | |
| let actorGrid = actor.MoveState.homeGrid; | |
| return GRID.same(actorGrid, grid); | |
| }, | |
| spriteToSpriteCollision: function(actor1, actor2) { | |
| return GRID.same(actor1.MoveState.homeGrid, actor2.MoveState.homeGrid); | |
| }, | |
| gridToCenterPX: function(grid) { | |
| var x = grid.x * ENGINE.INI.GRIDPIX + ENGINE.INI.GRIDPIX / 2; | |
| var y = grid.y * ENGINE.INI.GRIDPIX + ENGINE.INI.GRIDPIX / 2; | |
| return new Point(x, y); | |
| }, | |
| gridToSprite: function(grid, actor) { | |
| GRID.coordToSprite(GRID.gridToCoord(grid), actor); | |
| }, | |
| coordToSprite: function(coord, actor) { | |
| actor.x = coord.x + ENGINE.INI.GRIDPIX / 2; | |
| actor.y = coord.y + ENGINE.INI.GRIDPIX / 2; | |
| }, | |
| gridToCoord: function(grid) { | |
| var x = grid.x * ENGINE.INI.GRIDPIX; | |
| var y = grid.y * ENGINE.INI.GRIDPIX; | |
| return new Point(x, y); | |
| }, | |
| coordToGrid: function(x, y) { | |
| var tx = Math.floor(x / ENGINE.INI.GRIDPIX); | |
| var ty = Math.floor(y / ENGINE.INI.GRIDPIX); | |
| return new Grid(tx, ty); | |
| }, | |
| create: function(x, y) { | |
| var temp = []; | |
| var string = "1".repeat(x); | |
| for (var iy = 0; iy < y; iy++) { | |
| temp.push(string); | |
| } | |
| return temp; | |
| }, | |
| grid: function() { | |
| var CTX = LAYER.grid; | |
| var x = 0; | |
| var y = 0; | |
| CTX.strokeStyle = "#FFF"; | |
| //horizonal lines | |
| do { | |
| y += ENGINE.INI.GRIDPIX; | |
| CTX.beginPath(); | |
| CTX.setLineDash([1, 3]); | |
| CTX.moveTo(x, y); | |
| CTX.lineTo(CTX.canvas.width, y); | |
| CTX.closePath(); | |
| CTX.stroke(); | |
| } while (y <= CTX.canvas.height); | |
| //vertical lines | |
| y = 0; | |
| do { | |
| x += ENGINE.INI.GRIDPIX; | |
| CTX.beginPath(); | |
| CTX.setLineDash([1, 3]); | |
| CTX.moveTo(x, y); | |
| CTX.lineTo(x, CTX.canvas.height); | |
| CTX.closePath(); | |
| CTX.stroke(); | |
| } while (x <= CTX.canvas.width); | |
| }, | |
| paintText: function(point, text, layer, color = "#FFF") { | |
| var CTX = LAYER[layer]; | |
| CTX.font = "10px Consolas"; | |
| var y = point.y + ENGINE.INI.GRIDPIX / 2; | |
| var x = point.x + ENGINE.INI.GRIDPIX / 2; | |
| CTX.fillStyle = color; | |
| CTX.textAlign = "center"; | |
| CTX.fillText(text, x, y); | |
| }, | |
| paint: function( | |
| grid, | |
| floorIMG, | |
| wallIMG, | |
| floorLayer = "floor", | |
| wallLayer = "wall", | |
| drawGrid = false | |
| ) { | |
| ENGINE.clearLayer(floorLayer); | |
| ENGINE.clearLayer(wallLayer); | |
| ENGINE.fill(LAYER[floorLayer], floorIMG); | |
| ENGINE.fill(LAYER[wallLayer], wallIMG); | |
| if (drawGrid) { | |
| ENGINE.clearLayer("grid"); | |
| GRID.grid(); | |
| } | |
| }, | |
| repaint: function( | |
| grid, | |
| floorIMG, | |
| wallIMG, | |
| floorLayer = "floor", | |
| wallLayer = "wall", | |
| drawGrid = false | |
| ) { | |
| GRID.paint(grid, floorIMG, wallIMG, floorLayer, wallLayer, drawGrid); | |
| const height = grid.length; | |
| const width = grid[0].length; | |
| for (var y = 0; y < height; y++) { | |
| for (var x = 0; x < width; x++) { | |
| if (grid[y].charAt(x) === "0") { | |
| let point = GRID.gridToCoord({ x: x, y: y }); | |
| ENGINE.cutGrid(LAYER[wallLayer], point); | |
| } | |
| } | |
| } | |
| }, | |
| map: { | |
| pack: function(grid) { | |
| var RL = grid.length; | |
| var converted = []; | |
| for (var i = 0; i < RL; i++) { | |
| converted.push(parseInt(grid[i], 2)); | |
| } | |
| return converted; | |
| }, | |
| unpack: function(map) { | |
| var h = map.height; | |
| var w = map.width; | |
| if (h != map.grid.length) { | |
| throw "Map corrupted: height:" + h + " grid.length: " + map.grid.length; | |
| } | |
| var binary = []; | |
| for (var i = 0; i < h; i++) { | |
| let binTemp = float64ToInt64Binary(map.grid[i]).padStart(w, "0"); | |
| binary.push(binTemp); | |
| } | |
| return binary; | |
| } | |
| }, | |
| isBlock: function(x, y, map = MAP[GAME.level]) { | |
| var block = map.grid[y].charAt(x); | |
| if (block === "1") { | |
| return true; | |
| } else return false; | |
| }, | |
| gridIsBlock: function(grid, map = MAP[GAME.level]) { | |
| return GRID.isBlock(grid.x, grid.y, map); | |
| }, | |
| trueToGrid: function(actor) { | |
| var TX = actor.x - ENGINE.INI.GRIDPIX / 2; | |
| var TY = actor.y - ENGINE.INI.GRIDPIX / 2; | |
| var GX = TX / ENGINE.INI.GRIDPIX; | |
| var GY = TY / ENGINE.INI.GRIDPIX; | |
| var MX = TX % ENGINE.INI.GRIDPIX; | |
| var MY = TY % ENGINE.INI.GRIDPIX; | |
| if (MX || MY) { | |
| return null; | |
| } else return { x: GX, y: GY }; | |
| }, | |
| same: function(grid1, grid2) { | |
| if (grid1 === null || grid2 === null) return false; | |
| if (grid1.x === grid2.x && grid1.y === grid2.y) { | |
| return true; | |
| } else return false; | |
| }, | |
| isGridIn: function(grid, gridArray) { | |
| for (var q = 0; q < gridArray.length; q++) { | |
| if (grid.x === gridArray[q].x && grid.y === gridArray[q].y) { | |
| return q; | |
| } | |
| } | |
| return -1; | |
| }, | |
| getDirections: function(grid, obstacles = []) { | |
| var directions = []; | |
| for (let D = 0; D < ENGINE.directions.length; D++) { | |
| let newGrid = grid.add(ENGINE.directions[D]); | |
| if (!GRID.gridIsBlock(newGrid) && newGrid.isInAt(obstacles) === -1) { | |
| directions.push(ENGINE.directions[D]); | |
| } | |
| } | |
| return directions; | |
| }, | |
| findCrossroad: function(start, dir) { | |
| let directions = GRID.getDirections(start); | |
| let back = dir.mirror(); | |
| let BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| while (directions.length < 2) { | |
| dir = directions[0]; | |
| start = start.add(dir); | |
| directions = GRID.getDirections(start); | |
| back = dir.mirror(); | |
| BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| } | |
| return start; | |
| }, | |
| findCrossroadAndLastDir: function(start, dir) { | |
| let directions = GRID.getDirections(start); | |
| let back = dir.mirror(); | |
| let BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| while (directions.length < 2) { | |
| dir = directions[0]; | |
| start = start.add(dir); | |
| directions = GRID.getDirections(start); | |
| back = dir.mirror(); | |
| BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| } | |
| return { finish: start, dir: dir }; | |
| }, | |
| pathToCrossroad: function(start, dir) { | |
| let path = []; | |
| path.push(dir); | |
| start = start.add(dir); | |
| let directions = GRID.getDirections(start); | |
| if (directions.length === 1) return path; | |
| let back = dir.mirror(); | |
| let BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| while (directions.length === 1) { | |
| dir = directions[0]; | |
| path.push(dir); | |
| start = start.add(dir); | |
| directions = GRID.getDirections(start); | |
| if (directions.length === 1) return path; | |
| back = dir.mirror(); | |
| BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| } | |
| return path; | |
| }, | |
| findLengthToCrossroad: function(start, stack) { | |
| if (stack === null) return; | |
| var q = 0; | |
| do { | |
| if (stack[q] === undefined) return null; | |
| start = start.add(stack[q]); | |
| q++; | |
| } while (GRID.getDirections(start).length < 3); | |
| return q; | |
| }, | |
| translateMove: function(entity, changeView = false, onFinish = null) { | |
| entity.actor.x += entity.MoveState.dir.x * entity.speed; | |
| entity.actor.y += entity.MoveState.dir.y * entity.speed; | |
| entity.actor.orientation = entity.actor.getOrientation( | |
| entity.MoveState.dir | |
| ); | |
| entity.actor.animateMove(entity.actor.orientation); | |
| entity.MoveState.homeGrid = GRID.coordToGrid( | |
| entity.actor.x, | |
| entity.actor.y | |
| ); | |
| if (changeView) { | |
| ENGINE.VIEWPORT.check(entity.actor); | |
| } | |
| ENGINE.VIEWPORT.alignTo(entity.actor); | |
| if (GRID.same(entity.MoveState.endGrid, GRID.trueToGrid(entity.actor))) { | |
| entity.MoveState.moving = false; | |
| entity.MoveState.startGrid = entity.MoveState.endGrid; | |
| entity.MoveState.homeGrid = entity.MoveState.endGrid; | |
| if (onFinish) onFinish.call(); | |
| } | |
| return; | |
| }, | |
| blockMove: function(entity, changeView = false) { | |
| let newGrid = entity.MoveState.startGrid.add(entity.MoveState.dir); | |
| entity.MoveState.reset(newGrid); | |
| GRID.gridToSprite(newGrid, entity.actor); | |
| entity.actor.orientation = entity.actor.getOrientation( | |
| entity.MoveState.dir | |
| ); | |
| entity.actor.animateMove(entity.actor.orientation); | |
| if (changeView) { | |
| ENGINE.VIEWPORT.check(entity.actor); | |
| } | |
| ENGINE.VIEWPORT.alignTo(entity.actor); | |
| }, | |
| teleportToGrid: function(entity, grid, changeView = false) { | |
| entity.MoveState.reset(grid); | |
| GRID.gridToSprite(grid, entity.actor); | |
| if (changeView) { | |
| ENGINE.VIEWPORT.check(entity.actor); | |
| } | |
| ENGINE.VIEWPORT.alignTo(entity.actor); | |
| }, | |
| findDungeonPath: function( | |
| start, | |
| finish, | |
| obstacles, | |
| limit = ENGINE.INI.MAX_PATH, | |
| firstDir = new Vector(0, 0) | |
| ) { | |
| //for dungeons with rooms | |
| const finishID = MAP[GAME.level].DUNGEON.isInWhichRoom(finish); | |
| var Q = new NodeQ(); | |
| Q.list.push(new Node(start, finish, [firstDir])); | |
| if (Q.list[0].dist === 0) return null; | |
| var selected; | |
| var round = 0; | |
| while (Q.list.length > 0) { | |
| selected = Q.list.shift(); | |
| if (selected.path > limit) { | |
| selected.status = "Excess"; | |
| selected.stack.splice(0, 1); | |
| return selected; | |
| } | |
| const selectedID = MAP[GAME.level].DUNGEON.isInWhichRoom(selected.grid); | |
| if ((selectedID && finishID && selectedID.id !== finishID.id) || (selectedID && finishID === null)) { | |
| let doors = selectedID.door.clone(); | |
| for (let i = doors.length - 1; i >= 0; i--){ | |
| if (doors[i].isInAt(selected.history) >= 0){ | |
| doors.splice(i, 1); | |
| } | |
| } | |
| for (let i = doors.length - 1; i >= 0; i--){ | |
| if (doors[i].isInAt(MAP[GAME.level].DUNGEON.obstacles) >= 0){ | |
| doors.splice(i, 1); | |
| } | |
| } | |
| let Q_array = []; | |
| if (doors.length > 0){ | |
| for (let i = doors.length - 1; i >= 0; i--){ | |
| let newDoorNode = GRID.findPath(selected.grid, doors[i], MAP[GAME.level].DUNGEON.obstacles, 99, selected.stack[selected.stack.length - 1]); | |
| Q_array.push(newDoorNode); | |
| } | |
| } | |
| for (let i = 0, LN = Q_array.length; i < LN; i++){ | |
| let node = selected.append(Q_array[i], finish); | |
| Q.queue(node); | |
| } | |
| } else { | |
| let nodes = GRID.FP.nextNodesFromQueue(selected, finish, round, obstacles); | |
| for (let q = 0; q < nodes.length; q++){ | |
| if (nodes[q].dist === 0) { | |
| nodes[q].stack.splice(0, 1); | |
| nodes[q].status = "Found"; | |
| return nodes[q]; | |
| } else Q.queue(nodes[q]); | |
| } | |
| } | |
| round++; | |
| if (round > ENGINE.INI.PATH_ROUNDS) { | |
| break; | |
| } | |
| } | |
| return GRID.FP.returnSolution(Q, selected); | |
| }, | |
| findPath: function( | |
| start, | |
| finish, | |
| obstacles, | |
| limit = ENGINE.INI.MAX_PATH, | |
| firstDir = new Vector(0, 0) | |
| ) { | |
| //for corridors only | |
| var Q = new NodeQ(); | |
| Q.list.push(new Node(start, finish, [firstDir])); | |
| if (Q.list[0].dist === 0) return null; | |
| var selected; | |
| var round = 0; | |
| while (Q.list.length > 0) { | |
| selected = Q.list.shift(); | |
| if (selected.path > limit) { | |
| selected.status = "Excess"; | |
| selected.stack.splice(0, 1); | |
| return selected; | |
| } | |
| let nodes = GRID.FP.nextNodesFromQueue(selected, finish, round, obstacles); | |
| for (let q = 0; q < nodes.length; q++){ | |
| if (nodes[q].dist === 0) { | |
| nodes[q].stack.splice(0, 1); | |
| nodes[q].status = "Found"; | |
| return nodes[q]; | |
| } else Q.queue(nodes[q]); | |
| } | |
| round++; | |
| if (round > ENGINE.INI.PATH_ROUNDS) { | |
| break; | |
| } | |
| } | |
| return GRID.FP.returnSolution(Q, selected); | |
| }, | |
| FP: { | |
| nextNodesFromQueue: function(selected, finish, round, obstacles){ | |
| let nodes = []; | |
| let dirs = GRID.getDirections(selected.grid, obstacles); | |
| let back = selected.stack[selected.stack.length - 1].mirror(); | |
| let iB = back.isInAt(dirs); | |
| if (iB !== -1) { | |
| let waste = dirs.splice(iB, 1); | |
| } | |
| for (let q = 0; q < dirs.length; q++) { | |
| let HG = selected.grid.add(dirs[q]); | |
| if (HG.isInAt(selected.history) >= 0) { | |
| continue; | |
| } | |
| let history = [].concat(selected.history); | |
| history.push(HG); | |
| let I_stack = [].concat(selected.stack); | |
| I_stack.push(dirs[q]); | |
| let node = new Node(HG, finish, I_stack, selected.path + 1, history, round); | |
| nodes.push(node); | |
| } | |
| return nodes; | |
| }, | |
| returnSolution: function(Q, selected){ | |
| if (Q.list.length > 0) { | |
| Q.list[0].stack.splice(0, 1); | |
| Q.list[0].status = "Abandoned"; | |
| return Q.list[0]; | |
| } else { | |
| selected.status = "NoSolution"; | |
| selected.stack.splice(0, 1); | |
| return selected; | |
| } | |
| } | |
| }, | |
| findPathToFirstCrossroad: function( | |
| start, | |
| finish, | |
| firstDir = new Vector(0, 0) | |
| ) { | |
| let path = GRID.findPath(start, finish, firstDir); | |
| let len = GRID.findLengthToCrossroad(start, path); | |
| if (len > 0) path.splice(len); | |
| return path; | |
| }, | |
| paintPath: function(layer, color, path, start, z = 0) { | |
| if (path === null) return; | |
| var CTX = LAYER[layer]; | |
| ENGINE.clearLayer(layer); | |
| CTX.strokeStyle = color; | |
| var point = GRID.gridToCenterPX(start); | |
| point.toViewport(); | |
| var PL = path.length; | |
| CTX.beginPath(); | |
| CTX.moveTo(point.x + z, point.y + z); | |
| for (let q = 0; q < PL; q++) { | |
| point = point.translate(path[q]); | |
| CTX.lineTo(point.x + z, point.y + z); | |
| CTX.stroke(); | |
| } | |
| }, | |
| AI: { | |
| // assumes HERO is hunted entity; please refactor this! | |
| advancer: { | |
| hunt: function() { | |
| let next = GRID.findCrossroadAndLastDir( | |
| HERO.MoveState.startGrid, | |
| HERO.MoveState.dir | |
| ); | |
| let nextCR = next.finish; | |
| let directions = GRID.getDirections(nextCR); | |
| let back = next.dir.mirror(); | |
| let BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| if (HERO.MoveState.dir.isInAt(directions) !== -1) { | |
| return { | |
| type: "grid", | |
| return: GRID.findCrossroad( | |
| nextCR.add(HERO.MoveState.dir), | |
| HERO.MoveState.dir | |
| ) | |
| }; | |
| } else { | |
| let LNs = []; | |
| let CRs = []; | |
| for (let q = 0; q < directions.length; q++) { | |
| CRs.push( | |
| GRID.findCrossroad(nextCR.add(directions[q]), directions[q]) | |
| ); | |
| LNs.push(CRs[q].distance(HERO.MoveState.startGrid)); | |
| } | |
| let qq = LNs.indexOf(Math.min(...LNs)); | |
| return { type: "grid", return: CRs[qq] }; | |
| } | |
| } | |
| }, | |
| default: { | |
| hunt: function() { | |
| return { | |
| type: "grid", | |
| return: GRID.findCrossroad( | |
| HERO.MoveState.startGrid, | |
| HERO.MoveState.dir | |
| ) | |
| }; | |
| } | |
| }, | |
| shadower: { | |
| hunt: function(MS, tolerance) { | |
| let solutions = MS.endGrid.directionSolutions(HERO.MoveState.homeGrid); | |
| let directions = GRID.getDirections(MS.endGrid); | |
| let back = MS.dir.mirror(); | |
| let BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| let selected; | |
| if (directions.length === 1) { | |
| selected = directions[0]; | |
| } else { | |
| if ( | |
| MS.goingAway(HERO.MoveState) || | |
| !MS.towards(HERO.MoveState, tolerance) | |
| ) { | |
| if (HERO.MoveState.dir.isInAt(directions) !== -1) { | |
| selected = HERO.MoveState.dir; | |
| } else selected = solve(); | |
| } else { | |
| let contra = HERO.MoveState.dir.mirror(); | |
| if (contra.isInAt(directions) !== -1) { | |
| selected = contra; | |
| } else selected = solve(); | |
| } | |
| } | |
| if (!selected) { | |
| selected = directions.chooseRandom(); | |
| } | |
| let path = GRID.pathToCrossroad(MS.endGrid, selected); | |
| return { type: "path", return: path }; | |
| function solve() { | |
| for (let q = 0; q < 2; q++) { | |
| if (solutions[q].dir.isInAt(directions) !== -1) | |
| return solutions[q].dir; | |
| } | |
| return null; | |
| } | |
| } | |
| }, | |
| follower: { | |
| hunt: function() { | |
| return { | |
| type: "grid", | |
| return: GRID.findCrossroad( | |
| HERO.MoveState.startGrid, | |
| HERO.MoveState.dir.mirror() | |
| ) | |
| }; | |
| } | |
| }, | |
| wanderer: { | |
| hunt: function(MS) { | |
| let directions = GRID.getDirections(MS.endGrid); | |
| if (directions.length > 1) { | |
| let back = MS.dir.mirror(); | |
| let BI = back.isInAt(directions); | |
| if (BI !== -1) directions.splice(BI, 1); | |
| } | |
| let selected = directions.chooseRandom(); | |
| let path = GRID.pathToCrossroad(MS.endGrid, selected); | |
| return { type: "path", return: path }; | |
| } | |
| }, | |
| keepTheDistance: { | |
| hunt:function(MS, reference, setDistance){ | |
| // if all distances below setDistance it does nothing --> it should run if possible | |
| //console.log("keeping the distance", arguments); | |
| let directions = GRID.getDirections(MS.endGrid, MAP[GAME.level].DUNGEON.obstacles); | |
| //console.log("directions", directions); | |
| let possible = []; | |
| let max = []; | |
| let curMax = 0; | |
| for (let i = 0; i < directions.length; i++){ | |
| let test = MS.endGrid.add(directions[i]); | |
| let distance = test.distanceDiagonal(reference); | |
| console.log(i, directions[i], test, distance); | |
| if (distance === setDistance) possible.push(directions[i]); | |
| if (distance > curMax){ | |
| max.clear(); | |
| curMax = distance; | |
| max.push(directions[i]); | |
| } else if (distance === curMax) max.push(directions[i]); | |
| } | |
| let path; | |
| //console.log("possible", possible, max); | |
| if (possible.length > 0){ | |
| path = [possible.chooseRandom()]; | |
| } else if (max.length > 0){ | |
| path = [max.chooseRandom()]; | |
| } else path = []; | |
| return { type: "path", return: path }; | |
| } | |
| } | |
| }, | |
| gridToIndex: function(grid) { | |
| return grid.x + grid.y * MAP[GAME.level].width; | |
| }, | |
| indexToGrid: function(index) { | |
| let x = index % MAP[GAME.level].width; | |
| let y = Math.floor(index / MAP[GAME.level].width); | |
| return new Grid(x, y); | |
| }, | |
| vision: function(startGrid, endGrid) { | |
| if (GRID.same(startGrid, endGrid)) return true; | |
| let path = GRID.raycasting(startGrid, endGrid); | |
| return GRID.pathClear(path); | |
| }, | |
| raycasting: function(startGrid, endGrid) { | |
| let normDir = startGrid.direction(endGrid); | |
| let path = []; | |
| path.push(Grid.toClass(startGrid)); | |
| let x = startGrid.x; | |
| let y = startGrid.y; | |
| let dx = Math.abs(endGrid.x - x); | |
| let dy = -Math.abs(endGrid.y - y); | |
| let Err = dx + dy; | |
| let E2, node; | |
| do { | |
| E2 = Err * 2; | |
| if (E2 >= dy) { | |
| Err += dy; | |
| x += normDir.x; | |
| } | |
| if (E2 <= dx) { | |
| Err += dx; | |
| y += normDir.y; | |
| } | |
| node = new Grid(x, y); | |
| path.push(node); | |
| } while (!GRID.same(node, endGrid)); | |
| return path; | |
| }, | |
| pathClear: function(path) { | |
| if (path.length === 0) return true; | |
| for (let q = 0; q < path.length; q++) { | |
| if (GRID.gridIsBlock(path[q])) return false; | |
| } | |
| return true; | |
| } | |
| }; | |
| class Node { | |
| constructor(HG, goal, stack, path, history, iterations) { | |
| this.grid = HG; | |
| this.stack = stack; | |
| this.history = history || [HG]; | |
| this.path = path || 0; | |
| this.dist = this.grid.distance(goal); | |
| this.priority = this.path + this.dist; | |
| this.status = "Progress"; | |
| this.iterations = iterations || 0; | |
| } | |
| append(node, goal){ | |
| let stack = this.stack.concat(node.stack); | |
| let history = this.history.concat(node.history.slice(1)); | |
| let path = this.path + node.path; | |
| return new Node(node.grid, goal, stack, path, history); | |
| } | |
| } | |
| class NodeQ { | |
| constructor() { | |
| this.list = []; | |
| } | |
| queue(node) { | |
| var included = false; | |
| for (let q = 0; q < this.list.length; q++) { | |
| if (this.list[q].priority >= node.priority) { | |
| this.list.splice(q, 0, node); | |
| included = true; | |
| break; | |
| } | |
| } | |
| if (!included) this.list.push(node); | |
| } | |
| } | |
| class MoveState { | |
| constructor(startGrid, dir) { | |
| this.startGrid = Grid.toClass(startGrid); | |
| this.dir = dir || null; | |
| this.homeGrid = Grid.toClass(startGrid); | |
| this.endGrid = Grid.toClass(startGrid); | |
| this.moving = false; | |
| } | |
| setEnd() { | |
| if (this.dir !== null) { | |
| this.endGrid = this.startGrid.add(this.dir); | |
| this.moving = true; | |
| } | |
| } | |
| next(dir) { | |
| if (dir !== null) { | |
| this.startGrid = this.endGrid; | |
| this.dir = dir; | |
| this.setEnd(); | |
| } | |
| } | |
| flip() { | |
| this.homeGrid = this.startGrid; | |
| this.startGrid = this.endGrid; | |
| this.endGrid = this.homeGrid; | |
| } | |
| reverse() { | |
| this.dir = this.dir.mirror(); | |
| this.flip(); | |
| } | |
| goingAway(MS) { | |
| let oldDistance = this.homeGrid.distance(MS.startGrid); | |
| let newDistance = this.homeGrid.distance(MS.startGrid.add(MS.dir)); | |
| return newDistance > oldDistance; | |
| } | |
| towards(MS, tolerance = 5) { | |
| let oldDistance = this.homeGrid.distance(MS.startGrid); | |
| let newDistance = this.homeGrid.distance(MS.startGrid.add(MS.dir)); | |
| return newDistance < oldDistance && newDistance < tolerance; | |
| } | |
| closerGrid(MS) { | |
| if ( | |
| this.startGrid.distance(MS.homeGrid) < this.endGrid.distance(MS.homeGrid) | |
| ) { | |
| return this.startGrid; | |
| } else { | |
| return this.endGrid; | |
| } | |
| } | |
| reset(grid) { | |
| this.startGrid = Grid.toClass(grid); | |
| this.homeGrid = Grid.toClass(grid); | |
| this.endGrid = Grid.toClass(grid); | |
| this.moving = false; | |
| } | |
| } | |
| var VIEW = { | |
| init: function() { | |
| VIEW.x = 0; | |
| VIEW.y = 0; | |
| VIEW.speed = 1; | |
| VIEW.actor = new ACTOR(null, 0, 0); | |
| }, | |
| move: function(dir) { | |
| VIEW.actor.x += VIEW.speed * ENGINE.INI.GRIDPIX * dir.x; | |
| VIEW.actor.y += VIEW.speed * ENGINE.INI.GRIDPIX * dir.y; | |
| if (VIEW.actor.x < 0) VIEW.actor.x = 0; | |
| if (VIEW.actor.y < 0) VIEW.actor.y = 0; | |
| if (VIEW.actor.x > ENGINE.VIEWPORT.max.x) | |
| VIEW.actor.x = ENGINE.VIEWPORT.max.x; | |
| if (VIEW.actor.y > ENGINE.VIEWPORT.max.y) | |
| VIEW.actor.y = ENGINE.VIEWPORT.max.y; | |
| ENGINE.VIEWPORT.check(VIEW.actor); | |
| ENGINE.VIEWPORT.alignTo(VIEW.actor); | |
| } | |
| }; | |
| var FORM = { | |
| INI: { | |
| DIV: "#ROOM" | |
| } | |
| }; | |
| class Form { | |
| constructor(name, x, y, w, h, wedge) { | |
| this.name = name; | |
| this.x = x; | |
| this.y = y; | |
| this.w = w; | |
| this.h = h; | |
| $(FORM.INI.DIV).append( | |
| `<div id = 'FORM' class = 'form'><h1>${this.name}</h1><hr></div>` | |
| ); | |
| $("#FORM").css({ | |
| top: this.y, | |
| left: this.x, | |
| width: this.w, | |
| height: this.h | |
| }); | |
| $("#FORM").append(wedge); | |
| } | |
| } | |
| class Inventory { | |
| constructor() { | |
| this.list = []; | |
| } | |
| add(element) { | |
| for (let q = 0, QL = this.list.length; q < QL; q++) { | |
| let item = this.list[q].object; | |
| if (element.id === item.id) { | |
| this.list[q].count++; | |
| return; | |
| } | |
| } | |
| this.list.push(new Item(element, 1)); | |
| } | |
| remove(index) { | |
| let element = this.list[index].object; | |
| this.list[index].count--; | |
| if (this.list[index].count === 0) this.list.splice(index, 1); | |
| return element; | |
| } | |
| size() { | |
| return this.list.length; | |
| } | |
| find(prop, value) { | |
| for (let q = 0, QL = this.list.length; q < QL; q++) { | |
| let item = this.list[q].object; | |
| if (item[prop] === value) return q; | |
| } | |
| return null; | |
| } | |
| getCount(prop, value) { | |
| let index = this.find(prop, value); | |
| if (index !== null) { | |
| return this.list[index].count; | |
| } else return 0; | |
| } | |
| } | |
| class Item { | |
| constructor(object, count) { | |
| this.object = object; | |
| this.count = count; | |
| } | |
| } | |
| class SimpleTimer { | |
| constructor(seconds, func) { | |
| this.value = seconds; | |
| this.start = performance.now(); | |
| this.now = null; | |
| this.delta = null; | |
| this.func = func; | |
| } | |
| update() { | |
| this.now = performance.now(); | |
| this.delta = Math.round((this.now - this.start) / 1000); | |
| if (this.delta >= this.value) this.func.call(); | |
| } | |
| } | |
| class Timer { | |
| constructor() { | |
| this.start = Date.now(); | |
| } | |
| time() { | |
| let time = (Date.now() - this.start) / 1000; | |
| let hours = Math.floor(time / 3600); | |
| let min = Math.floor((time % 3600) / 60); | |
| let sec = Math.floor((time % 3600) % 60); | |
| return { h: hours, m: min, s: sec }; | |
| } | |
| timeString() { | |
| let time = this.time(); | |
| let str = time.h.toString().padStart(2, "0") + ":"; | |
| str += time.m.toString().padStart(2, "0") + ":"; | |
| str += time.s.toString().padStart(2, "0"); | |
| return str; | |
| } | |
| } | |
| var CONSOLE = { | |
| id: "Console", | |
| set: function(id) { | |
| CONSOLE.id = id; | |
| }, | |
| print: function(text) { | |
| $(`#${CONSOLE.id}`).append(`<p>${text}</p>`); | |
| $(`#${CONSOLE.id}`) | |
| .children() | |
| .last()[0] | |
| .scrollIntoView(); | |
| } | |
| }; | |
| //END | |
| console.log(`%cENGINE ${ENGINE.VERSION} loaded.`, ENGINE.CSS); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment