UNPKG

@fuwu-yuan/bgew

Version:

Baguette Game Engine Web is a french 2D Web game engine

650 lines (649 loc) 22.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Board = void 0; const network_1 = require("./network"); const config_1 = require("./config"); const entities_1 = require("./entities"); const Debug_1 = require("../classes/Debug"); const Dispatcher_1 = require("../classes/Dispatcher"); const Gameloop_1 = require("../classes/Gameloop"); const detect_collisions_1 = require("detect-collisions"); const sound_1 = require("./sound"); const Stats = require("stats.js"); /** * The borad is the main part of your Game */ class Board { constructor(name, version, width, height, gameElement = null, background = "transparent", enableHIDPI = false, enableCollisionSystem = true) { this.entities = []; this.gameloop = null; this.gameloopId = null; this.steps = {}; this._step = null; this._lastCursor = "default"; this.dispatcher = new Dispatcher_1.Dispatcher(); this._scale = 1; this._gravity = 0; this._collisionSystem = null; this._collisionResult = null; this._sounds = {}; this._events = []; this._paused = false; this._name = name; this._version = version; this._networkManager = new network_1.NetworkManager(this); this._config = new config_1.BoardConfig(); this._debug = new Debug_1.Debug(); if (enableCollisionSystem) { this._collisionSystem = new detect_collisions_1.Collisions(); this._collisionResult = this._collisionSystem.createResult(); } this.config.board.size.width = width; this.config.board.size.height = height; this.config.board.background = background; this._gameHTMLElement = gameElement !== null ? gameElement : document.body; this.canvas = this.createCanvasElem(enableHIDPI); this._ctx = this.canvas.getContext('2d'); if (enableHIDPI) { this.ctx.scale(this.getPixelRatio(), this.getPixelRatio()); } this.defaultStrokeStyle = this.ctx.strokeStyle; this.defaultFillStyle = this.ctx.fillStyle; this.initEvents(); } createCanvasElem(enableHIDPI) { const elem = document.createElement('canvas'); elem.width = this.config.board.size.width; elem.height = this.config.board.size.height; elem.style.background = this.config.board.background; this._gameHTMLElement.style.position = "relative"; this._gameHTMLElement.appendChild(elem); if (enableHIDPI) { elem.width = this.config.board.size.width * this.getPixelRatio(); elem.height = this.config.board.size.height * this.getPixelRatio(); elem.style.width = this.config.board.size.width + "px"; elem.style.height = this.config.board.size.height + "px"; } return elem; } countEntities() { let count = this.entities.length; for (const entity of this.entities) { if (entity instanceof entities_1.Container) { count += entity.countEntities(); } } return count; } /** * Start the game loop (update and draw entities) */ start() { if (this.debug.stats) { var stats = new Stats(); stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom document.body.appendChild(stats.dom); } this.step.onEnter({}); this.gameloop = new Gameloop_1.Gameloop(); this.gameloopId = this.gameloop.setGameLoop((delta) => { if (this.debug.stats) stats.begin(); // Canvas resize handle this.canvas.width = this.config.board.size.width * this.scale; this.canvas.height = this.config.board.size.height * this.scale; // Inputs this.dispatchMouseEvents(); this.dispatchKeyboardEvents(); if (!this.paused) { // Update this.step.update(delta * 1000); // Collision if (this.collisionSystem) { this.collisionSystem.update(); } this.step.checkCollisions(); // Draw this.step.draw(); } if (this.debug.stats) stats.end(); //console.log(Math.round(1000/(delta*1000)) + " FPS"); }, 1000 / this._config.game.FPS); } /*private loop(lastUpdate: number, lastLoopDuration: number = 0) { window.setTimeout22(() => { let now = (new Date()).getTime(); let delta = now - lastUpdate; lastUpdate = now; this.canvas.width = this.config.board.size.width * this.scale; this.canvas.height = this.config.board.size.height * this.scale; this.step.update(delta); this.step.draw(); let after = (new Date()).getTime(); this.loop(lastUpdate, after-now); }, (1000/this._config.game.FPS)); }*/ /** * Stop the game loop (game will freeze) */ stop() { if (this.gameloop && this.gameloopId) { this.gameloop.clearGameLoop(this.gameloopId); } } /** * Add a step in step list (you need to add your game step before calling moveToStep(step)) * @param step */ addStep(step) { this.steps[step.name] = step; } /** * Add an array of steps (you need to add your game step before calling moveToStep(step)) * @param steps */ addSteps(steps) { steps.forEach((step) => { this.addStep(step); }); } get paused() { return this._paused; } set paused(paused) { this._paused = paused; } pause() { this.paused = true; } resume() { this.paused = false; } get version() { return this._version; } set version(value) { this._version = value; } get name() { return this._name; } set name(value) { this._name = value; } /** * Get Network Manager */ get networkManager() { return this._networkManager; } set networkManager(networkManager) { this._networkManager = networkManager; } get canvas() { return this._canvas; } set canvas(value) { this._canvas = value; } /** * Get the current step */ get step() { return this._step; } /** * Set the current step (no transitions function is called (onEnter / onLeave)) * * @param step */ set step(step) { this._step = step; } /** * The the game config */ get config() { return this._config; } /** * Change the game step (will call onLeave of current step, and onEnter on new step) * @param step * @param data * @param fade */ moveToStep(step, data = {}, fade) { if (fade) { const self = this; this.addEntity(new class extends entities_1.Rectangle { constructor() { super(...arguments); this._opacity = 0; } update(delta) { if (this.opacity < 1) { this.opacity += 1 / (fade.duration / delta); if (this.opacity > 1) { this.opacity = 1; } } else { if (self.step) { self.step.onLeave(); } self.step = self.steps[step]; if (self.step) { self.reset(); self.step.onEnter(data); self.addEntity(new class extends entities_1.Rectangle { constructor() { super(...arguments); this._opacity = 1; } update(delta) { if (this.opacity > 0) { this.opacity -= 1 / (fade.duration / delta); if (this.opacity < 0) { this.opacity = 0; } } else { self.removeEntity(this); } } }(0, 0, this.width, this.height, fade.color, fade.color)); self.step.draw(); } else { console.error("No step found with name '" + step + "'"); } } } }(0, 0, this.width, this.height, fade.color, fade.color)); } else { if (this.step) { this.step.onLeave(); } this.step = this.steps[step]; if (this.step) { this.reset(); this.step.onEnter(data); this.step.draw(); } else { console.error("No step found with name '" + step + "'"); } } } /** * Add an entity to the board. * Entity will be updated and drawn immediately (calling update() and draw() from entity) * * @param entity */ addEntity(entity) { entity.init(this); this.entities.push(entity); } /** * Add multiple entities * This function will call addEntity(entity) for each entity of the array * @param entities */ addEntities(entities) { for (const entity of entities) { this.addEntity(entity); } } /** * Find an entity by id * @param id * @param recursive search recursively in entity that can contains other entities (like Entities.Container) * @return Entity found entity with the given id or undefined if not found */ findEntity(id, recursive = false) { let entity = this.entities.find((entity) => { return entity.id === id; }); if (typeof entity === "undefined" && recursive) { for (const e of this.entities) { if (e instanceof entities_1.Container) { entity = e.findEntity(id, recursive); if (typeof entity !== 'undefined') break; } } } return entity; } /** * Remove the entity from board. * The entity will disappear from the game * @param entity */ removeEntity(entity) { const index = this.entities.indexOf(entity, 0); if (index > -1) { if (this.entities[index] instanceof entities_1.Container) { this.entities[index].removeEntities(this.entities[index].entities); } this.entities.splice(index, 1); if (this.collisionSystem) { this.collisionSystem.remove(entity.body); } } } /** * Remove all the entities from board. * The entities will disappear from the game * @param entities */ removeEntities(entities) { for (let entity of entities) { this.removeEntity(entity); } } /** * Change the cursor * @param cursor */ changeCursor(cursor) { this._lastCursor = document.body.style.cursor; document.body.style.cursor = cursor; } restoreCursor() { document.body.style.cursor = this._lastCursor; } /** * Register a sound to play it later using function {@link playSound} * @param name Sound name * @param src One or more src (same sound with multiple src formats) * @param repeat Ether to repeat of not the sound at the end * @param volume Volume to play sound by default (default 0.5) * @param sprites Object defining sprites in this sound (optional) * @return Sound the newly created sound */ registerSound(name, src, repeat = false, volume = 0.5, sprites = {}) { let sound = new sound_1.Sound(name, src, repeat, volume, sprites); this._sounds[name] = sound; return sound; } getSound(name) { if (typeof this._sounds[name] !== 'undefined') { return this._sounds[name]; } console.error("No sound registed with name '" + name + "'. Registered sounds : " + Object.keys(this._sounds).join(' | ')); return null; } /** * Play a sound previously registered using function {@link registerSound} * @param name the registered sound name * @param repeat play the sound again at the end * @param volume volume to play the sound (between 0.0 and 1.0) * @param sprite the sprite to play */ playSound(name, repeat, volume, sprite) { if (typeof this._sounds[name] !== 'undefined') { return this._sounds[name].play(repeat, volume, sprite); } console.error("No sound registed with name '" + name + "'. Registered sounds : " + Object.keys(this._sounds).join(' | ')); return null; } /** * Stop the sound * * @param name Sound name * @param fadeout Ether to fadeout the sound before stopping it * @param fadeDuration Fadeout duration * @param id Optional sound id */ stopSound(name, fadeout = false, fadeDuration = 1000, id) { if (typeof this._sounds[name] !== 'undefined') { this._sounds[name].stop(fadeout, fadeDuration, id); } else { console.error("No sound registed with name '" + name + "'. Registered sounds : " + Object.keys(this._sounds).join(' | ')); } } /** * Remove all entities and clear the board */ reset() { for (const entity of this.entities) { entity.onDestroy(); } this.entities = []; if (this._collisionSystem) { this._collisionSystem = new detect_collisions_1.Collisions(); } this.clear(); document.body.style.cursor = "default"; } /** * Clear the board (will be drawn again on next game loop) */ clear() { var _a; (_a = this.ctx) === null || _a === void 0 ? void 0 : _a.clearRect(0, 0, this.canvas.width, this.canvas.height); //this.ctx?.beginPath(); } /** * Get the CanvasRenderingContext2D to draw */ get ctx() { return this._ctx; } /** * Reset all styles (stroke and fill) */ resetStyles() { this.ctx.strokeStyle = this.defaultStrokeStyle; this.ctx.fillStyle = this.defaultFillStyle; } /** * On mouse event, catch it and dispatch right event to entities * * @private */ dispatchMouseEvents() { for (let i = 0; i < this._events.length; i++) { let event = this._events[i]; if (event instanceof MouseEvent) { event.preventDefault(); const rect = this.canvas.getBoundingClientRect(); const x = (event.clientX - rect.left) * (1 / this.scale); const y = (event.clientY - rect.top) * (1 / this.scale); this.dispatcher.dispatch(event.type, event, x, y); this.entities.forEach(function (entity) { if (entity.disabled || !entity.visible) return; if (entity.intersect(x, y, event)) { if (event.type === "mousemove") { if (!entity.hovered) { entity.hovered = true; entity.dispatcher.dispatch("mouseenter", new MouseEvent("mouseenter", event), x, y); } } if (event.type === "click") { if (!entity.focus) { entity.focus = true; } } entity.dispatcher.dispatch(event.type, event, x, y); } else { if (event.type === "mousemove") { if (entity.hovered) { entity.hovered = false; entity.dispatcher.dispatch("mouseleave", new MouseEvent("mouseleave", event), x, y); } } if (event.type === "click") { if (entity.focus) { entity.focus = false; } } } }); this._events.splice(i, 1); i--; } } } /** * On keyboard event, catch it and dispatch right event to entities * * @private */ dispatchKeyboardEvents() { for (let i = 0; i < this._events.length; i++) { let event = this._events[i]; if (event instanceof KeyboardEvent) { this.dispatcher.dispatch(event.type, event); this.entities.forEach(function (entity) { if (entity.disabled || !entity.visible) return; if (entity.focus) { entity.dispatcher.dispatch(event.type, event); } }); this._events.splice(i, 1); i--; } } } /** * Listen mouse event on this entity * @param event An event from this list : click, dblclick, contextmenu, mousedown, mouseup, mouseenter, mouseleave, mousemove, all * @param callback */ onMouseEvent(event, callback) { this.dispatcher.on(event, callback); } /** * Listen mouse event on this entity * @param event An event from this list : keyup, keydown, keypress, all * @param callback */ onKeyboardEvent(event, callback) { this.dispatcher.on(event, callback); } get width() { return this.config.board.size.width; } get height() { return this.config.board.size.height; } set width(value) { this.config.board.size.width = value; this.canvas.width = value; } set height(value) { this.config.board.size.height = value; this.canvas.height = value; } get scale() { return this._scale; } set scale(value) { this._scale = value; } get gravity() { return this._gravity; } set gravity(value) { this._gravity = value; } get debug() { return this._debug; } get gameHTMLElement() { return this._gameHTMLElement; } getEntitiesAt(x = null, y = null) { let entities = []; for (const entity of this.entities) { if (y === null && x !== null) { if (entity.absX <= x && entity.absX + entity.width >= x) { entities.push(entity); } } else if (x === null && y !== null) { if (entity.absY <= y && entity.absY + entity.height >= y) { entities.push(entity); } } else if (x !== null && y !== null) { if (entity.intersect(x, y) && entities.indexOf(entity) === -1) { entities.push(entity); } } } return entities; } getEntitiesIn(x1, y1, x2, y2) { let entities = []; for (const entity of this.entities) { if (entity instanceof entities_1.Container) { entities = entities.concat(entity.getEntitiesIn(x1, y1, x2, y2)); } else if (entity.absX < x2 && entity.absX + entity.width > x1 && entity.absY < y2 && entity.height + entity.absY > y1) { entities.push(entity); } } return entities; } getPixelRatio() { return (window.devicePixelRatio || 1); } ; /** * Shortcut to add timer to current GameStep {@link GameStep.addTimer} * @param time * @param end * @param repeat */ addTimer(time, end, repeat = false) { return this.step.addTimer(time, end, repeat); } /** * Shortcut to remove timer from current GameStep {@link GameStep.removeTimer} * @param timer */ removeTimer(timer) { this.step.removeTimer(timer); } get collisionSystem() { return this._collisionSystem; } get collisionResult() { return this._collisionResult; } saveEventForLater(event) { this._events.push(event); } /** * Init all events from the board * * @private */ initEvents() { this.canvas.addEventListener('click', this.saveEventForLater.bind(this)); this.canvas.addEventListener('dblclick', this.saveEventForLater.bind(this)); this.canvas.addEventListener('contextmenu', this.saveEventForLater.bind(this)); this.canvas.addEventListener('mousedown', this.saveEventForLater.bind(this)); this.canvas.addEventListener('mouseup', this.saveEventForLater.bind(this)); this.canvas.addEventListener('mouseenter', this.saveEventForLater.bind(this)); this.canvas.addEventListener('mouseleave', this.saveEventForLater.bind(this)); this.canvas.addEventListener('mousemove', this.saveEventForLater.bind(this)); window.addEventListener("keyup", this.saveEventForLater.bind(this)); window.addEventListener("keydown", this.saveEventForLater.bind(this)); window.addEventListener("keypress", this.saveEventForLater.bind(this)); } } exports.Board = Board;