UNPKG

@dothq/littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

1,008 lines (1,006 loc) 127 kB
/* LittleJS - The Tiny JavaScript Game Engine That Can MIT License - Copyright 2019 Frank Force */ /* LittleJS Debug System Debug Features - debug overlay with mouse pick - debug primitive rendering - save screenshots */ 'use strict'; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; var debug = 1; var enableAsserts = 1; var debugPointSize = .5; var showWatermark = 1; var godMode = 0; var debugPrimitives = []; var debugOverlay = 0; var debugPhysics = 0; var debugRaycast = 0; var debugParticles = 0; var debugGamepads = 0; var debugMedals = 0; var debugCanvas = -1; var debugTakeScreenshot; var downloadLink; // debug helper functions var ASSERT = enableAsserts ? function () { var assert = []; for (var _i = 0; _i < arguments.length; _i++) { assert[_i] = arguments[_i]; } return console.assert.apply(console, assert); } : function () { }; var debugRect = function (pos, size, color, time, angle, fill) { if (size === void 0) { size = vec2(0); } if (color === void 0) { color = '#fff'; } if (time === void 0) { time = 0; } if (angle === void 0) { angle = 0; } if (fill === void 0) { fill = 0; } ASSERT(typeof color == 'string'); // pass in regular html strings as colors debugPrimitives.push({ pos: pos, size: vec2(size), color: color, time: new Timer(time), angle: angle, fill: fill }); }; var debugCircle = function (pos, radius, color, time, fill) { if (radius === void 0) { radius = 0; } if (color === void 0) { color = '#fff'; } if (time === void 0) { time = 0; } if (fill === void 0) { fill = 0; } ASSERT(typeof color == 'string'); // pass in regular html strings as colors debugPrimitives.push({ pos: pos, size: radius, color: color, time: new Timer(time), angle: 0, fill: fill }); }; var debugPoint = function (pos, color, time, angle) { return debugRect(pos, 0, color, time, angle); }; var debugLine = function (posA, posB, color, thickness, time) { if (thickness === void 0) { thickness = .1; } var halfDelta = vec2((posB.x - posA.x) / 2, (posB.y - posA.y) / 2); var size = vec2(thickness, halfDelta.length() * 2); debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), 1); }; var debugAABB = function (pA, pB, sA, sB, color) { var minPos = vec2(min(pA.x - sA.x / 2, pB.x - sB.x / 2), min(pA.y - sA.y / 2, pB.y - sB.y / 2)); var maxPos = vec2(max(pA.x + sA.x / 2, pB.x + sB.x / 2), max(pA.y + sA.y / 2, pB.y + sB.y / 2)); debugRect(minPos.lerp(maxPos, .5), maxPos.subtract(minPos), color); }; var debugText = function (text, pos, size, color, time, angle, font) { if (text === void 0) { text = ''; } if (size === void 0) { size = 1; } if (color === void 0) { color = '#fff'; } if (time === void 0) { time = 0; } if (angle === void 0) { angle = 0; } if (font === void 0) { font = 'monospace'; } ASSERT(typeof color == 'string'); // pass in regular html strings as colors debugPrimitives.push({ text: text, pos: pos, size: size, color: color, time: new Timer(time), angle: angle, font: font }); }; var debugClear = function () { return debugPrimitives = []; }; // save a canvas to disk var debugSaveCanvas = function (canvas, filename) { if (filename === void 0) { filename = engineName + '.png'; } downloadLink.download = 'screenshot.png'; downloadLink.href = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'); downloadLink.click(); }; /////////////////////////////////////////////////////////////////////////////// // engine debug function (called automatically) var debugInit = function () { // create link for saving screenshots document.body.appendChild(downloadLink = document.createElement('a')); downloadLink.style.display = 'none'; }; var debugUpdate = function () { if (!debug) return; if (keyWasPressed(192)) // ~ debugOverlay = !debugOverlay; if (keyWasPressed(49)) // 1 debugPhysics = !debugPhysics, debugParticles = 0; if (keyWasPressed(50)) // 2 debugParticles = !debugParticles, debugPhysics = 0; if (keyWasPressed(51)) // 3 godMode = !godMode; if (keyWasPressed(53)) // 5 debugTakeScreenshot = 1; //if (keyWasPressed(54)) // 6 // debugToggleParticleEditor(); if (keyWasPressed(55)) // 7 debugGamepads = !debugGamepads; //if (keyWasPressed(56)) // 8 //if (keyWasPressed(57)) // 9 if (keyWasPressed(48)) // 0 showWatermark = !showWatermark; }; var debugRender = function () { glCopyToContext(mainContext); if (debugTakeScreenshot) { // composite canvas glCopyToContext(mainContext, 1); mainContext.drawImage(overlayCanvas, 0, 0); overlayCanvas.width |= 0; debugSaveCanvas(mainCanvas); debugTakeScreenshot = 0; } if (debugGamepads && gamepadsEnable && navigator.getGamepads) { // poll gamepads var gamepads = navigator.getGamepads(); for (var i = gamepads.length; i--;) { var gamepad = gamepads[i]; if (gamepad) { // gamepad debug display var stickScale = 1; var buttonScale = .2; var centerPos = cameraPos; var sticks = stickData[i]; for (var j = sticks.length; j--;) { var drawPos = centerPos.add(vec2(j * stickScale * 2, i * stickScale * 3)); var stickPos = drawPos.add(sticks[j].scale(stickScale)); debugCircle(drawPos, stickScale, '#fff7', 0, 1); debugLine(drawPos, stickPos, '#f00'); debugPoint(stickPos, '#f00'); } for (var j = gamepad.buttons.length; j--;) { var drawPos = centerPos.add(vec2(j * buttonScale * 2, i * stickScale * 3 - stickScale - buttonScale)); var pressed = gamepad.buttons[j].pressed; debugCircle(drawPos, buttonScale, pressed ? '#f00' : '#fff7', 0, 1); debugText(j, drawPos, .2); } } } } if (debugOverlay) { for (var _i = 0, engineObjects_1 = engineObjects; _i < engineObjects_1.length; _i++) { var o = engineObjects_1[_i]; if (o.canvas) continue; // skip tile layers var size = o.size.copy(); size.x = max(size.x, .2); size.y = max(size.y, .2); var color = new Color(o.collideTiles ? 1 : 0, o.collideSolidObjects ? 1 : 0, o.isSolid ? 1 : 0, o.parent ? .2 : .5); // show object info drawRect(o.pos, size, color); drawRect(o.pos, size.scale(.8), o.parent ? new Color(1, 1, 1, .5) : new Color(0, 0, 0, .8)); o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0, 0, 1, .5)); } // mouse pick var bestDistance = Infinity, bestObject = void 0; for (var _a = 0, engineObjects_2 = engineObjects; _a < engineObjects_2.length; _a++) { var o = engineObjects_2[_a]; var distance = mousePos.distanceSquared(o.pos); if (distance < bestDistance) { bestDistance = distance; bestObject = o; } } if (bestObject) { var saveContext = mainContext; mainContext = overlayContext; var raycastHitPos = tileCollisionRaycast(bestObject.pos, mousePos); raycastHitPos && drawRect(raycastHitPos.int().add(vec2(.5)), vec2(1), new Color(0, 1, 1, .3)); drawRect(mousePos.int().add(vec2(.5)), vec2(1), new Color(0, 0, 1, .5)); drawLine(mousePos, bestObject.pos, .1, !raycastHitPos ? new Color(0, 1, 0, .5) : new Color(1, 0, 0, .5)); var pos = mousePos.copy(), height = vec2(0, .5); var printVec2 = function (v) { return '(' + (v.x > 0 ? ' ' : '') + (v.x).toFixed(2) + ',' + (v.y > 0 ? ' ' : '') + (v.y).toFixed(2) + ')'; }; var args = [.5, new Color, .05, undefined, undefined, 'monospace']; drawText.apply(void 0, __spreadArray(['pos = ' + printVec2(bestObject.pos) + (bestObject.angle > 0 ? ' ' : ' ') + (bestObject.angle * 180 / PI).toFixed(1) + '°', pos = pos.add(height)], args)); drawText.apply(void 0, __spreadArray(['vel = ' + printVec2(bestObject.velocity), pos = pos.add(height)], args)); drawText.apply(void 0, __spreadArray(['size = ' + printVec2(bestObject.size), pos = pos.add(height)], args)); drawText.apply(void 0, __spreadArray(['collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height)], args)); mainContext = saveContext; } glCopyToContext(mainContext); } { // render debug rects overlayContext.lineWidth = 1; var pointSize_1 = debugPointSize * cameraScale; debugPrimitives.forEach(function (p) { // create canvas transform from world space to screen space var pos = worldToScreen(p.pos); overlayContext.save(); overlayContext.lineWidth = 2; overlayContext.translate(pos.x | 0, pos.y | 0); overlayContext.rotate(p.angle); overlayContext.fillStyle = overlayContext.strokeStyle = p.color; if (p.text != undefined) { overlayContext.font = p.size * cameraScale + 'px ' + p.font; overlayContext.textAlign = 'center'; overlayContext.textBaseline = 'middle'; overlayContext.fillText(p.text, 0, 0); } else if (p.size == 0 || p.size.x === 0 && p.size.y === 0) { // point overlayContext.fillRect(-pointSize_1 / 2, -1, pointSize_1, 3); overlayContext.fillRect(-1, -pointSize_1 / 2, 3, pointSize_1); } else if (p.size.x != undefined) { // rect var w = p.size.x * cameraScale | 0, h = p.size.y * cameraScale | 0; p.fill && overlayContext.fillRect(-w / 2 | 0, -h / 2 | 0, w, h); overlayContext.strokeRect(-w / 2 | 0, -h / 2 | 0, w, h); } else { // circle overlayContext.beginPath(); overlayContext.arc(0, 0, p.size * cameraScale, 0, 9); p.fill && overlayContext.fill(); overlayContext.stroke(); } overlayContext.restore(); }); overlayContext.fillStyle = overlayContext.strokeStyle = '#fff'; } { var x = 9, y = -20, h = 30; overlayContext.fillStyle = '#fff'; overlayContext.textAlign = 'left'; overlayContext.textBaseline = 'top'; overlayContext.font = '28px monospace'; overlayContext.shadowColor = '#000'; overlayContext.shadowBlur = 9; if (debugOverlay) { overlayContext.fillText(engineName, x, y += h); overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h); overlayContext.fillText('Time: ' + formatTime(time), x, y += h); overlayContext.fillText('---------', x, y += h); overlayContext.fillStyle = '#f00'; overlayContext.fillText('~: Debug Overlay', x, y += h); overlayContext.fillStyle = debugPhysics ? '#f00' : '#fff'; overlayContext.fillText('1: Debug Physics', x, y += h); overlayContext.fillStyle = debugParticles ? '#f00' : '#fff'; overlayContext.fillText('2: Debug Particles', x, y += h); overlayContext.fillStyle = godMode ? '#f00' : '#fff'; overlayContext.fillText('3: God Mode', x, y += h); overlayContext.fillStyle = '#fff'; overlayContext.fillText('5: Save Screenshot', x, y += h); //overlayContext.fillStyle = debugParticleEditor ? '#f00' : '#fff'; //overlayContext.fillText('6: Particle Editor', x, y += h); overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff'; overlayContext.fillText('7: Debug Gamepads', x, y += h); } else { overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h); overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h); overlayContext.fillText(godMode ? 'God Mode' : '', x, y += h); overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h); } overlayContext.shadowBlur = 0; } debugPrimitives = debugPrimitives.filter(function (r) { return r.time.get() < 0; }); }; /////////////////////////////////////////////////////////////////////////////// // particle system editor var debugParticleEditor = 0, debugParticleSystem, debugParticleSystemDiv, particleSystemCode; var debugToggleParticleEditor = function () { debugParticleEditor = !debugParticleEditor; if (debugParticleEditor) { if (!debugParticleSystem || debugParticleSystem.destroyed) debugParticleSystem = new ParticleEmitter(cameraPos); } else if (debugParticleSystem && !debugParticleSystem.destroyed) debugParticleSystem.destroy(); var colorToHex = function (color) { var componentToHex = function (c) { var hex = (c * 255 | 0).toString(16); return hex.length == 1 ? '0' + hex : hex; }; return '#' + componentToHex(color.r) + componentToHex(color.g) + componentToHex(color.b); }; var hexToColor = function (hex) { return new Color(parseInt(hex.substr(1, 2), 16) / 255, parseInt(hex.substr(3, 2), 16) / 255, parseInt(hex.substr(5, 2), 16) / 255); }; if (!debugParticleSystemDiv) { var div = debugParticleSystemDiv = document.createElement('div'); div.innerHTML = '<big><b>Particle Editor'; div.style = 'position:absolute;top:10;left:10;color:#fff'; document.body.appendChild(div); var _loop_1 = function (setting) { var input = setting[2] = document.createElement('input'); var name_1 = setting[0]; var type = setting[1]; if (type) { if (type == 'color') { input.type = type; var color = debugParticleSystem[name_1]; input.value = colorToHex(color); } else if (type == 'alpha' && name_1 == 'colorStartAlpha') input.value = debugParticleSystem.colorStartA.a; else if (type == 'alpha' && name_1 == 'colorEndAlpha') input.value = debugParticleSystem.colorEndA.a; else if (name_1 == 'tileSizeX') input.value = debugParticleSystem.tileSize.x; else if (name_1 == 'tileSizeY') input.value = debugParticleSystem.tileSize.y; } else input.value = debugParticleSystem[name_1] || '0'; input.oninput = function (e) { var inputFloat = parseFloat(input.value) || 0; if (type) { if (type == 'color') { var color = hexToColor(input.value); debugParticleSystem[name_1].r = color.r; debugParticleSystem[name_1].g = color.g; debugParticleSystem[name_1].b = color.b; } else if (type == 'alpha' && name_1 == 'colorStartAlpha') { debugParticleSystem.colorStartA.a = clamp(inputFloat); debugParticleSystem.colorStartB.a = clamp(inputFloat); } else if (type == 'alpha' && name_1 == 'colorEndAlpha') { debugParticleSystem.colorEndA.a = clamp(inputFloat); debugParticleSystem.colorEndB.a = clamp(inputFloat); } else if (name_1 == 'tileSizeX') { debugParticleSystem.tileSize = vec2(parseInt(input.value), debugParticleSystem.tileSize.y); } else if (name_1 == 'tileSizeY') { debugParticleSystem.tileSize.y = vec2(debugParticleSystem.tileSize.x, parseInt(input.value)); } } else debugParticleSystem[name_1] = inputFloat; updateCode_1(); }; div.appendChild(document.createElement('br')); div.appendChild(input); div.appendChild(document.createTextNode(' ' + name_1)); }; for (var _i = 0, debugParticleSettings_1 = debugParticleSettings; _i < debugParticleSettings_1.length; _i++) { var setting = debugParticleSettings_1[_i]; _loop_1(setting); } div.appendChild(document.createElement('br')); div.appendChild(document.createElement('br')); div.appendChild(particleSystemCode = document.createElement('input')); particleSystemCode.disabled = true; div.appendChild(document.createTextNode(' code')); div.appendChild(document.createElement('br')); var button = document.createElement('button'); div.appendChild(button); button.innerHTML = 'Copy To Clipboard'; button.onclick = function (e) { return navigator.clipboard.writeText(particleSystemCode.value); }; var updateCode_1 = function () { var code = ''; var count = 0; for (var _i = 0, debugParticleSettings_2 = debugParticleSettings; _i < debugParticleSettings_2.length; _i++) { var setting = debugParticleSettings_2[_i]; var name_2 = setting[0]; var type = setting[1]; var value = void 0; if (name_2 == 'tileSizeX' || type == 'alpha') continue; if (count++) code += ', '; if (name_2 == 'tileSizeY') { value = "vec2(" + debugParticleSystem.tileSize.x + "," + debugParticleSystem.tileSize.y + ")"; } else if (type == 'color') { var c = debugParticleSystem[name_2]; value = "new Color(" + c.r + "," + c.g + "," + c.b + "," + c.a + ")"; } else value = debugParticleSystem[name_2]; code += value; } particleSystemCode.value = '...[' + code + ']'; }; updateCode_1(); } debugParticleSystemDiv.style.display = debugParticleEditor ? '' : 'none'; }; var debugParticleSettings = [ ['emitSize'], ['emitTime'], ['emitRate'], ['emitConeAngle'], ['tileIndex'], ['tileSizeX', 'tileSize'], ['tileSizeY', 'tileSize'], ['colorStartA', 'color'], ['colorStartB', 'color'], ['colorStartAlpha', 'alpha'], ['colorEndA', 'color'], ['colorEndB', 'color'], ['colorEndAlpha', 'alpha'], ['particleTime'], ['sizeStart'], ['sizeEnd'], ['speed'], ['angleSpeed'], ['damping'], ['angleDamping'], ['gravityScale'], ['particleConeAngle'], ['fadeRate'], ['randomness'], ['collideTiles'], ['additive'], ['randomColorComponents'], ['renderOrder'], ]; /* LittleJS Utility Classes and Functions - Vector2 - fast, simple, easy vector class - Color - holds a rgba color with math functions - Timer - tracks time automatically - Small math lib */ 'use strict'; /////////////////////////////////////////////////////////////////////////////// // helper functions var PI = Math.PI; var isChrome = window['chrome']; var abs = function (a) { return a < 0 ? -a : a; }; var sign = function (a) { return a < 0 ? -1 : 1; }; var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; var mod = function (a, b) { return ((a % b) + b) % b; }; var clamp = function (v, max, min) { if (max === void 0) { max = 1; } if (min === void 0) { min = 0; } return (ASSERT(max > min), v < min ? min : v > max ? max : v); }; var percent = function (v, max, min) { if (max === void 0) { max = 1; } if (min === void 0) { min = 0; } return max - min ? clamp((v - min) / (max - min)) : 0; }; var lerp = function (p, max, min) { if (max === void 0) { max = 1; } if (min === void 0) { min = 0; } return min + clamp(p) * (max - min); }; var formatTime = function (t) { return (t / 60 | 0) + ':' + (t % 60 < 10 ? '0' : '') + (t % 60 | 0); }; var isOverlapping = function (pA, sA, pB, sB) { return abs(pA.x - pB.x) * 2 < sA.x + sB.x & abs(pA.y - pB.y) * 2 < sA.y + sB.y; }; var nearestPowerOfTwo = function (v) { return Math.pow(2, Math.ceil(Math.log2(v))); }; var wave = function (f, a, t) { if (f === void 0) { f = 1; } if (a === void 0) { a = 1; } if (t === void 0) { t = time; } return a / 2 * (1 - Math.cos(t * f * 2 * PI)); }; var smoothStep = function (p) { return p * p * (3 - 2 * p); }; // random functions var rand = function (a, b) { if (a === void 0) { a = 1; } if (b === void 0) { b = 0; } return b + (a - b) * Math.random(); }; var randInt = function (a, b) { if (a === void 0) { a = 1; } if (b === void 0) { b = 0; } return rand(a, b) | 0; }; var randSign = function () { return (rand(2) | 0) * 2 - 1; }; var randInCircle = function (radius, minRadius) { if (radius === void 0) { radius = 1; } if (minRadius === void 0) { minRadius = 0; } return radius > 0 ? randVector(radius * Math.pow(rand(minRadius / radius, 1), .5)) : new Vector2; }; var randVector = function (length) { if (length === void 0) { length = 1; } return new Vector2().setAngle(rand(2 * PI), length); }; var randColor = function (cA, cB, linear) { if (cA === void 0) { cA = new Color; } if (cB === void 0) { cB = new Color(0, 0, 0, 1); } return linear ? cA.lerp(cB, rand()) : new Color(rand(cA.r, cB.r), rand(cA.g, cB.g), rand(cA.b, cB.b), rand(cA.a, cB.a)); }; // seeded random numbers using xorshift var randSeed = 1; var randSeeded = function (a, b) { if (a === void 0) { a = 1; } if (b === void 0) { b = 0; } randSeed ^= randSeed << 13; randSeed ^= randSeed >>> 17; randSeed ^= randSeed << 5; return b + (a - b) * abs(randSeed % 1e9) / 1e9; }; // create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar var vec2 = function (x, y) { if (x === void 0) { x = 0; } return x.x == undefined ? new Vector2(x, y == undefined ? x : y) : new Vector2(x.x, x.y); }; /////////////////////////////////////////////////////////////////////////////// var Vector2 = /** @class */ (function () { function Vector2(x, y) { if (x === void 0) { x = 0; } if (y === void 0) { y = 0; } this.x = x; this.y = y; } // basic math operators, a vector or scaler can be passed in Vector2.prototype.copy = function () { return new Vector2(this.x, this.y); }; Vector2.prototype.scale = function (s) { ASSERT(s.x == undefined); return new Vector2(this.x * s, this.y * s); }; Vector2.prototype.add = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x + v.x, this.y + v.y); }; Vector2.prototype.subtract = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x - v.x, this.y - v.y); }; Vector2.prototype.multiply = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x * v.x, this.y * v.y); }; Vector2.prototype.divide = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x / v.x, this.y / v.y); }; // vector math operators Vector2.prototype.length = function () { return Math.pow(this.lengthSquared(), .5); }; Vector2.prototype.lengthSquared = function () { return Math.pow(this.x, 2) + Math.pow(this.y, 2); }; Vector2.prototype.distance = function (p) { return Math.pow(this.distanceSquared(p), .5); }; Vector2.prototype.distanceSquared = function (p) { return Math.pow((this.x - p.x), 2) + Math.pow((this.y - p.y), 2); }; Vector2.prototype.normalize = function (length) { if (length === void 0) { length = 1; } var l = this.length(); return l ? this.scale(length / l) : new Vector2(length); }; Vector2.prototype.clampLength = function (length) { if (length === void 0) { length = 1; } var l = this.length(); return l > length ? this.scale(length / l) : this; }; Vector2.prototype.dot = function (v) { ASSERT(v.x != undefined); return this.x * v.x + this.y * v.y; }; Vector2.prototype.cross = function (v) { ASSERT(v.x != undefined); return this.x * v.y - this.y * v.x; }; Vector2.prototype.angle = function () { return Math.atan2(this.x, this.y); }; Vector2.prototype.setAngle = function (a, length) { if (length === void 0) { length = 1; } this.x = length * Math.sin(a); this.y = length * Math.cos(a); return this; }; Vector2.prototype.rotate = function (a) { var c = Math.cos(a), s = Math.sin(a); return new Vector2(this.x * c - this.y * s, this.x * s + this.y * c); }; Vector2.prototype.direction = function () { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }; Vector2.prototype.flip = function () { return new Vector2(this.y, this.x); }; Vector2.prototype.invert = function () { return new Vector2(this.y, -this.x); }; Vector2.prototype.round = function () { return new Vector2(Math.round(this.x), Math.round(this.y)); }; Vector2.prototype.floor = function () { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }; Vector2.prototype.int = function () { return new Vector2(this.x | 0, this.y | 0); }; Vector2.prototype.lerp = function (v, p) { ASSERT(v.x != undefined); return this.add(v.subtract(this).scale(clamp(p))); }; Vector2.prototype.area = function () { return this.x * this.y; }; Vector2.prototype.arrayCheck = function (arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }; return Vector2; }()); /////////////////////////////////////////////////////////////////////////////// var Color = /** @class */ (function () { function Color(r, g, b, a) { if (r === void 0) { r = 1; } if (g === void 0) { g = 1; } if (b === void 0) { b = 1; } if (a === void 0) { a = 1; } this.r = r; this.g = g; this.b = b; this.a = a; } Color.prototype.copy = function (c) { return new Color(this.r, this.g, this.b, this.a); }; Color.prototype.add = function (c) { return new Color(this.r + c.r, this.g + c.g, this.b + c.b, this.a + c.a); }; Color.prototype.subtract = function (c) { return new Color(this.r - c.r, this.g - c.g, this.b - c.b, this.a - c.a); }; Color.prototype.multiply = function (c) { return new Color(this.r * c.r, this.g * c.g, this.b * c.b, this.a * c.a); }; Color.prototype.scale = function (s, a) { if (a === void 0) { a = s; } return new Color(this.r * s, this.g * s, this.b * s, this.a * a); }; Color.prototype.clamp = function () { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }; Color.prototype.lerp = function (c, p) { return this.add(c.subtract(this).scale(clamp(p))); }; Color.prototype.mutate = function (amount, alphaAmount) { if (amount === void 0) { amount = .05; } if (alphaAmount === void 0) { alphaAmount = 0; } return new Color(this.r + rand(amount, -amount), this.g + rand(amount, -amount), this.b + rand(amount, -amount), this.a + rand(alphaAmount, -alphaAmount)).clamp(); }; Color.prototype.rgba = function () { ASSERT(this.r >= 0 && this.r <= 1 && this.g >= 0 && this.g <= 1 && this.b >= 0 && this.b <= 1 && this.a >= 0 && this.a <= 1); return "rgb(" + (this.r * 255 | 0) + "," + (this.g * 255 | 0) + "," + (this.b * 255 | 0) + "," + this.a + ")"; }; Color.prototype.rgbaInt = function () { ASSERT(this.r >= 0 && this.r <= 1 && this.g >= 0 && this.g <= 1 && this.b >= 0 && this.b <= 1 && this.a >= 0 && this.a <= 1); return (this.r * 255 | 0) + (this.g * 255 << 8) + (this.b * 255 << 16) + (this.a * 255 << 24); }; Color.prototype.setHSLA = function (h, s, l, a) { if (h === void 0) { h = 0; } if (s === void 0) { s = 0; } if (l === void 0) { l = 1; } if (a === void 0) { a = 1; } var q = l < .5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q, f = function (p, q, t) { return (t = ((t % 1) + 1) % 1) < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p; }; this.r = f(p, q, h + 1 / 3); this.g = f(p, q, h); this.b = f(p, q, h - 1 / 3); this.a = a; return this; }; return Color; }()); /////////////////////////////////////////////////////////////////////////////// var Timer = /** @class */ (function () { function Timer(timeLeft) { this.time = timeLeft == undefined ? undefined : time + timeLeft; this.setTime = timeLeft; } Timer.prototype.set = function (timeLeft) { if (timeLeft === void 0) { timeLeft = 0; } this.time = time + timeLeft; this.setTime = timeLeft; }; Timer.prototype.unset = function () { this.time = undefined; }; Timer.prototype.isSet = function () { return this.time != undefined; }; Timer.prototype.active = function () { return time <= this.time; }; Timer.prototype.elapsed = function () { return time > this.time; }; Timer.prototype.get = function () { return this.isSet() ? time - this.time : 0; }; Timer.prototype.getPercent = function () { return this.isSet() ? percent(this.time - time, 0, this.setTime) : 0; }; return Timer; }()); /* LittleJS Engine Configuration */ /////////////////////////////////////////////////////////////////////////////// // display settings var maxWidth = 1920, maxHeight = 1200; // up to 1080p and 16:10 var defaultFont = 'arial'; // font used for text rendering var fixedWidth = 0, fixedHeight = 0; // use native resolution var fixedFitToWindow = 1; // stretch canvas to fit window //const fixedWidth = 1280, fixedHeight = 720; // 720p //const fixedWidth = 1920, fixedHeight = 1080; // 1080p //const fixedWidth = 128, fixedHeight = 128; // PICO-8 //const fixedWidth = 240, fixedHeight = 136; // TIC-80 /////////////////////////////////////////////////////////////////////////////// // tile sheet settings var defaultTileSize = vec2(16); // default size of tiles in pixels var tileBleedShrinkFix = .3; // prevent tile bleeding from neighbors var pixelated = 1; // use crisp pixels for pixel art /////////////////////////////////////////////////////////////////////////////// // webgl config var glEnable = 1; // can run without gl (texured coloring will be disabled) var glOverlay = 0; // fix slow rendering in some browsers by not compositing the WebGL canvas /////////////////////////////////////////////////////////////////////////////// // object config var defaultObjectSize = vec2(.999); // size of objecs, tiny bit less then 1 to fit in holes var defaultObjectMass = 1; // how heavy are objects for collison calcuations var defaultObjectDamping = .99; // how much to slow velocity by each frame 0-1 var defaultObjectAngleDamping = .99; // how much to slow angular velocity each frame 0-1 var defaultObjectElasticity = 0; // how much to bounce 0-1 var defaultObjectFriction = .8; // how much to slow when touching 0-1 var maxObjectSpeed = 1; // camp max speed to avoid fast objects missing collisions /////////////////////////////////////////////////////////////////////////////// // input config var gamepadsEnable = 1; // should gamepads be allowed var touchInputEnable = 1; // touch input is routed to mouse var copyGamepadDirectionToStick = 1; // allow players to use dpad as analog stick var copyWASDToDpad = 1; // allow players to use WASD as direction keys /////////////////////////////////////////////////////////////////////////////// // audio config var soundEnable = 1; // all audio can be disabled var audioVolume = .5; // volume for sound, music and speech var defaultSoundRange = 30; // range where sound no longer plays var defaultSoundTaper = .7; // what range percent to start tapering off sound 0-1 /////////////////////////////////////////////////////////////////////////////// // medals config var medalDisplayTime = 5; // how long to show medals var medalDisplaySlideTime = .5; // how quick to slide on/off medals var medalDisplayWidth = 640; // width of medal display var medalDisplayHeight = 99; // height of medal display var medalDisplayIconSize = 80; // size of icon in medal display /* LittleJS - The Tiny JavaScript Game Engine That Can MIT License - Copyright 2019 Frank Force Engine Features - Engine and debug system are separate from game code - Object oriented with base class engine object - Engine handles core update loop - Base class object handles update, physics, collision, rendering, etc - Engine helper classes and functions like Vector2, Color, and Timer - Super fast rendering system for tile sheets - Sound effects audio with zzfx and music with zzfxm - Input processing system with gamepad and touchscreen support - Tile layer rendering and collision system - Particle effect system - Automatically calls gameInit(), gameUpdate(), gameUpdatePost(), gameRender(), gameRenderPost() - Debug tools and debug rendering system - Call engineInit() to start it up! */ 'use strict'; var engineName = 'LittleJS'; var engineVersion = '1.0.16'; var FPS = 60, timeDelta = 1 / FPS; // engine uses a fixed time step var tileImage = new Image(); // everything uses the same tile sheet // core engine variables var mainCanvas, mainContext, overlayCanvas, overlayContext, mainCanvasSize = vec2(), engineObjects = [], engineCollideObjects = [], cameraPos = vec2(), cameraScale = max(defaultTileSize.x, defaultTileSize.y), frame = 0, time = 0, realTime = 0, paused = 0, frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0, gravity = 0, tileImageSize, tileImageSizeInverse, shrinkTilesX, shrinkTilesY, drawCount; // call this function to start the engine function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource) { // init engine when tiles load tileImage.onload = function () { // save tile image info tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height)); debug && (tileImage.onload = function () { return ASSERT(1); }); // tile sheet can not reloaded shrinkTilesX = tileBleedShrinkFix / tileImageSize.x; shrinkTilesY = tileBleedShrinkFix / tileImageSize.y; // setup html document.body.appendChild(mainCanvas = document.createElement('canvas')); document.body.style = 'margin:0;overflow:hidden;background:#000'; mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)' + (pixelated ? ';image-rendering:crisp-edges;image-rendering:pixelated' : ''); // pixelated rendering mainContext = mainCanvas.getContext('2d'); // init stuff and start engine debugInit(); glInit(); // create overlay canvas for hud to appear above gl canvas document.body.appendChild(overlayCanvas = document.createElement('canvas')); overlayCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)'; overlayContext = overlayCanvas.getContext('2d'); gameInit(); engineUpdate(); }; // main update loop var engineUpdate = function (frameTimeMS) { if (frameTimeMS === void 0) { frameTimeMS = 0; } requestAnimationFrame(engineUpdate); // update time keeping var frameTimeDeltaMS = frameTimeMS - frameTimeLastMS; frameTimeLastMS = frameTimeMS; if (debug || showWatermark) debugFPS = lerp(.05, 1e3 / (frameTimeDeltaMS || 1), debugFPS); if (debug) frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time realTime += frameTimeDeltaMS / 1e3; frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate if (paused) { // do post update even when paused inputUpdate(); debugUpdate(); gameUpdatePost(); inputUpdatePost(); } else { // apply time delta smoothing, improves smoothness of framerate in some browsers var deltaSmooth = 0; if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9) { // force an update each frame if time is close enough (not just a fast refresh rate) deltaSmooth = frameTimeBufferMS; frameTimeBufferMS = 0; } // update multiple frames if necessary in case of slow framerate for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / FPS) { // update game and objects inputUpdate(); gameUpdate(); engineUpdateObjects(); // do post update debugUpdate(); gameUpdatePost(); inputUpdatePost(); } // add the time smoothing back in frameTimeBufferMS += deltaSmooth; } if (fixedWidth) { // clear set fixed size mainCanvas.width = fixedWidth; mainCanvas.height = fixedHeight; if (fixedFitToWindow) { // fit to window by adding space on top or bottom if necessary var aspect = innerWidth / innerHeight; var fixedAspect = fixedWidth / fixedHeight; mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : ''; mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%'; if (glCanvas) { glCanvas.style.width = mainCanvas.style.width; glCanvas.style.height = mainCanvas.style.height; } } } else { // clear and set size to same as window mainCanvas.width = min(innerWidth, maxWidth); mainCanvas.height = min(innerHeight, maxHeight); } // save canvas size and clear overlay canvas mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height); mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art // render sort then render while removing destroyed objects glPreRender(mainCanvas.width, mainCanvas.height); gameRender(); engineObjects.sort(function (a, b) { return a.renderOrder - b.renderOrder; }); for (var _i = 0, engineObjects_3 = engineObjects; _i < engineObjects_3.length; _i++) { var o = engineObjects_3[_i]; o.destroyed || o.render(); } gameRenderPost(); medalsRender(); debugRender(); glCopyToContext(mainContext); if (showWatermark) { // update fps overlayContext.textAlign = 'right'; overlayContext.textBaseline = 'top'; overlayContext.font = '1em monospace'; overlayContext.fillStyle = '#000'; var text = engineName + ' ' + 'v' + engineVersion + ' / ' + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1); overlayContext.fillText(text, mainCanvas.width - 3, 3); overlayContext.fillStyle = '#fff'; overlayContext.fillText(text, mainCanvas.width - 2, 2); drawCount = 0; } }; // set tile image source to load the image and start the engine tileImageSource ? tileImage.src = tileImageSource : tileImage.onload(); } function engineUpdateObjects() { // recursive object update var updateObject = function (o) { if (!o.destroyed) { o.update(); for (var _i = 0, _a = o.children; _i < _a.length; _i++) { var child = _a[_i]; updateObject(child); } } }; for (var _i = 0, engineObjects_4 = engineObjects; _i < engineObjects_4.length; _i++) { var o = engineObjects_4[_i]; o.parent || updateObject(o); } // remove destroyed objects engineObjects = engineObjects.filter(function (o) { return !o.destroyed; }); engineCollideObjects = engineCollideObjects.filter(function (o) { return !o.destroyed; }); // increment frame and update time time = ++frame / FPS; } /* LittleJS Object Base Class - Base object class used by the engine - Automatically adds self to object list - Will be updated and rendered each frame - Renders as a sprite from a tilesheet by default - Can have color and addtive color applied - 2d Physics and collision system - Sorted by renderOrder - Objects can have children attached - Parents are updated before children, and set child transform - Call destroy() to get rid of objects */ 'use strict'; var EngineObject = /** @class */ (function () { function EngineObject(pos, size, tileIndex, tileSize, angle, color) { if (size === void 0) { size = defaultObjectSize; } if (tileIndex === void 0) { tileIndex = -1; } if (tileSize === void 0) { tileSize = defaultTileSize; } if (angle === void 0) { angle = 0; } // set passed in params ASSERT(pos && pos.x != undefined && size.x != undefined); // ensure pos and size are vec2s this.pos = pos.copy(); this.size = size; this.tileIndex = tileIndex; this.tileSize = tileSize; this.angle = angle; this.color = color; // set physics defaults this.mass = defaultObjectMass; this.damping = defaultObjectDamping; this.angleDamping = defaultObjectAngleDamping; this.elasticity = defaultObjectElasticity; this.friction = defaultObjectFriction; // init other object stuff this.spawnTime = time; this.velocity = vec2(this.collideSolidObjects = this.renderOrder = this.angleVelocity = 0); this.collideTiles = this.gravityScale = 1; this.children = []; // add to list of objects engineObjects.push(this); } EngineObject.prototype.update = function () { var parent = this.parent; if (parent) { // copy parent pos/angle this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(), 1)).rotate(-parent.angle).add(parent.pos); this.angle = parent.getMirrorSign() * this.localAngle + parent.angle; return; } // limit max speed to prevent missing collisions this.velocity.x = clamp(this.velocity.x, maxObjectSpeed, -maxObjectSpeed); this.velocity.y = clamp(this.velocity.y, maxObjectSpeed, -maxObjectSpeed); // apply physics var oldPos = this.pos.copy(); this.pos.x += this.velocity.x = this.damping * this.velocity.x; this.pos.y += this.velocity.y = this.damping * this.velocity.y + gravity * this.gravityScale; this.angle += this.angleVelocity *= this.angleDamping; // physics sanity checks ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1); ASSERT(this.damping >= 0 && this.damping <= 1); if (!this.mass) // do not update collision for fixed objects return; var wasMovingDown = this.velocity.y < 0; if (this.groundObject) { // apply friction in local space of ground object var groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0; this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction; this.groundObject = 0; //debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0'); } if (this.collideSolidObjects) { // check collisions against solid objects var epsilon = 1e-3; // necessary to push slightly outside of the collision for (var _i = 0, engineCollideObjects_1 = engineCollideObjects; _i < engineCollideObjects_1.length; _i++) { var o = engineCollideObjects_1[_i]; // non solid objects don't collide with eachother if (!this.isSolid & !o.isSolid || o.destroyed || o.parent) continue; // check collision if (!isOverlapping(this.pos, this.size, o.pos, o.size) || o == this) continue; // pass collision to objects if (!this.collideWithObject(o) | !o.collideWithObject(this)) continue; if (isOverlapping(oldPos, this.size, o.pos, o.size)) { // if already was touching, try to push away var deltaPos = oldPos.subtract(o.pos); var length_1 = deltaPos.length(); var pushAwayAccel = .001; // push away if alread overlapping var velocity = length_1 < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel / length_1); this.velocity = this.velocity.add(velocity); if (o.mass) // push away if not fixed o.velocity = o.velocity.subtract(velocity); debugPhysics && debugAABB(this.pos, o.pos, this.size, o.size, '#f00'); continue; } // check for collision var sx = this.size.x + o.size.x; var sy = this.size.y + o.size.y; var smallStepUp = (oldPos.y - o.pos.y) * 2 > sy + gravity; // prefer to push up if small delta var isBlockedX = abs(oldPos.y - o.pos.y) * 2 < sy; var isBlockedY = abs(oldPos.x - o.pos.x) * 2 < sx; if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision { // push outside object collision this.pos.y = o.pos.y + (sy / 2 + epsilon) * sign(oldPos.y - o.pos.y); if (o.groundObject && wasMovingDown || !o.mass) { // set ground object if landed on something if (wasMovingDown) this.groundObject = o; // bounce if other object is fixed or grounded this.velocity.y *= -this.elasticity; } else if (o.mass) { // inelastic collision var inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass); // elastic collision var elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass) + o.velocity.y * 2 * o.mass / (this.mass + o.mass); var elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass) + this.velocity.y * 2 * this.mass / (this.mass + o.mass); // lerp betwen elastic or inelastic based on elasticity var elasticity = max(this.elasticity, o.elasticity); this.velocity.y = lerp(elasticity, elastic0, inelastic); o.velocity.y = lerp(elasticity, elastic1, inelastic); } debugPhysics && smallStepUp && (abs(oldPos.x - o.pos.x) * 2 > sx) && console.log('stepUp', oldPos.y - o.pos.y); } if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision { // push outside collision this.pos.x = o.pos.x + (sx / 2 + epsilon) * sign(oldPos.x - o.pos.x); if (o.mass) { // inelastic collision var inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass); // elastic collision var elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass) + o.velocity.x * 2 * o.mass / (this.mass + o.mass); var elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass) + this.velocity.x * 2 * this.mass / (this.mass + o.mass); // lerp betwen elasti