var HOLD_TO_MOVE = false; var PATH_DIAGONAL = true; //Contstants var tau = 6.283185307179586476925286766559; var pi = 3.1415926535897932384626433832795; var tau_over_4 = 1.57079632679; var tau_over_8 = 0.785398163; var sqrt_2 = 1.41421356237; var sqrt_2_over_2 = 0.70710678118; var dtime = 17; var last_timestamp = 0; //Key Constants var K_W = 87; var K_A = 65; var K_S = 83; var K_D = 68; var K_UPARROW = 38; var K_DOWNARROW = 40; var K_LEFTARROW = 37; var K_RIGHTARROW = 39; //Prototypes Array.prototype.max = function() { return Math.max.apply(null, this); }; Array.prototype.min = function() { return Math.min.apply(null, this); }; //Useful Functions //Create a unique id var p_id = 1; function get_id() { return p_id++; } //Make a Vec2 Faster function vec2(x, y) { return new Vec2(x, y); } function clamp(a, min, max) { return Math.min(Math.max(a, min), max); } //Dampens a number based on a dt. The threshold is the minimum value before it just returns 0 function dampen(x, m, dt, threshold) { var ret = Math.pow(m, dt) * x; return (Math.abs(ret) > threshold ? ret : 0); } //Logs all arguments using the first one as the seperator function logAll(sep) { var s = ""; for (var i = 1; i < arguments.length; i++) { s += arguments[i] + sep; } console.log(s); } //Returns current time in milliseconds from 1970 function time() { return (new Date()).getTime(); } //Returns a random number between plus or minus x function variance(x) { return (Math.random() - 0.5) * 2 * x } //Returns a random number in [0, max) function rand(max) { return max * Math.random(); } function randSelect(arr) { var index = Math.floor(rand(arr.length)); if (index == arr.length) index = arr.length - 1; return arr[index]; } //Relative Position Calculation function screenPos(pos) { return pos.minus(cameraPos); } function worldPos(screenPos) { return screenPos.plus(cameraPos); } //Draw Functions function setFillStyle(style) { ctx.fillStyle = style; } function setStrokeStyle(style) { ctx.strokeStyle = style; } function setLineWidth(width) { ctx.lineWidth = width; } /* Possible Values * butt * round * square */ function setLineCap(lineCap) { ctx.lineCap = lineCap; } function setFont(font) { ctx.font = font; } /* Possible Values * start (default) * end * center * left * right */ function setTextAlign(textalign) { ctx.textAlign = textalign; } /* Possible Values * alphabetic (default) * top * hanging * middle * ideographic * bottom */ function setTextBaseline(textbaseline) { ctx.textBaseline = textbaseline; } function measureText(text) { return ctx.measureText(text); } function fillCircle(pos, r) { ctx.beginPath(); ctx.arc(pos.x - cameraPos.x, pos.y - cameraPos.y, r, 0, tau, false); ctx.fill(); } function fillRect(x, y, w, h) { ctx.fillRect(x - cameraPos.x, y - cameraPos.y, w, h); } function fillImage(x, y, w, h, img) { ctx.drawImage(img, x - cameraPos.x, y - cameraPos.y, w, h); } //Fills an image rotated **With Center as Origin** function fillImageRotated(x, y, w, h, img, angle) { ctx.save(); ctx.translate(x - cameraPos.x, y - cameraPos.y); ctx.rotate(angle); ctx.drawImage(img, -w / 2, -h / 2, w, h); ctx.restore(); } //Fills an animated image rotated **With Center as Origin** //Works with vertically animated images function fillAnimatedImageRotated(x, y, w, h, img, angle, frame, wpf, hpf) { ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.drawImage(img, 0, frame * hpf, wpf, hpf, -w / 2, -h / 2, w, h); ctx.restore(); } function fillTileRect(x, y) { fillRect(x * tileSize.x, y * tileSize.y, tileSize.x + tileSizeDrawDelta, tileSize.y * tileSizeDrawDelta); } function fillRectRel(x, y, w, h) { ctx.fillRect(x, y, w, h); } function fillText(text, x, y) { ctx.fillText(text, x - cameraPos.x, y - cameraPos.y); } function fillTextRel(text, x, y) { ctx.fillText(text, x, y); } function drawLine(x1, y1, x2, y2) { ctx.beginPath(); ctx.moveTo(x1 - cameraPos.x, y1 - cameraPos.y); ctx.lineTo(x2 - cameraPos.x, y2 - cameraPos.y); ctx.stroke(); } //Draws an arrow from p1 to p2 with a 90 degree point of length n function drawArrow(x1, y1, x2, y2, n = 10) { drawLine(x1, y1, x2, y2); var v = vec2(x1 - x2, y1 - y2); var nub = Vec2.normalize(v.rotate(tau_over_8)).times(n); drawLine(x2, y2, x2 + nub.x, y2 + nub.y); nub = nub.rotate(-tau_over_4); drawLine(x2, y2, x2 + nub.x, y2 + nub.y); } function clearCanvas() { ctx.fillRect(0, 0, c.width, c.height); } //Other Draw Functions function drawStatusBar(x, y, w, h, across, bgColor, fgColor) { setFillStyle(bgColor); fillRect(x, y, w, h); setFillStyle(fgColor); fillRect(x, y, across * w, h); } function fillRoundedRect(x, y, w, h, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); //top ctx.lineTo(x + w - radius, y); //top right ctx.quadraticCurveTo(x + w, y, x + w, y + radius); //right ctx.lineTo(x + w, y + h - radius); //bottom right ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h); //bottom ctx.lineTo(x + radius, y + h); //bottom left ctx.quadraticCurveTo(x, y + h, x, y + h - radius); //left ctx.lineTo(x, y + radius); //top left ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); ctx.fill(); } //Returns text in array of lines with measureText x of width or less //Text is split by word (seperator is spaces) function getTextLines(text, width) { var ret = []; var lineStart = 0; var prev = 0; var iter = 0; while (true) { iter = text.indexOf(" ", iter + 1); if (iter == -1) { ret.push(text.slice(lineStart, text.length)); return ret; } var len = measureText(text.slice(lineStart, iter)).width; if (len > width) { ret.push(text.slice(lineStart, prev)); lineStart = iter = prev + 1; } else { prev = iter; } } } //fills a text box with fixed width //s = the fixed size //px, py = padding in x / y //lh = line height //ls = line spacing //radius = rounded radius (optional) //text top left aligned. function fillTextBox(text, fgColor, bgColor, x, y, px, py, w, lh, ls, alpha, radius) { ctx.save(); ctx.globalAlpha = alpha; var textLines = text.split("\n"); var drawnTextLines = []; for (var i = 0; i < textLines.length; i++) { drawnTextLines = drawnTextLines.concat(getTextLines(textLines[i], w - py * 2)); } var totalHeight = drawnTextLines.length * (lh + ls) - ls + py * 2; setFillStyle(bgColor); if (typeof(radius) == "number") { fillRoundedRect(x, y, w, totalHeight, radius); } else { fillRect(x, y, w, totalHeight); } setFillStyle(fgColor); setTextAlign("left"); setTextBaseline("top"); var offsetY = y + py; for (var i = 0; i < drawnTextLines.length; i++) { fillText(drawnTextLines[i], x + px, offsetY); offsetY += lh + ls; } ctx.restore(); } //Drawable Classes class Slider { constructor(pos, width, lineWidth, lineColor, radius, circleColor, value, minVal, maxVal) { this.pos = pos.copy(); this.width = width; this.lineWidth = lineWidth; this.lineColor = lineColor; this.radius = radius; this.circleColor = circleColor; this.value = value; this.minVal = minVal; this.maxVal = maxVal; this.transitioning = false; } update(t) { if (mousePressed) { if (pointInRect(mousePos, this.pos.minus(vec2(0, this.radius)), vec2(this.width, this.radius * 2))) { this.transitioning = true; } } if (!mouseDown) { this.transitioning = false; } if (this.transitioning) { this.value = (((clamp(mousePos.x, this.pos.x, this.pos.x + this.width) - this.pos.x) / this.width) * (this.maxVal - this.minVal) + this.minVal); } } draw(t) { //Draw Line setLineWidth(this.lineWidth); setLineCap("round"); setStrokeStyle(this.lineColor); drawLine(this.pos.x, this.pos.y, this.pos.x + this.width, this.pos.y); setFillStyle(this.circleColor); fillCircle(vec2(this.pos.x + (this.width * (this.value - this.minVal) / (this.maxVal - this.minVal)), this.pos.y), this.radius); } } //Converts a world to a grid for pathing //False = Blocked, True = Open function gridFromWorld(world) { grid = []; world.forEach((o, i) => { grid.push([]); o.forEach((l) => { if (l == "#") { grid[i].push(false); } else { grid[i].push(true); } }); }); return grid; } //Grid Position Calculation //Center of Tile function gridPosToWorldPos(pos) { return vec2(pos.x * tileSize.x + tileSize.x / 2, pos.y * tileSize.y + tileSize.y / 2); } //Top Left Of Tile function gridPosToWorldPosTL(pos) { return vec2(pos.x * tileSize.x, pos.y * tileSize.y); } function worldPosToGridPos(pos) { return vec2( Math.floor(pos.x / tileSize.x), Math.floor(pos.y / tileSize.y) ); } //Path Finding //Returns the value of a grid at the specified position function gridAt(grid, pos) { if (pos.y < 0 || pos.x < 0 || pos.y >= grid.length || pos.x >= grid[pos.y].length) { return false; } return grid[pos.y][pos.x]; } //Returns neighbors of a position node function getNeighbors(grid, pos) { ret = []; if (gridAt(grid, pos.plus(vec2(1, 0)))) { ret.push(pos.plus(vec2(1, 0))); } if (gridAt(grid, pos.plus(vec2(-1, 0)))) { ret.push(pos.plus(vec2(-1, 0))); } if (gridAt(grid, pos.plus(vec2(0, 1)))) { ret.push(pos.plus(vec2(0, 1))); } if (gridAt(grid, pos.plus(vec2(0, -1)))) { ret.push(pos.plus(vec2(0, -1))); } if (PATH_DIAGONAL) { //TODO } return ret; } function heuristic(a, b) { return Vec2.distance(a, b); } function createPath(cameFrom, a, start) { ret = [a]; while (!a.equals(start)) { a = cameFrom[a.h()]; ret.unshift(a); } return ret; } function getPath(grid, start, goal) { //Returns empty list on failure otherwise a list of grid coords gScore = {}; gScore[start.h()] = 0; fScore = {}; fScore[start.h()] = heuristic(start, goal); openSet = [start]; closedSet = []; cameFrom = {}; success = false; while (openSet.length != 0) { openf = openSet.map((o) => { return fScore[o.h()]; }); var ci = openf.indexOf(openf.min()); var c = openSet[ci].copy(); if (c.equals(goal)) { //return finished path return createPath(cameFrom, c, start); } openSet.splice(ci, 1); closedSet.push(c); //Possible Speedup: Also Make closedSets set grid values to //blocked once they are in the closedSet var neighbors = getNeighbors(grid, c); for (var i = 0; i < neighbors.length; i++) { var o = neighbors[i]; if (o.inArray(closedSet)) { //Path already evaluated continue; } if (!o.inArray(openSet)) { openSet.push(o); } var tgScore = gScore[c.h()] + Vec2.distance(c, o); var cgScore = gScore[o.h()]; if (typeof(cgScore) !== "undefined" && tgScore > cgScore) { //Not a better path continue; } cameFrom[o.h()] = c; gScore[o.h()] = tgScore; fScore[o.h()] = tgScore + heuristic(o, goal); } } return []; } function getWorldPath(start, goal) { var path = getPath(worldGrid, worldPosToGridPos(start), worldPosToGridPos(goal)); path = path.map((o) => { return gridPosToWorldPos(o); }); if (path.length > 0) { path.push(goal); } return path; } //Collision Detection function pointInRect(pos, rPos, rSize) { return ( pos.x >= rPos.x && pos.y >= rPos.y && pos.x <= rPos.x + rSize.x && pos.y <= rPos.y + rSize.y ); } function rectInRect(r1Pos, r1Size, r2Pos, r2Size) { return ( pointInRect(r2Pos, r1Pos, r1Size) || pointInRect(r2Pos.plusv(r2Size.x, 0), r1Pos, r1Size) || pointInRect(r2Pos.plusv(0, r2Size.y), r1Pos, r1Size) || pointInRect(r2Pos.plus(r2Size), r1Pos, r1Size) || pointInRect(r1Pos, r2Pos, r2Size) || pointInRect(r1Pos.plusv(r1Size.x, 0), r2Pos, r2Size) || pointInRect(r1Pos.plusv(0, r1Size.y), r2Pos, r2Size) || pointInRect(r1Pos.plus(r1Size), r2Pos, r2Size) ); } function circleInRect(cPos, cRadius, rPos, rSize) { return ( //Rect Points In Circle pointInCircle(rPos, cPos, cRadius) || pointInCircle(rPos.plusv(rSize.x, 0), cPos, cRadius) || pointInCircle(rPos.plusv(0, rSize.y), cPos, cRadius) || pointInCircle(rPos.plus(rSize), cPos, cRadius) || //Circle Points In Rect pointInRect(cPos.minusv(cRadius, 0), rPos, rSize) || pointInRect(cPos.minusv(-cRadius, 0), rPos, rSize) || pointInRect(cPos.minusv(0, cRadius), rPos, rSize) || pointInRect(cPos.minusv(0, -cRadius), rPos, rSize) ); } function lineFunc(x1, y1, x2, y2) { return (x, y) => { return (y2 - y1) * x + (x1 - x2) * y + (x2 * y1 - x1 * y2); }; } function collideLineRect(lx1, ly1, lx2, ly2, rx1, ry1, rx2, ry2) { //Projected No Intersection if (lx1 > rx1 && lx1 > rx2 && lx2 > rx1 && lx2 > rx2) { return false; } if (lx1 < rx1 && lx1 < rx2 && lx2 < rx1 && lx2 < rx2) { return false; } if (ly1 > ry1 && ly1 > ry2 && ly2 > ry1 && ly2 > ry2) { return false; } if (ly1 < ry1 && ly1 < ry2 && ly2 < ry1 && ly2 < ry2) { return false; } //All points same side of line var f = lineFunc(lx1, lx2, ly1, ly2); p = 0; n = 0; var a = f(rx1, ry1); if (a > 0) { p += 1; } else if (a < 0) { n += 1; } else { return true; } a = f(rx1, ry2); if (a > 0) { p += 1; } else if (a < 0) { n += 1; } else { return true; } a = f(rx2, ry1); if (a > 0) { p += 1; } else if (a < 0) { n += 1; } else { return true; } a = f(rx2, ry2); if (a > 0) { p += 1; } else if (a < 0) { n += 1; } else { return true; } if (p == 4 || n == 4) { return false; } return true; } function isClear(grid, start, end, radius) { //Returns whether the path is clear of world tiles from the start to the end var rad = typeof(radius) === "undefined" ? 0 : radius; var startGrid = worldPosToGridPos(start); var endGrid = worldPosToGridPos(end); var sx = 0;//Math.max(Math.min(startGrid.x, endGrid.x), 0); var ex = grid[0].length - 1;//Math.min(Math.max(startGrid.x, endGrid.x), grid[0].length - 1); var sy = 0;//Math.max(Math.min(startGrid.y, endGrid.y), 0); var ey = grid.length - 1;//Math.min(Math.max(startGrid.y, endGrid.y), grid.length - 1); var dr = null; var s1 = null; var s2 = null; var e1 = null; var e2 = null; if (rad != 0) { var d = end.minus(start); d = Vec2.normalize(d).times(rad); dr = vec2(-d.y, d.x); s1 = start.plus(dr); s2 = start.minus(dr); e1 = end.plus(dr); e2 = end.minus(dr); logAll(", ", dr.str(), start.str(), s1.str(), s2.str(), end.str(), e1.str(), e2.str()); } for (var x = sx; x <= ex; x++) { for (var y = sy; y <= ey; y++) { if (!gridAt(grid, vec2(x, y))) { var a = gridPosToWorldPosTL(vec2(x, y)); var b = gridPosToWorldPosTL(vec2(x + 1, y + 1)); if (collideLineRect(start.x, start.y, end.x, end.y, a.x, a.y, b.x, b.y)) { return false; } if (rad != 0) { if (collideLineRect(s1.x, s1.y, e1.x, e1.y, a.x, a.y, b.x, b.y)) { return false; } if (collideLineRect(s2.x, s2.y, e2.x, e2.y, a.x, a.y, b.x, b.y)) { return false; } } } } } return true; } function circleOnScreen(pos, radius) { return ( pos.x - cameraPos.x + radius >= 0 && pos.y - cameraPos.y + radius >= 0 && pos.x - cameraPos.x - radius <= c.width && pos.y - cameraPos.y - radius <= c.height ); } function squareOnScreen(pos, size) { return ( pos.x - cameraPos.x + size.x >= 0 && pos.y - cameraPos.y + size.x >= 0 && pos.x - cameraPos.x <= c.width && pos.y - cameraPos.y <= c.height ); } function circleInCircle(apos, arad, bpos, brad) { return circleCollision(apos, arad, bpos, brad); } function circleCollision(apos, arad, bpos, brad) { var r = arad + brad; var r2 = r * r; return apos.distanceToSqrd(bpos) < r2; } //Circle collision for 2 objects with a pos and radius variable function collision(a, b) { var r = (a.radius + b.radius); var r2 = r * r; return a.pos.distanceToSqrd(b.pos) < r2; } //Vector with 2 dimensions class Vec2 { constructor(x, y) { if (typeof(x) == 'undefined') x = 0; if (typeof(y) == 'undefined') y = 0; this.x = x; this.y = y; } copy() { return vec2(this.x, this.y); } setTo(other) { this.x = other.x; this.y = other.y; } setAngle(radians) { var mag = this.length(); this.x = mag * Math.cos(radians) this.y = mag * Math.sin(radians); } str(decimals) { return "< " + this.x.toFixed(decimals) + ", " + this.y.toFixed(decimals) + " >"; } h() { return this.x + "!" + this.y } isZero() { return this.x == 0 && this.y == 0; } equals(other) { return this.x == other.x && this.y == other.y; } plus(other) { return vec2(this.x + other.x, this.y + other.y); } modx(n) { var v = vec2(this.x % n, this.y); if (v.x < 0) { v.x += n; } return v; } mody(n) { var v = vec2(this.x, this.y % n); if (v.y < 0) { v.y += n; } return v; } minus(other) { return vec2(this.x - other.x, this.y - other.y); } rotate(radians) { //Get mag and angle, add degrees to angle, return from mag and angle var angle = Vec2.angle(this) + radians; var mag = this.length(); return vec2(mag * Math.cos(angle), mag * Math.sin(angle)); } times(scalar) { if (typeof(scalar) !== "number") { console.log("WARNING: Trying to multiply vector by non-number"); return this; } return vec2(this.x * scalar, this.y * scalar); } inArray(arr) { for (var i = 0; i < arr.length; i++) { if (this.equals(arr[i])) { return true; } } return false; } distanceTo(other) { return Vec2.distance(this, other); } distanceToSqrd(other) { return Vec2.distanceSqrd(this, other); } length() { return Math.sqrt(this.x * this.x + this.y * this.y); } lengthSqrd() { return this.x * this.x + this.y * this.y; } static distance(a, b) { var m = a.minus(b); return Math.sqrt(m.x * m.x + m.y * m.y); } static distanceSqrd(a, b) { var m = a.minus(b); return m.x * m.x + m.y * m.y; } static length(v) { return Math.sqrt(v.x * v.x + v.y * v.y); } static normalize(v) { var d = Vec2.length(v); if (d == 0) { return vec2(sqrt_2_over_2, sqrt_2_over_2); } return vec2(v.x / d, v.y / d); } static jiggle(v, maxdist) { //Changes the components of v by random amounts up to +/- maxdist //This is a square jiggle. return vec2(v.x + variance(maxdist), v.y + variance(maxdist)); } static angle(v) { if (v.x == 0) { if (v.y == 0) { return 0; } else if (v.y > 0) { return tau_over_4; } else { return -tau_over_4; } } else if (v.x > 0) { return Math.atan(v.y / v.x); } else { return Math.atan(v.y / v.x) + pi; } } } //Canvas Stuff var c; var ctx; //Engine Stuff //Key Maps - true = down, false = up //Stored as string -> bool map var keys = {} var keysPressed = {} var mousePos = vec2(); var mouseDown = false; var mousePressed = null; var mouseMiddleDown = false; var mouseMiddlePressed = null; var mouseRightDown = false; var mouseRightPressed = null; var cameraPos = vec2(); //Input window.onkeydown = keyDown; window.onkeyup = keyUp; window.onmousemove = mouseMove; window.onmousedown = md; window.onmouseup = mu; window.onload = load; window.oncontextmenu = function(e) { e.preventDefault(); }; function load() { initCanvas(); initGame(); startLoop(); } function keyDown(e) { keys[e.keyCode + ""] = true; keysPressed[e.keyCode + ""] = true; } function keyUp(e) { keys[e.keyCode + ""] = false; keysPressed[e.keyCode + ""] = false; } function mouseMove(e) { mousePos.x = e.clientX; mousePos.y = e.clientY; } //Mouse Down function md(e) { if (e.button == 0) { mouseDown = true; mousePressed = true; } else if (e.button == 1) { mouseMiddleDown = true; mouseMiddlePressed = true; } else if (e.button == 2) { mouseRightDown = true; mouseRightPressed = true; } //otherwise another mouse button } //Mouse Up function mu(e) { if (e.button == 0) { mouseDown = false; mousePressed = false; } else if (e.button == 1) { mouseMiddleDown = false; mouseMiddlePressed = false; } else if (e.button == 2) { mouseRightDown = false; mouseRightPressed = false; } //otherwise another mouse button } function initCanvas() { c = document.getElementById("game"); c.width = document.body.clientWidth; c.height = document.body.clientHeight; ctx = c.getContext("2d"); } function initGame() { console.log("Base Game Init"); } function update(timestamp) { } function draw(t) { ctx.fillStyle = "#ffffff"; clearCanvas(); } function startLoop() { window.requestAnimationFrame(loop); } function loop(timestamp) { dtime = timestamp - last_timestamp; last_timestamp = timestamp; update(timestamp, dtime); draw(timestamp, dtime); keysPressed = {}; mousePressed = null; mouseMiddlePressed = null; mouseRightPressed = null; window.requestAnimationFrame(loop); }