webai/archive/game-engine-reference.js
2024-08-04 14:16:35 -07:00

941 lines
20 KiB
JavaScript

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);
}