UNPKG

fisk-engine

Version:

Typescript based HTML5 game engine

567 lines (552 loc) 20.1 kB
import { Howl } from 'howler'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } class FiskGame { constructor({ height, width, images = [], sounds = [], selector, imageSmoothing = false, stageData = [], onReady = () => { }, customCollision, } = { height: 0, width: 0, selector: "", }) { this.customCollision = () => { }; this.imagesLoaded = 0; this.totalImages = 0; this.soundsLoaded = 0; this.totalSounds = 0; this.sounds = {}; this.soundNames = []; this.currentKeys = []; this.stageData = {}; this.firstInteract = false; this.width = width; this.height = height; this.canvas = this.createMainCanvas(selector); this.context = this.canvas.getContext("2d"); this.ctx = this.context; this.updateScale(); this.bindScreenResize(); this.setImageSmoothing(imageSmoothing); this.customCollision = customCollision ? customCollision : (a, b) => { }; this.totalImages = images.length; this.totalSounds = sounds.length; this.images = {}; this.onReady = onReady; this.audioContext = new AudioContext(); this.preloadImages(images, () => { this.preloadSounds(sounds, () => { this.preloadData(stageData, () => { this.bindClick(); this.setupKeyboardBinding(); this.render(); this.logicLoop = window.setInterval(this.logic.bind(this), 33); this.onReady(this); }); }); }); } checkFirstInteract() { if (this.firstInteract === false && this.audioContext.state === "suspended") { this.firstInteract = true; this.audioContext.resume(); } } onKeydown(event) { this.checkFirstInteract(); if (this.currentStage) { this.currentStage.interactors.forEach((entity) => { if (entity.onKeydown) { entity.onKeydown(event, this); } }); this.currentStage.onKeydownQueue.forEach((func) => { func(event, this); }); } } onKeyup(event) { this.checkFirstInteract(); if (this.currentStage) { this.currentStage.interactors.forEach((entity) => { if (entity.onKeyup) { entity.onKeyup(event, this); } }); this.currentStage.onKeyupQueue.forEach((func) => { func(event, this); }); } } setupKeyboardBinding() { this.currentKeys = []; document.addEventListener("keydown", (event) => { const index = this.currentKeys.indexOf(event.key); if (index < 0) { this.currentKeys.push(event.key); this.onKeydown(event); } }, false); document.addEventListener("keyup", (event) => { const index = this.currentKeys.indexOf(event.key); if (index >= 0) { this.currentKeys.splice(index, 1); this.onKeyup(event); } }, false); } stopAllSounds() { this.soundNames.forEach((name) => { this.sounds[name].stop(); }); } preloadImages(arr, callback) { function last(game, passedCallback) { game.imagesLoaded += 1; if (game.imagesLoaded === game.totalImages) { passedCallback(); } } if (arr.length > 0) { arr.forEach((url) => { let image = new Image(); image.onload = () => { this.images[url] = image; last(this, callback); }; image.src = url; }); } else { callback(); } } preloadSounds(arr, callback) { function last(game, passedCallback) { game.soundsLoaded += 1; if (game.soundsLoaded === game.totalSounds) { passedCallback(); } } if (arr.length > 0) { arr.forEach((options) => { options.onload = () => { last(this, callback); }; this.sounds[options.name] = new Howl(options); this.soundNames.push(options.name); }); } else { callback(); } } preloadData(arr, callback) { let count = 0; if (arr.length > 0) { arr.forEach((url) => __awaiter(this, void 0, void 0, function* () { const response = yield fetch(url); const data = yield response.json(); this.stageData[url] = data; count += 1; if (count === arr.length) { callback(); } })); } else { callback(); } } get scale() { const scaleX = window.innerWidth / this.canvas.offsetWidth; const scaleY = window.innerHeight / this.canvas.offsetHeight; const scaleToFit = Math.min(scaleX, scaleY); return scaleToFit; } updateScale() { const scale = this.scale; const canvasSize = this.canvas.offsetWidth * scale; const initTranslateX = (window.innerWidth - this.canvas.offsetWidth) / 2; const initTranslateY = (window.innerHeight - this.canvas.offsetHeight) / 2; this.canvas.style.transformOrigin = "50% 50%"; this.canvas.style.transform = `translateX(${initTranslateX}px) translateY(${initTranslateY}px) scale(${scale})`; } bindScreenResize() { window.addEventListener("resize", this.updateScale.bind(this), false); } getClick(event) { return { x: event.offsetX - 5, y: event.offsetY - 5, width: 10, height: 10, }; } getTouch(event, game) { const canvasBounds = game.canvas.getBoundingClientRect(); const x = event.changedTouches[0].pageX - canvasBounds.left; const y = event.changedTouches[0].pageY - canvasBounds.top; const percentWidth = ((x - 5) / canvasBounds.width) * 100; const percentHeight = ((y - 5) / canvasBounds.height) * 100; return { x: (game.width * percentWidth) / 100, y: (game.height * percentHeight) / 100, width: 10, height: 10, }; } onClick(event) { this.checkFirstInteract(); if (this.currentStage) { event.preventDefault(); const click = this.getClick(event); let clicked; this.currentStage.interactors.forEach((element) => { if (this.simpleCollisionCheck(click, element)) { clicked = element; } }); if (!clicked) { this.currentStage.onClickQueue.forEach((func) => { func(event, this); }); } else { if (clicked.onClick) { clicked.onClick(event, this); } } } } onTouch(event) { this.checkFirstInteract(); if (this.currentStage) { event.preventDefault(); const touch = this.getTouch(event, this); let touched; this.currentStage.interactors.forEach((element) => { if (this.simpleCollisionCheck(touch, element)) { touched = element; } }); if (!touched) { this.currentStage.onTouchQueue.forEach((func) => { func(event, this); }); } else { if (touched.onTouch) { touched.onTouch(event, this); } } } } onMouseMove(event) { if (this.currentStage) { event.preventDefault(); if (this.currentStage.onMouseMoveQueue.length > 0) { this.currentStage.onMouseMoveQueue.forEach((func) => { func(event, this); }); } } } onMouseUp(event) { if (this.currentStage) { event.preventDefault(); if (this.currentStage.onMouseUpQueue.length > 0) { this.currentStage.onMouseUpQueue.forEach((func) => { func(event, this); }); } } } onMouseDown(event) { if (this.currentStage) { event.preventDefault(); if (this.currentStage.onMouseDownQueue.length > 0) { this.currentStage.onMouseDownQueue.forEach((func) => { func(event, this); }); } } } onTouchMove(event) { if (this.currentStage) { event.preventDefault(); if (this.currentStage.onTouchMoveQueue.length > 0) { this.currentStage.onTouchMoveQueue.forEach((func) => { func(event, this); }); } } } onTouchEnd(event) { if (this.currentStage) { event.preventDefault(); if (this.currentStage.onTouchEndQueue.length > 0) { this.currentStage.onTouchEndQueue.forEach((func) => { func(event, this); }); } } } bindClick() { this.canvas.addEventListener("touchstart", this.onTouch.bind(this)); this.canvas.addEventListener("touchend", this.onTouchEnd.bind(this)); this.canvas.addEventListener("click", this.onClick.bind(this)); this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this)); this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this)); this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this)); this.canvas.addEventListener("touchmove", this.onTouchMove.bind(this)); } createMainCanvas(selector) { const canvas = document.createElement("canvas"); const parent = document.querySelector(selector); if (canvas && parent) { canvas.width = this.width; canvas.height = this.height; parent.appendChild(canvas); return canvas; } else { throw `Selector: "${selector}" doesn't exist in document`; } } setImageSmoothing(smoothing) { this.ctx.imageSmoothingEnabled = smoothing; } render() { this.ctx.clearRect(0, 0, this.width, this.height); if (this.currentStage) { this.currentStage.renderQueue.forEach((element) => { element.render(this.ctx); }); } window.requestAnimationFrame(this.render.bind(this)); } collisionCheck(a, b) { if (this.customCollision !== null) { return this.customCollision(a, b); } else { return this.simpleCollisionCheck(a, b); } } simpleCollisionCheck(a, b) { if (a.width && b.width && a.height && b.height) { if ((a.x >= b.x && a.x <= b.x + b.width) || (a.x + a.width >= b.x && a.x + a.width <= b.x + b.width)) { if ((a.y >= b.y && a.y <= b.y + b.height) || (a.y + a.height >= b.y && a.y + a.height <= b.y + b.height)) { return true; } return false; } } return false; } logic() { if (this.currentStage) { this.currentStage.logicQueue.forEach((element) => { element.logic(this); }); } } } class StaticImage { constructor({ image, x, y, startX, startY, width, height }) { this.renderable = true; this.imageUrl = image instanceof HTMLImageElement ? image.src : 'canvas'; this.x = x; this.y = y; this.image = image; this.width = width ? width : this.image.width; this.height = height ? height : this.image.height; this.startX = startX; this.startY = startY; } render(ctx) { if (this.startX !== undefined && this.startY !== undefined) { ctx.drawImage(this.image, this.startX, this.startY, this.width, this.height, this.x, this.y, this.width, this.height); } else { ctx.drawImage(this.image, this.x, this.y, this.width, this.height); } } } class InteractiveImage extends StaticImage { constructor(config) { super(config); this.interactive = true; if (config.onClick) { this.onClick = config.onClick; } if (config.onTouch) { this.onTouch = config.onTouch; } if (config.onKeydown) { this.onKeydown = config.onKeydown; } if (config.onKeyup) { this.onKeyup = config.onKeyup; } } } function isEngine(entity) { return entity.currentStage !== undefined; } function removeFromStage(entity, gameStage) { const stage = isEngine(gameStage) ? gameStage.currentStage : gameStage; const entityIndex = stage.entities.indexOf(entity); const logicIndex = stage.logicQueue.indexOf(entity); const renderIndex = stage.renderQueue.indexOf(entity); const collisionIndex = stage.collisionQueue.indexOf(entity); const interactorsIndex = stage.interactors.indexOf(entity); if (entityIndex >= 0) { stage.entities.splice(entityIndex, 1); } if (logicIndex >= 0) { stage.logicQueue.splice(logicIndex, 1); } if (renderIndex >= 0) { stage.renderQueue.splice(renderIndex, 1); } if (collisionIndex >= 0) { stage.collisionQueue.splice(collisionIndex, 1); } if (interactorsIndex >= 0) { stage.interactors.splice(interactorsIndex, 1); } } function hexToRgb(hex) { var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (result) { return `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})`; } else { return null; } } function rgbToRgba(rgb, alpha) { const index = rgb.indexOf("rgba") >= 0 ? 5 : 4; const split = rgb.split(","); if (index === 4) { return `rgba(${split[0].substring(index)}, ${split[1].trim()}, ${split[2] .trim() .substring(0, split[2].trim().indexOf(")"))}, ${alpha})`; } else { return `rgba(${split[0].substring(index)}, ${split[1].trim()}, ${split[2].trim()}, ${alpha})`; } } function randomNumberBetween(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1) + min); } function countDecimals(value) { let text = value.toString(); // verify if number 0.000005 is represented as "5e-6" if (text.indexOf("e-") > -1) { let [base, trail] = text.split("e-"); let deg = parseInt(trail, 10); return deg; } // count decimals for number in representation like "0.123456" if (Math.floor(value) !== value) { return value.toString().split(".")[1].length || 0; } return 0; } function randomDecimalBetween(min, max) { const minDec = countDecimals(min); const maxDec = countDecimals(max); const decimalPlaces = Math.max(minDec, maxDec); const rand = Math.random() * (max - min) + min; const power = Math.pow(10, decimalPlaces); return Math.floor(rand * power) / power; } class GameStage { constructor(config = { entities: [], gameReference: {}, }) { this.logicQueue = []; this.renderQueue = []; this.onClickQueue = []; this.entities = []; this.onClickQueue = config.onClickQueue ? config.onClickQueue : []; this.onTouchQueue = config.onTouchQueue ? config.onTouchQueue : []; this.onTouchMoveQueue = config.onTouchMoveQueue ? config.onTouchMoveQueue : []; this.onTouchEndQueue = config.onTouchEndQueue ? config.onTouchEndQueue : []; this.onMouseMoveQueue = config.onMouseMoveQueue ? config.onMouseMoveQueue : []; this.onMouseUpQueue = config.onMouseUpQueue ? config.onMouseUpQueue : []; this.onMouseDownQueue = config.onMouseDownQueue ? config.onMouseDownQueue : []; this.onKeydownQueue = config.onKeydownQueue ? config.onKeydownQueue : []; this.onKeyupQueue = config.onKeyupQueue ? config.onKeyupQueue : []; this.collisionQueue = []; this.interactors = []; this.setupEntities(this.entities, config.gameReference); this.populateEntities(config.entities); } setupEntities(entities, game) { entities.forEach((entity) => { if (entity.setup) { entity.setup(game); } }); } populateEntities(entities) { entities.forEach((entity) => { this.entities.push(entity); const interactable = entity; if (interactable.interactive) { this.interactors.push(interactable); } const logical = entity; if (logical.logical) { this.logicQueue.push(logical); } const renderable = entity; if (renderable.renderable) { this.renderQueue.push(renderable); } const collidable = entity; if (collidable.collidable) { this.collisionQueue.push(collidable); } }); } removeEntity(entity) { removeFromStage(entity, this); } } export { FiskGame, GameStage, InteractiveImage, StaticImage, hexToRgb, randomDecimalBetween, randomNumberBetween, removeFromStage, rgbToRgba };