UNPKG

@moderrkowo/jsgl

Version:

Client-side JavaScript library for creating web 2D games. Focusing at objective game.

764 lines (763 loc) 29.3 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Game = void 0; var Vector2_1 = require("./structs/Vector2"); var Signals_1 = require("./events/Signals"); var Renderer_1 = require("./drawing/Renderer"); var GameObject_1 = require("./gameobjects/GameObject"); var ClickableGameObject_1 = require("./gameobjects/ClickableGameObject"); var DrawableGameObject_1 = require("./gameobjects/DrawableGameObject"); var MathUtils_1 = require("./utils/math/MathUtils"); var GameSettings_1 = require("./structs/GameSettings"); var Input_1 = require("./Input"); /** * @group Important Classes */ var Game = /** @class */ (function () { // Constructor /** * Constructs new Game instance with given settings. * @param gameSettings The settings */ function Game(gameSettings) { var _this = this; this.renderer = new Renderer_1.Renderer(this); // Game Loop Management this._isPlaying = false; this._unscaledTime = 0; this._deltaTime = 0; this.timeScale = 1; this.isNeedToUpdate = true; this._gameLoop = function (time) { _this._gameLoopUpdate(time); if (!document.hasFocus() && !_this.gameSettings.refreshWhenUnfocused) { if (_this._isPlaying) window.requestAnimationFrame(_this._gameLoop); return; } _this._gameLoopDraw(); // Continue loop if (_this._isPlaying) window.requestAnimationFrame(_this._gameLoop); }; // Signals this._signals = new Signals_1.Signals(); this.emit = function (channel, event) { return _this._signals.emit(channel, event); }; /** * Appends listener to event channel. * @method * @param channel - The listener event channel. * @param callback - Executed function after receiving a signal on given channel. * @example * game.on('channel', () => { console.log('received!') }); */ this.on = function (channel, callback) { return _this._signals.on(channel, callback); }; // Resources this.resources = new Map(); this._isLoadedAllResources = false; // GameObjects /** * Sorted game object array */ this.gameObjects = []; this._tempMouseWheelDeltaY = 0; this.gameSettings = __assign(__assign({}, GameSettings_1.defaultGameSettings), gameSettings); if (this.gameSettings.canvas !== undefined) { this.canvas = this.gameSettings.canvas; } else { throw new Error('Cannot asign canvas.'); } if (this.gameSettings.grid !== undefined) { this.grid = this.gameSettings.grid; } else { throw new Error('Cannot asign grid.'); } this.input = new Input_1.Input(); this._registerCanvasEvents(); if (this.gameSettings.autoResize) { window.addEventListener('resize', function () { _this.renderer.resizeCanvas(); _this.Update(); }); } this.renderer.ctx.imageSmoothingEnabled = true; if (this.gameSettings.canvasImageQuality !== undefined) this.renderer.ctx.imageSmoothingQuality = this.gameSettings.canvasImageQuality; this.canvasViewOffset = new Vector2_1.Vector2(); } // Canvas /** * Scaling canvas size on page to percentage of parent element. * @param percentage The decimal midpoint * @returns The final canvas size */ Game.prototype.RescaleCanvasToParentElement = function (percentage) { this.renderer.canvasParentSize = percentage; this.renderer.resizeCanvas(); return this.renderer.canvasSize; }; Game.prototype._registerCanvasEvents = function () { var _this = this; this.canvas.addEventListener('mousemove', function (event) { _this.mouseMoveHandler(_this, event); }); document.addEventListener('mouseup', function () { _this.mouseUpHandler(_this); }); document.addEventListener('mousedown', function () { _this.mouseDownHandler(_this); }); this.canvas.addEventListener('click', function () { _this.mouseClickHandler(_this); }); // Appends mouse wheel listener to canvas this.canvas.addEventListener('wheel', function (event) { _this.mouseWheelHandler(_this, event); }, { passive: false }); document.addEventListener('keydown', function (event) { if (event.defaultPrevented) { return; } event.preventDefault(); var code = event.code.toLowerCase().replace('key', ''); _this.input.keysDown.add(code); }); document.addEventListener('keyup', function (event) { if (event.defaultPrevented) { return; } event.preventDefault(); var code = event.code.toLowerCase().replace('key', ''); _this.input.keysUp.add(code); }); }; Game.prototype._gameLoopUpdate = function (time) { // Calculation deltaTime this._deltaTime = time - this._unscaledTime; this._unscaledTime = time; // Update var tickEvent = { unscaledDeltaTime: this._deltaTime / 1000, unscaledTime: this._unscaledTime / 1000, deltaTime: (this._deltaTime / 1000) * this.timeScale, timeScale: this.timeScale, game: this, }; // Events update var hoveredGameObject = this.mouseHoveredGameObject; var mouseEvent = this.constructMouseEvent(); var lastHoveredGameObject = this.lastMouseHoveredGameObject; if (lastHoveredGameObject !== hoveredGameObject) { if (lastHoveredGameObject !== undefined) lastHoveredGameObject.OnMouseHoverEnd(mouseEvent); hoveredGameObject === null || hoveredGameObject === void 0 ? void 0 : hoveredGameObject.OnMouseHoverStart(mouseEvent); this.lastMouseHoveredGameObject = hoveredGameObject; } this.input.mouseScrollDelta = new Vector2_1.Vector2(0, this._tempMouseWheelDeltaY); if (this._tempMouseWheelDeltaY !== 0) { this.emit('mouseScroll', mouseEvent); } this._tempMouseWheelDeltaY = 0; var keyEvent = { game: this, input: this.input, }; if (this.input.keysDown.size > 0) this.emit('keyDown', keyEvent); if (this.input.keysUp.size > 0) this.emit('keyUp', keyEvent); // Update for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var gameObject = _a[_i]; if (!gameObject.enabled) continue; try { gameObject.Update(tickEvent); } catch (e) { console.warn("Problem with executing Update @ ".concat(gameObject.constructor.name, " "), gameObject); if (e instanceof Error) console.error(e.message); } } // Fixed Update for (var _b = 0, _c = this.gameObjects; _b < _c.length; _b++) { var gameObject = _c[_b]; if (!gameObject.enabled) continue; try { gameObject.FixedUpdate(tickEvent); } catch (e) { console.warn("Problem with executing FixedUpdate @ ".concat(gameObject.constructor.name, " "), gameObject); if (e instanceof Error) console.error(e.message); } } // this.input.keysDown.clear(); this.input.keysUp.clear(); }; Game.prototype._gameLoopDraw = function () { if (this.isNeedToUpdate || this.gameSettings.drawAlways) { this.renderer.clearFrame(); var drawEvent = { renderer: this.renderer, game: this, }; this.emit('draw', drawEvent); for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var gameObject = _a[_i]; if (!gameObject.enabled) continue; if (!(gameObject instanceof DrawableGameObject_1.DrawableGameObject)) continue; try { gameObject.OnDraw(drawEvent); if (gameObject instanceof ClickableGameObject_1.ClickableGameObject) { var clickable = gameObject; this.renderer.drawHitbox(clickable); } } catch (e) { console.warn("Problem with executing draw @ ".concat(gameObject.constructor.name, " "), gameObject); if (e instanceof Error) console.error(e.message); } } this.isNeedToUpdate = false; } }; /** * Tells the game it needs to be redrawn. * Call it when some visible objects has been changed. */ Game.prototype.Update = function () { this.isNeedToUpdate = true; }; /** * Starts new game loop */ Game.prototype.StartGameLoop = function () { var _this = this; if (this._isPlaying) { console.warn('Cannot start new game loop when the game loop exists.'); return; } window.requestAnimationFrame(function () { window.requestAnimationFrame(_this._gameLoop.bind(_this)); _this._isPlaying = true; }); }; /** * WARNING! Stops the game loop */ Game.prototype.StopGameLoop = function () { console.warn('Stopped the game loop! Restarting the game loop will cause a time skip.'); this._isPlaying = false; }; /** * Starts the game loop and emit `start` ({@link GameEvent}) event. */ Game.prototype.Start = function () { this.StartGameLoop(); this.emit('start', { game: this }); }; /** * Starts loading game resources and returns promise. * @method * @returns The Promise * @example * game.LoadGameAndStart().then((e) => { * console.log('Game sucessfully loaded!'); * }).catch((error) => { * console.error('Error'); * }).finally(() => { * console.log("Finally"); * }); */ Game.prototype.LoadGameAndStart = function () { var _this = this; return new Promise(function (resolve, reject) { if (_this._isPlaying) reject(new Error('Cannot load and start game because the game loop currently exists!')); var whenLoaded = function () { _this.Start(); resolve({ game: _this }); }; _this.on('loadAllResources', whenLoaded); try { setTimeout(function () { _this.LoadAllResources(); }, 1); } catch (error) { reject(error); } }); }; /** * Loads resource with resource manager. * @method * @param type - The resource type * @param uid - The resource unique key * @param path - The resource path * @example * game.LoadResource('image', 'player', './resources/img/player.png'); */ Game.prototype.LoadResource = function (type, uid, path) { if (type === 'image') { this.resources.set(uid, { uid: uid, type: type, path: path, object: undefined, loaded: false, }); } else { throw new Error('Unknown resource type.'); } this.LoadAllResources(); }; /** * Creates {@link HTMLImageElement} from path. * @method * @param path - The image path * @returns The image element * @example * game.CreateImage('./resources/img/player.png'); */ Game.prototype.CreateImage = function (path) { var img = new Image(); img.src = path; return img; }; /** * Gets the resource by unique resource key. * @method * @param uid - The unique resource key. * @returns The resource * @example * const resource = game.GetResource('player'); */ Game.prototype.GetResource = function (uid) { var res = this.resources.get(uid); if (res === undefined) throw new Error('Resource not loaded!'); return res; }; /** * Gets the image resource by unique resource key. * @method * @param uid - The unique resource key. * @returns The image * @example * const image = game.GetImage('player'); */ Game.prototype.GetImage = function (uid) { var res = this.GetResource(uid); if (res.type !== 'image') return undefined; if (!res.loaded) return undefined; return res.object; }; /** * Starts loading all resources which is not loaded. Emits {@link GameEvent} at `loadAllResources` channel. */ Game.prototype.LoadAllResources = function () { var _this = this; if (this.resources.size === 0) { this._signals.emit('loadAllResources', { game: this }); this._isLoadedAllResources = true; return; } var resourcesCount = 0; var resourceLoaded = function () { resourcesCount -= 1; if (resourcesCount === 0) { if (!_this._isLoadedAllResources) { _this._signals.emit('loadAllResources', { game: _this }); _this._isLoadedAllResources = true; } } }; this.resources.forEach(function (resource) { if (resource.loaded) return; if (resource.type === 'image') { // Loading image var image_1 = new Image(); image_1.src = resource.path; image_1.addEventListener('error', function () { resource.loaded = false; console.error("Cannot load ".concat(resource.uid, " resource.")); resourceLoaded(); }); image_1.addEventListener('load', function () { resource.object = image_1; resource.loaded = true; resourceLoaded(); }); } else { // Unknown type console.warn("Type \"".concat(resource.type, "\" is unknown resources type.")); return; } resourcesCount += 1; }); }; /** * Sorts all game objects by sorting order property. */ Game.prototype.SortGameObjects = function () { this.gameObjects.sort(function (a, b) { return a.sortingOrder - b.sortingOrder; }); }; /** * Adds unique game object to game. * @method * @param gameObject - The unique game object * @returns The added game object * @example * game.AddGameObject(new JSGL.GameObject()); */ Game.prototype.AddGameObject = function (gameObject) { if (!(gameObject instanceof GameObject_1.GameObject)) throw new Error('Cannot add not GameObject!'); for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var otherGameObjects = _a[_i]; if (gameObject.id === otherGameObjects.id) throw new Error('Cannot add this same GameObject!'); } if (gameObject.name === undefined) gameObject.name = gameObject.constructor.name; this.gameObjects.push(gameObject); this.SortGameObjects(); var gameObjectSpawnEvent = { game: this, gameObjectId: gameObject.id, }; gameObject.Start(gameObjectSpawnEvent); this.emit('spawnedGameObject', gameObjectSpawnEvent); return gameObject; }; /** * Destroys existed game object by reference * @method * @param gameObject - The game object reference * @example * const gameObject = ... * game.DestroyGameObjectByRef(gameObject); */ Game.prototype.DestroyGameObjectByRef = function (gameObject) { if (!(gameObject instanceof GameObject_1.GameObject)) throw new Error('Param gameObject must be an GameObject object!'); var index = this.gameObjects.indexOf(gameObject); if (index === -1) return; var onDestroyEvent = { game: this, }; gameObject.Destroy(onDestroyEvent); this.gameObjects.splice(index, 1); this.SortGameObjects(); }; /** * Destroys existed game object by unique id * @method * @param id - The unique id * @example * game.DestroyGameObjectById('51870300-4187221613-3012590175-3461657014'); */ Game.prototype.DestroyGameObjectById = function (id) { if (typeof id !== 'string') throw new Error('Param id must be string!'); var gameObject = this.GetGameObjectById(id); if (gameObject === undefined) return; var index = this.gameObjects.findIndex(function (element) { return element.id === gameObject.id; }); if (index === -1) return; var onDestroyEvent = { game: this, }; this.gameObjects[index].Destroy(onDestroyEvent); this.gameObjects.splice(index, 1); this.SortGameObjects(); }; /** * Destroys existed game object by index in game objects array * @method * @param index - The index * @example * game.DestroyGameObjectByIndex(0); */ Game.prototype.DestroyGameObjectByIndex = function (index) { if (typeof index !== 'number') throw new Error('Param index must be number!'); if (index < 0) throw new Error('Index cannot be lower than 0!'); if (index >= this.gameObjects.length) throw new Error('Index cannot be bigger than maximum index!'); var onDestroyEvent = { game: this, }; this.gameObjects[index].Destroy(onDestroyEvent); this.gameObjects.splice(index, 1); this.SortGameObjects(); }; /** * Gets all existed game objects with type equal to param. * @param type - The type * @returns Array of selected game objects * @example * game.GetGameObjectsByType(JSGL.Shape); */ Game.prototype.GetGameObjectsByType = function (type) { if (typeof type != 'function' || !(type instanceof Object)) throw new Error('Type must be an object!'); var result = []; for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var gameObject = _a[_i]; if (gameObject instanceof type) result.push(gameObject); } return result; }; /** * Gets all existed game objects by given name. * @method * @param name - The name. * @returns Array of game objects with given name. * @example * game.GetGameObjectsByName('exampleName'); */ Game.prototype.GetGameObjectsByName = function (name) { if (typeof name != 'string') throw new Error('Name of object must be string!'); var result = []; for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var gameObject = _a[_i]; if (gameObject.name === name) result.push(gameObject); } return result; }; /** * Gets all existed game objects by given tag. * @method * @param tag The - tag. * @returns The array of game objects with given tag. * @example * game.GetGameObjectsByTag('exampleTag'); */ Game.prototype.GetGameObjectsByTag = function (tag) { if (typeof tag != 'string') throw new Error('Name of object must be string!'); var result = []; for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var gameObject = _a[_i]; if (gameObject.tag === tag) result.push(gameObject); } return result; }; /** * Gets game object by unique id. * @method * @param id - The unique id * @returns The game object * @example * game.GetGameObjectById('51870300-4187221613-3012590175-3461657014'); */ Game.prototype.GetGameObjectById = function (id) { if (typeof id !== 'string') throw new Error('Param id must be a string!'); for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) { var gameObject = _a[_i]; if (gameObject.id === id) return gameObject; } return undefined; }; // Sounds Management /** * Plays sound at the page. * @method * @param path - The path to sound file * @param loop - is looped? * @param volume - The volume decimal midpoint * @example * game.PlaySound('./resources/sounds/death.mp3', false, 0.8); */ Game.prototype.PlaySound = function (path, loop, volume) { if (loop === void 0) { loop = false; } if (volume === void 0) { volume = 1; } var audio = new Audio(); var src = document.createElement('source'); src.type = 'audio/mpeg'; src.src = path; audio.appendChild(src); audio.loop = loop; audio.volume = volume; audio.play().then(); }; Object.defineProperty(Game.prototype, "mousePos", { /** * Use `input.mouseWorldPosition` instead. * @deprecated since version 1.0.8 */ get: function () { return this.input.mouseWorldPosition; }, enumerable: false, configurable: true }); Object.defineProperty(Game.prototype, "mouseClientPos", { /** * Use `input.mouseClientPosition` instead. * @deprecated since version 1.0.8 */ get: function () { return this.input.mouseClientPosition; }, enumerable: false, configurable: true }); Object.defineProperty(Game.prototype, "mousePrecisePos", { /** * Use `input.mousePreciseWorldPosition` instead. * @deprecated since version 1.0.8 */ get: function () { return this.input.mousePreciseWorldPosition; }, enumerable: false, configurable: true }); Object.defineProperty(Game.prototype, "isMousePrimaryButtonDown", { /** * Use `input.isMousePrimaryButtonDown` instead. * @deprecated since version 1.0.8 */ get: function () { return this.input.isMousePrimaryButtonDown; }, enumerable: false, configurable: true }); Object.defineProperty(Game.prototype, "mouseHoveredGameObject", { get: function () { for (var i = this.gameObjects.length - 1; i >= 0; i -= 1) { var gameObject = this.gameObjects[i]; if (!gameObject.enabled) continue; if (!(gameObject instanceof ClickableGameObject_1.ClickableGameObject)) continue; var clickableObj = gameObject; if (clickableObj.ignoreRaycast) continue; if (clickableObj.transform.scale.x <= 0 || clickableObj.transform.scale.y <= 0) continue; var minX = clickableObj.transform.position.x; var maxX = clickableObj.transform.position.x + clickableObj.transform.scale.x; var minY = clickableObj.transform.position.y; var maxY = clickableObj.transform.position.y + clickableObj.transform.scale.y; var isInRange = (0, MathUtils_1.IsInRange)(this.input.mousePreciseWorldPosition.x, minX, maxX) && (0, MathUtils_1.IsInRange)(this.input.mousePreciseWorldPosition.y, minY, maxY); if (isInRange) { return clickableObj; } } return undefined; }, enumerable: false, configurable: true }); Game.prototype.constructMouseEvent = function () { var gameMouseEvent = { game: this, mousePos: this.input.mouseWorldPosition, mousePrecisePos: this.input.mousePreciseWorldPosition, mouseClientPos: this.input.mouseClientPosition, isMouseDown: this.input.isMousePrimaryButtonDown, mouseWorldPosition: this.input.mouseWorldPosition, mousePreciseWorldPositon: this.input.mousePreciseWorldPosition, mouseLocalPosition: this.input.mouseLocalPosition, mousePreciseLocalPosition: this.input.mousePreciseLocalPosition, mouseScrollDelta: this.input.mouseScrollDelta, }; return gameMouseEvent; }; Game.prototype.mouseMoveHandler = function (game, event) { game.input.mouseClientPosition = new Vector2_1.Vector2(event.offsetX, event.offsetY); var worldPosition = game.input.mouseClientPosition.clone(); var gridSize = game.renderer.gridSize; worldPosition.divide(gridSize); var localPosition = worldPosition.clone(); game.input.mousePreciseLocalPosition = localPosition.clone().floor(); game.input.mouseLocalPosition = localPosition.clone().floor(); worldPosition.add(this.canvasViewOffset); game.input.mousePreciseWorldPosition = worldPosition.clone(); worldPosition.floor(); game.input.mouseWorldPosition = worldPosition; var mouseEvent = game.constructMouseEvent(); game.emit('mouseMove', mouseEvent); }; Game.prototype.mouseDownHandler = function (game) { var gameMouseEvent = game.constructMouseEvent(); game.input.isMousePrimaryButtonDown = true; var hoveredGameObject = this.mouseHoveredGameObject; if (hoveredGameObject !== undefined) hoveredGameObject.OnMouseDown(gameMouseEvent); game.emit('mouseDown', gameMouseEvent); }; Game.prototype.mouseUpHandler = function (game) { var gameMouseEvent = game.constructMouseEvent(); game.input.isMousePrimaryButtonDown = false; var hoveredGameObject = this.mouseHoveredGameObject; if (hoveredGameObject !== undefined) hoveredGameObject.OnMouseUp(gameMouseEvent); game.emit('mouseUp', gameMouseEvent); }; Game.prototype.mouseClickHandler = function (game) { var gameMouseEvent = game.constructMouseEvent(); var hoveredGameObject = this.mouseHoveredGameObject; if (hoveredGameObject !== undefined) hoveredGameObject.OnMouseClick(gameMouseEvent); game.emit('mouseClick', gameMouseEvent); }; Game.prototype.mouseWheelHandler = function (game, event) { event.preventDefault(); this._tempMouseWheelDeltaY = -event.deltaY; }; Game.prototype.GetRandomPosition = function () { return new Vector2_1.Vector2((0, MathUtils_1.floor)(Math.random() * this.grid.x + this.canvasViewOffset.x), (0, MathUtils_1.floor)(Math.random() * this.grid.y + this.canvasViewOffset.y)); }; Game.prototype.GetRandomPositionIn = function (min, max) { return new Vector2_1.Vector2((0, MathUtils_1.RandomInRange)(min.x, max.x), (0, MathUtils_1.RandomInRange)(min.y, max.x)); }; return Game; }()); exports.Game = Game;