diff --git a/index.html b/index.html index 5f26784..1f0cf21 100644 --- a/index.html +++ b/index.html @@ -7,12 +7,12 @@ - -
-
-
-
-
+ + + + + + diff --git a/index.js b/index.js index 9d7ed3c..229cf93 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,12 @@ -import MainTileSet from './content/tilesets/MainTileSet.js'; -import MainTileMap from './content/tilemaps/MainTileMap.js'; -import CustomCanvas from './src/canvases/СustomCanvas.js'; -import ResizeableCanvasMixin from './src/canvases/mixins/resizeable.js'; -import TileableCanvasMixin from './src/canvases/mixins/tileable.js'; -import DrawableCanvasMixin from './src/canvases/mixins/drawable.js'; +import TileSet from './src/TileSet/index.js'; +import TileMap from './src/TileMap/index.js'; +import CustomCanvas from './src/Сanvas/index.js'; +import ResizeableCanvasMixin from './src/Сanvas/mixins/resizeableCanvas.js'; +import TileableCanvasMixin from './src/Сanvas/mixins/tileableCanvas.js'; +import DrawableCanvasMixin from './src/Сanvas/mixins/drawableCanvas.js'; import drawImageFromMap from './src/utils/drawImageFromMap.js'; +import Character from './src/utils/classes/Character.js'; +import Scene from './src/Scene'; const MainCanvas = DrawableCanvasMixin(TileableCanvasMixin(ResizeableCanvasMixin(CustomCanvas))); @@ -36,7 +38,7 @@ const main = async () => { const mainCanvas = await MainCanvas.create({ el: document.getElementById('main'), size: { width: 512, height: 512 } }); const saveButton = createButton(document.body, 'Save', () => saveMap(mainCanvas)); const currentTileCanvas = await CustomCanvas.create({ el: document.getElementById('current'), size: { width: 64, height: 64 } }); - const mainTileSet = await MainTileSet.create({ el: document.getElementById('tileSet') }); + const mainTileSet = await TileSet.create({ el: document.getElementById('tileSet') }); mainTileSet.addEventListener(':multiSelect', ({ tiles }) => { mainCanvas.updateCurrentTiles(tiles); @@ -49,7 +51,77 @@ const main = async () => { } }); - const tileMap = await MainTileMap.create({ el: document.getElementById('tileMap'), size: { width: 512, height: 512 } }); + const tileMap = await TileMap.create({ el: document.getElementById('tileMap'), size: { width: 512, height: 512 } }); } -main(); +// main(); +async function test() { + // const canvas = document.createElement('canvas'); + // canvas.width = 500; + // canvas.height = 500; + // document.body.append(canvas); + // const ctx = canvas.getContext('2d'); + const scene = new Scene(document.body); + const player = await Character.create({ + coreElement: scene, + position: { x: 0, y: 0 }, + mainSettings: { + mainFlipbook: './content/sources/PNG/Knight/knight.png', + speed: 300, + }, + moveSettings: { + moveFlipbook: [ + './content/sources/PNG/Knight/Run/run1.png', + './content/sources/PNG/Knight/Run/run2.png', + './content/sources/PNG/Knight/Run/run3.png', + './content/sources/PNG/Knight/Run/run4.png', + './content/sources/PNG/Knight/Run/run5.png', + './content/sources/PNG/Knight/Run/run6.png', + './content/sources/PNG/Knight/Run/run7.png', + './content/sources/PNG/Knight/Run/run8.png', + ], + }, + jumpSettings: { + jumpFlipbook: [ + './content/sources/PNG/Knight/Jump/jump1.png', + './content/sources/PNG/Knight/Jump/jump2.png', + './content/sources/PNG/Knight/Jump/jump3.png', + './content/sources/PNG/Knight/Jump/jump4.png', + './content/sources/PNG/Knight/Jump/jump5.png', + './content/sources/PNG/Knight/Jump/jump6.png', + './content/sources/PNG/Knight/Jump/jump7.png', + ], + }, + attackSettings: { + attackFlipbook: [ + './content/sources/PNG/Knight/Attack/attack0.png', + './content/sources/PNG/Knight/Attack/attack1.png', + './content/sources/PNG/Knight/Attack/attack2.png', + './content/sources/PNG/Knight/Attack/attack4.png', + ], + }, + }); + + scene.addHero(player); + scene.start(); +// const tick = () => { +// // setTimeout(this.tick, 1000); +// requestAnimationFrame(tick); +// ctx.clearRect(0, 0, canvas.width, canvas.height); +// ctx.drawImage( +// player.render(), +// 0, +// 0, +// player.width, +// player.height, +// 0, +// 0, +// player.width, +// player.height, +// ); +// }; +// tick(); +} + +test(); + diff --git a/src/Scene/index.js b/src/Scene/index.js new file mode 100644 index 0000000..2244da0 --- /dev/null +++ b/src/Scene/index.js @@ -0,0 +1,126 @@ +import { Character } from '../utils/classes'; + +/** + * @class Scene - The core of a game. + */ +export default class Scene { + // _hero = null; + // _ctx = null; + _staticObjects = []; + _dynamicObjects = []; + + /** + * @constructor Scene + * @param {HTMLElement} element - a place where game will be rendering + * @param {number} [width=500] - width of a game viewport + * @param {number} [height=500] - height of a game viewport + */ + constructor(element, width, height) { + this._canvas = document.createElement('canvas'); + this._canvas.width = width || 500; + this._canvas.height = height || 500; + element.append(this._canvas); + this._ctx = this._canvas.getContext('2d'); + } + + /** + * Method to add a main Hero to a game + * @param {Character} hero - Instance of the Character class which would be the main hero of a game. + */ + addHero(hero) { + if (hero instanceof Character) { + this._hero = hero; + } + else throw new Error('Hero should be an instance of the Character class.') + } + + /** + * + * @param object + * @param {string} type - dynamic or static + */ + addObject(object, type) { + if (object && type === 'static') this._staticObjects.push(object); + if (object && type === 'dynamic') this._dynamicObjects.push(object); + } + + start() { + this._paused = false; + this._render(); + } + + pause() { + this._paused = true; + } + + checkBeyondPosition(x, y, width, height) { + if (x <= 0) return false; + if (x + width >= this._canvas.width) return false; + if (y < 0) return false; + return y + height < this._canvas.height; + } + + /** + * If object has collision with any static object returns true. + * @param {Character} object + * @returns {boolean} + */ + checkMoveCollisions(object) { + return this._staticObjects.some(obj => this._detectCollision(object, obj)) + } + + /** + * If object has collision with any dynamic object returns true. + * It means that object receives a damage. + * @param {Character} object + * @returns {boolean} + */ + checkDamageCollisions(object) { + return this._dynamicObjects.some(obj => this._detectCollision(object, obj)) + } + + _render() { + if (this._paused) return; + + requestAnimationFrame(this._render.bind(this)); + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this._renderBackground(); + this._renderObjects(); + this._renderHero(); + } + + _renderBackground() {} + + _renderObjects() { + this._staticObjects.forEach(staticObject => this._renderObject(staticObject)); + this._dynamicObjects.forEach(dynamicObject => this._renderObject(dynamicObject)); + } + + _renderHero() { + this._renderObject(this._hero); + } + + _renderObject(object) { + const { position, width, height } = object; + this._ctx.drawImage( + object.render(), + 0, + 0, + width, + height, + position.x, + position.y, + width, + height, + ); + } + + _detectCollision(a, b) { + const ax2 = a.position.x + a.width; + const ay2 = a.position.y + a.height; + const bx2 = b.position.x + b.width; + const by2 = b.position.y + b.height; + + return !(ax2 < b.x || a.x > bx2 || ay2 < b.y || a.y > by2); + } +} diff --git a/content/tilemaps/MainTileMap.js b/src/TileMap/index.js similarity index 84% rename from content/tilemaps/MainTileMap.js rename to src/TileMap/index.js index 2616f1c..bf916fe 100644 --- a/content/tilemaps/MainTileMap.js +++ b/src/TileMap/index.js @@ -1,14 +1,14 @@ -import { DrawableCanvas } from '../../src/canvases/mixins/drawable.js'; +import { DrawableCanvas } from '../Сanvas/mixins/drawableCanvas.js'; -export default class MainTileMap extends DrawableCanvas { - _imageSrcLink = './content/tilemaps/tilemap.png'; +export default class TileMap extends DrawableCanvas { + _imageSrcLink = './content/TileMap/tilemap.png'; _imageSrc = null; _sourceTileMapSize = { width: null, height: null, }; - _metadataSrcLink = './content/tilemaps/tilemap.json'; + _metadataSrcLink = './content/TileMap/tilemap.json'; _metadataSrc = null; constructor(options= {}) { diff --git a/content/tilemaps/tilemap.json b/src/TileMap/tilemap.json similarity index 100% rename from content/tilemaps/tilemap.json rename to src/TileMap/tilemap.json diff --git a/content/tilemaps/tilemap.png b/src/TileMap/tilemap.png similarity index 100% rename from content/tilemaps/tilemap.png rename to src/TileMap/tilemap.png diff --git a/content/tilesets/MainTileSet.js b/src/TileSet/index.js similarity index 78% rename from content/tilesets/MainTileSet.js rename to src/TileSet/index.js index 6630950..38aab92 100644 --- a/content/tilesets/MainTileSet.js +++ b/src/TileSet/index.js @@ -1,14 +1,14 @@ -import { Tileable } from '../../src/canvases/mixins/tileable.js'; -import SelectableCanvasMixin from '../../src/canvases/mixins/selectable.js'; -import ResizeableCanvasMixin from '../../src/canvases/mixins/resizeable.js'; -import buildEvent from '../../src/utils/buildEvent.js'; -import Tile from '../../src/classes/Tile.js'; +import { TileableCanvas } from '../Сanvas/mixins/tileableCanvas.js'; +import SelectableCanvasMixin from '../Сanvas/mixins/selectableCanvas.js'; +import ResizeableCanvasMixin from '../Сanvas/mixins/resizeableCanvas.js'; +import buildEvent from '../utils/buildEvent.js'; +import Tile from '../utils/classes/Tile.js'; -export default class MainTileSet extends SelectableCanvasMixin(ResizeableCanvasMixin(Tileable)) { - _imageSrcLink = 'content/tilesets/mainTileSet.png'; +export default class TileSet extends SelectableCanvasMixin(ResizeableCanvasMixin(TileableCanvas)) { + _imageSrcLink = 'content/TileSet/mainTileSet.png'; _imageSrc = null; - _metadataSrcLink = 'content/tilesets/main-tile-set.json'; + _metadataSrcLink = 'content/TileSet/main-tile-set.json'; _metadataSrc = null; _onMultiSelect({ from, to }) { @@ -78,7 +78,7 @@ export default class MainTileSet extends SelectableCanvasMixin(ResizeableCanvasM await Promise.all(promises); } - //TODO need to check if it needed. We have such method in MainTileMap.js + //TODO need to check if it needed. We have such method in index.js async _loadMetadata() { this._metadataSrc = await (await fetch(this._metadataSrcLink)).json(); } diff --git a/content/tilesets/mainTileSet.png b/src/TileSet/mainTileSet.png similarity index 100% rename from content/tilesets/mainTileSet.png rename to src/TileSet/mainTileSet.png diff --git a/src/utils/classes/Character.js b/src/utils/classes/Character.js new file mode 100644 index 0000000..2f68fa6 --- /dev/null +++ b/src/utils/classes/Character.js @@ -0,0 +1,424 @@ +import Flipbook from './Flipbook.js'; +import Sprite from './Sprite.js'; + +const ERROR_HELP_TEXT = 'Use Character.create method to create character with a set of sprites'; + +export default class Character { + _coreElement = null; + + _prevActionType = 'STOP'; + _currentActionType = 'STOP'; + _direction = 'RIGHT'; + _hooks = { + onStop: null, + onMove: null, + onDamage: null, + }; + + flipbook = null; + mainSettings = { + mainRightFlipbook: null, + mainLeftFlipbook: null, + /** + * The function should return a boolean value which indicates can a Character move or not. + * + * @callback checkPosition + * @returns {boolean} + */ + checkPosition: null, + speed: null, + }; + + moveSettings = { + moveRightFlipbook: null, + moveLeftFlipbook: null, + moveRightCode: 'ArrowRight', + moveLeftCode: 'ArrowLeft', + alternativeMoveRightCode: 'KeyD', + alternativeMoveLeftCode: 'KeyA', + }; + + jumpSettings = { + jumpRightFlipbook: null, + jumpLeftFlipbook: null, + jumpCode: 'ArrowUp', + alternativeJumpCode: 'KeyW', + }; + + attackSettings = { + attackRightFlipbook: null, + attackLeftFlipbook: null, + attackCode: 'Space', + }; + + position = { + x: 0, + y: 0, + }; + + /** + * @constructs The main method to create a character + * @param {Scene} coreElement - canvas on which Character will be rendered + * @param {Object} position - initial Character position + * @param {number} position.x - canvas coordinates + * @param {number} position.y - canvas coordinates + * @param {Object} mainSettings - main Character settings + * @param {string | string[]} mainSettings.mainFlipbook - url or array of url + * @param {checkPosition} mainSettings.checkPosition - function to check any collisions and possibility to move. + * @param {number} mainSettings.speed - speed of a Character in px per second + * @param {Object} moveSettings - settings for move action + * @param {string[]} moveSettings.moveFlipbook - array of url + * @param {string} moveSettings.moveRightCode - main right move action code + * @param {string} moveSettings.moveLeftCode - main left move action code + * @param {string} moveSettings.alternativeMoveRightCode - alternative right move action code + * @param {string} moveSettings.alternativeMoveLeftCode - alternative left move action code + * @param {Object} jumpSettings - settings for jump action + * @param {string[]} jumpSettings.jumpFlipbook - array of url + * @param {string} jumpSettings.jumpCode - main jump action code + * @param {string} jumpSettings.alternativeJumpCode - alternative jump action code + * @param {Object} attackSettings - settings for attack actions + * @param {string[]} attackSettings.attackFlipbook - array of url + * @param {string} attackSettings.attackCode - main attack action code + * @returns {Promise} + */ + static async create({ + coreElement, + position, + mainSettings, + moveSettings = {}, + jumpSettings = {}, + attackSettings = {}, + }) { + const { moveFlipbook, moveFlipbookMeta } = moveSettings; + const { jumpFlipbook, jumpFlipbookMeta } = jumpSettings; + const { attackFlipbook, attackFlipbookMeta } = attackSettings; + + const characterSettings = { + coreElement, + position, + mainSettings, + moveSettings: { ...moveSettings }, + jumpSettings: { ...jumpSettings }, + attackSettings: { ...attackSettings }, + }; + if (typeof mainSettings.mainFlipbook === 'string') characterSettings.mainSettings.mainRightFlipbook = await Flipbook.create([mainSettings.mainFlipbook]); + if (typeof mainSettings.mainFlipbook === 'string') characterSettings.mainSettings.mainLeftFlipbook = await Flipbook.create([mainSettings.mainFlipbook], { mirror: true}); + if (mainSettings.mainFlipbook instanceof Array) characterSettings.mainSettings.mainRightFlipbook = await Flipbook.create(mainSettings.mainFlipbook); + if (mainSettings.mainFlipbook instanceof Array) characterSettings.mainSettings.mainLeftFlipbook = await Flipbook.create(mainSettings.mainFlipbook, { mirror: true }); + if (moveFlipbook instanceof Array) { + characterSettings.moveSettings.moveRightFlipbook = await Flipbook.create(moveFlipbook, moveFlipbookMeta); + characterSettings.moveSettings.moveLeftFlipbook = await Flipbook.create(moveFlipbook, { + ...moveFlipbookMeta, + mirror: true, + }); + } + if (jumpFlipbook instanceof Array) { + characterSettings.jumpSettings.jumpRightFlipbook = await Flipbook.create(jumpFlipbook, jumpFlipbookMeta); + characterSettings.jumpSettings.jumpLeftFlipbook = await Flipbook.create(jumpFlipbook, { + ...jumpFlipbookMeta, + mirror: true, + }); + } + if (attackFlipbook instanceof Array) { + characterSettings.attackSettings.attackRightFlipbook = await Flipbook.create(attackFlipbook, attackFlipbookMeta); + characterSettings.attackSettings.attackLeftFlipbook = await Flipbook.create(attackFlipbook, { + ...attackFlipbookMeta, + mirror: true, + }); + } + + return new Character(characterSettings); + } + + /** + * @param {Scene} coreElement - canvas on which Character will be rendered + * @param {Object} position - initial Character position + * @param {number} position.x - canvas coordinates + * @param {number} position.y - canvas coordinates + * @param {Object} mainSettings - main Character settings + * @param {Flipbook} mainSettings.mainRightFlipbook + * @param {Flipbook} mainSettings.mainLeftFlipbook + * @param {checkPosition} mainSettings.checkPosition - function to check any collisions and possibility to move. + * @param {number} mainSettings.speed - speed of a Character in px per second + * @param {Object} moveSettings - settings for move action + * @param {Flipbook} moveSettings.moveRightFlipbook + * @param {Flipbook} moveSettings.moveLeftFlipbook + * @param {string} moveSettings.moveRightCode - main right move action code + * @param {string} moveSettings.moveLeftCode - main left move action code + * @param {string} moveSettings.alternativeMoveRightCode - alternative right move action code + * @param {string} moveSettings.alternativeMoveLeftCode - alternative left move action code + * @param {Object} jumpSettings - settings for jump action + * @param {Flipbook} jumpSettings.jumpRightFlipbook + * @param {Flipbook} jumpSettings.jumpLeftFlipbook + * @param {string} jumpSettings.jumpCode - main jump action code + * @param {string} jumpSettings.alternativeJumpCode - alternative jump action code + * @param {Object} attackSettings - settings for attack actions + * @param {Flipbook} attackSettings.attackRightFlipbook + * @param {Flipbook} attackSettings.attackLeftFlipbook + * @param {string} attackSettings.attackCode - main attack action code + * @returns {Character} + */ + constructor({ + coreElement, + position, + mainSettings, + moveSettings: { + moveRightFlipbook, + moveLeftFlipbook, + moveRightCode, + moveLeftCode, + alternativeMoveRightCode, + alternativeMoveLeftCode, + } = {}, + jumpSettings: { + jumpRightFlipbook, + jumpLeftFlipbook, + jumpCode, + alternativeJumpCode, + } = {}, + attackSettings: { + attackRightFlipbook, + attackLeftFlipbook, + attackCode, + } = {}, + }) { + if (coreElement) this._coreElement = coreElement; + else throw new Error('coreElement is required for Character!'); + + this._validateFlipbooks(mainSettings.mainRightFlipbook, moveRightFlipbook, jumpRightFlipbook, attackRightFlipbook); + + this.mainSettings = mainSettings; + this.mainSettings.checkPosition = mainSettings.checkPosition || (() => true); + this.moveSettings.moveRightFlipbook = moveRightFlipbook; + this.moveSettings.moveLeftFlipbook = moveLeftFlipbook; + this.jumpSettings.jumpRightFlipbook = jumpRightFlipbook; + this.jumpSettings.jumpLeftFlipbook = jumpLeftFlipbook; + this.attackSettings.attackRightFlipbook = attackRightFlipbook; + this.attackSettings.attackLeftFlipbook = attackLeftFlipbook; + // move codes override + if (moveRightCode) this.moveSettings.moveRightCode = moveRightCode; + if (moveLeftCode) this.moveSettings.moveLeftCode = moveLeftCode; + if (alternativeMoveRightCode) this.moveSettings.alternativeMoveRightCode = alternativeMoveRightCode; + if (alternativeMoveLeftCode) this.moveSettings.alternativeMoveLeftCode = alternativeMoveLeftCode; + // jump codes override + if (jumpCode) this.jumpSettings.jumpCode = jumpCode; + if (alternativeJumpCode) this.jumpSettings.alternativeJumpCode = alternativeJumpCode; + // attack code override + if (attackCode) this.attackSettings.attackCode = attackCode; + + // initial position overrides + if (typeof position.x === 'number') this.position.x = position.x; + if (typeof position.y === 'number') this.position.y = position.y; + + this._actionHandlerHash = { + [this.moveSettings.moveLeftCode]: this.moveLeft.bind(this), + [this.moveSettings.alternativeMoveLeftCode]: this.moveLeft.bind(this), + [this.moveSettings.moveRightCode]: this.moveRight.bind(this), + [this.moveSettings.alternativeMoveRightCode]: this.moveRight.bind(this), + [this.jumpSettings.jumpCode]: this.jump.bind(this), + [this.jumpSettings.alternativeJumpCode]: this.jump.bind(this), + [this.attackSettings.attackCode]: this.attack.bind(this), + }; + this._createOffscreenCanvas(); + this._initListeners(); + this._setOnChangeJumpFrame(); + this.stop(); + } + + get currentActionType() { + return this._currentActionType; + } + + set currentActionType(actionName) { + if (this.currentActionType !== actionName) this._currentActionType = actionName; + if (actionName === 'STOP') { + if (this._hooks.onStop instanceof Function) this._hooks.onStop(); + } + } + + moveRight() { + if (this._moving && this._direction === 'RIGHT') return; + this.currentActionType = 'MOVE'; + this._direction = 'RIGHT'; + this._moving = true; + this.flipbook = this.moveSettings.moveRightFlipbook; + this.flipbook.start(); + } + moveLeft() { + if (this._moving && this._direction === 'LEFT') return; + this.currentActionType = 'MOVE'; + this._direction = 'LEFT'; + this._moving = true; + this.flipbook = this.moveSettings.moveLeftFlipbook; + this.flipbook.start(); + } + jump() { + if (this._jumping) return; + this._jumping = true; + this.currentActionType = 'JUMP'; + this.flipbook = this._direction === 'RIGHT' ? this.jumpSettings.jumpRightFlipbook : this.jumpSettings.jumpLeftFlipbook; + this.flipbook.start(); + } + stop() { + if (this._jumping) return; + if (!this._jumping && this._moving) { + this._stopAllFlipbooks(); + if (this._direction === 'RIGHT') this.moveRight(); + if (this._direction === 'LEFT') this.moveLeft(); + } else { + this.currentActionType = 'STOP'; + this._stopAllFlipbooks(); + this.flipbook = this._direction === 'RIGHT' ? this.mainSettings.mainRightFlipbook : this.mainSettings.mainLeftFlipbook; + if (this.flipbook instanceof Flipbook) this.flipbook.start(); + } + } + attack() { + if (this.currentActionType === 'ATTACK') return; + this.currentActionType = 'ATTACK'; + this.flipbook = this._direction === 'RIGHT' ? this.attackSettings.attackRightFlipbook : this.attackSettings.attackLeftFlipbook; + this.flipbook.start(); + } + + /** + * The main method for rendering a Character. Return current frame of a Character. + * You can call it any time you want to rerender your scene. + * Frame will change based on Flipbook settings which you have passed as a argument. + * @returns {Image | HTMLCanvasElement} + */ + render() { + const offset = this._getOffset(); + if (this._moving && this._direction === 'RIGHT') this._changePosition(offset); + if (this._moving && this._direction === 'LEFT') this._changePosition(-offset); + + this._lastRenderTime = Date.now(); + return this.flipbook.currentSprite; + } + + /** + * You have to call destroy method if a Character will disappear to prevent memory leaks + */ + destroy() { + window.removeEventListener('keydown', this._keydownEventHandler); + window.removeEventListener('keyup', this._keyupEventHandler); + } + + /** + * This method is used to add hooks for a character. + * Available hooks - onMove, onStop, onDamage + * @param hook + * @param handler + */ + on(hook, handler) { + const isValidHook = ['onMove', 'onStop', 'onDamage'].includes(hook); + const isValidHandler = handler instanceof Function; + if (isValidHook && isValidHandler) this._hooks[hook] = handler; + } + + _validateFlipbooks(mainFlipbook, moveFlipbook, jumpFlipbook, attackFlipbook) { + const isMainFlipbookValid = mainFlipbook != null && (mainFlipbook instanceof Sprite || mainFlipbook instanceof Flipbook); + const isMoveFlipbookValid = moveFlipbook != null && moveFlipbook instanceof Flipbook; + const isJumpFlipbookValid = jumpFlipbook != null && jumpFlipbook instanceof Flipbook; + const isAttackFlipbookValid = attackFlipbook != null && attackFlipbook instanceof Flipbook; + + const invalidFlipbooks = []; + + if (!isMainFlipbookValid) invalidFlipbooks.push('mainFlipbook'); + if (!isMoveFlipbookValid) invalidFlipbooks.push('moveFlipbook'); + if (!isJumpFlipbookValid) invalidFlipbooks.push('jumpFlipbook'); + if (!isAttackFlipbookValid) invalidFlipbooks.push('attackFlipbook'); + + if (invalidFlipbooks.length) throw new Error(`${invalidFlipbooks} are required! ${ERROR_HELP_TEXT}`) + } + + _createOffscreenCanvas() { + this._offscreenCanvas = document.createElement('canvas'); + this._offscreenCanvas.width = this.mainSettings.mainFlipbook.width; + this._offscreenCanvas.height = this.mainSettings.mainFlipbook.height; + this._renderer = this._offscreenCanvas.getContext('2d'); + this._renderer.imageSmoothingEnabled = false; + } + + _initListeners() { + window.addEventListener('keydown', this._keydownEventHandler.bind(this), { passive: true }); + window.addEventListener('keyup', this._keyupEventHandler.bind(this), { passive: true }); + } + + _keydownEventHandler(event) { + if (Object.keys(this._actionHandlerHash).includes(event.code)) this._actionHandlerHash[event.code](event); + } + + _keyupEventHandler(event) { + if ([this.jumpSettings.jumpCode, this.jumpSettings.alternativeJumpCode].includes(event.code)) return; + if ([ + this.moveSettings.moveRightCode, + this.moveSettings.alternativeMoveRightCode, + this.moveSettings.moveLeftCode, + this.moveSettings.alternativeMoveLeftCode, + ].includes(event.code)) { + this._moving = false; + } + this.stop(); + } + + get width() { + return this.flipbook.currentSprite.width; + } + + get height() { + return this.flipbook.currentSprite.height; + } + + _changePosition(dx = 0, dy = 0) { + const isWithin = this._coreElement.checkBeyondPosition(this.position.x + dx, this.position.y + dy, this.width, this.height); + const hasMoveCollisions = this._coreElement.checkMoveCollisions(this); + if (isWithin && !hasMoveCollisions) { + this.position.x += dx; + this.position.y += dy; + if (this._hooks.onMove instanceof Function) this._hooks.onMove(); + } + const isDamageReceived = this._coreElement.checkDamageCollisions(this); + if (isDamageReceived) { + if (this._hooks.onDamage instanceof Function) this._hooks.onDamage(); + } + } + + _setOnChangeJumpFrame() { + const onChangeHandler = (frameNumber, frameCount) => { + const middleFrameNumber = Math.ceil(frameCount / 2); + if (frameNumber > 1 && frameNumber < middleFrameNumber) this._changePosition(0, -8); + if (frameNumber > middleFrameNumber && frameNumber < frameCount) this._changePosition(0, 8); + if (frameNumber === frameCount) { + this.actionType = this._prevActionType; + } + }; + const catchEndJumping = (frameNumber, frameCount) => { + if (frameNumber === frameCount) { + this._jumping = false; + this.stop(); + } + }; + this.jumpSettings.jumpLeftFlipbook.on('frameChange', onChangeHandler); + this.jumpSettings.jumpLeftFlipbook.on('frameChange', catchEndJumping); + this.jumpSettings.jumpRightFlipbook.on('frameChange', onChangeHandler); + this.jumpSettings.jumpRightFlipbook.on('frameChange', catchEndJumping); + } + + _getOffset() { + const timeChange = Date.now() - this._lastRenderTime; + const dt = timeChange / 1000.0; + return this.mainSettings.speed * dt; + } + + _stopAllFlipbooks() { + this._jumping = false; + this._moving = false; + this.mainSettings.mainLeftFlipbook.stop(); + this.mainSettings.mainRightFlipbook.stop(); + this.jumpSettings.jumpRightFlipbook.stop(); + this.jumpSettings.jumpLeftFlipbook.stop(); + this.moveSettings.moveRightFlipbook.stop(); + this.moveSettings.moveLeftFlipbook.stop(); + this.attackSettings.attackRightFlipbook.stop(); + this.attackSettings.attackLeftFlipbook.stop(); + } +} diff --git a/src/classes/Cursor.js b/src/utils/classes/Cursor.js similarity index 94% rename from src/classes/Cursor.js rename to src/utils/classes/Cursor.js index 795c7fc..e60a39f 100644 --- a/src/classes/Cursor.js +++ b/src/utils/classes/Cursor.js @@ -1,5 +1,5 @@ -import drawImageFromMap from '../utils/drawImageFromMap.js'; -import getTilesRectSizes from '../utils/getTilesRectSizes.js'; +import drawImageFromMap from '../drawImageFromMap.js'; +import getTilesRectSizes from '../getTilesRectSizes.js'; const updateImageColorVolume = (imageData) => { let pixels = imageData.data; diff --git a/src/utils/classes/Flipbook.js b/src/utils/classes/Flipbook.js new file mode 100644 index 0000000..ac1010f --- /dev/null +++ b/src/utils/classes/Flipbook.js @@ -0,0 +1,140 @@ +import Sprite from './Sprite.js'; + +/** + * Flipbook is used to animate multiple images (like GIFs) + */ +export default class Flipbook { + options = { + frameDuration: 100, + mirror: false, + }; + _spriteUrls = []; + _sprites = []; + _eventHash = { + frameChange: [], + }; + _currentSprite = null; + _currentSpriteIndex = 0; + _timer = null; + _availableEvents = ['frameChange']; + + /** + * @constructs The main method to create Flipbook. + * @param {string[]} sprites - array of image links + * @param {Object} [options] - meta info for Flippbok + * @param {number} [options.frameDuration=300] - duration between frames + * @param {boolean} [options.mirror=false] - if true all sprites would be mirrored + * @returns {Promise} + */ + static async create(sprites, options) { + const instance = new this(sprites, options); + await instance.init(); + return instance; + } + + constructor(sprites, options = {}) { + if (sprites == null || sprites.length < 1) throw new Error('Sprites are required!'); + if (options.frameDuration) this.options.frameDuration = options.frameDuration; + if (options.mirror) this.options.mirror = options.mirror; + this._spriteUrls = sprites; + } + + async init() { + for (const url of this._spriteUrls) this._sprites.push(new Sprite(url)); + await this.load(); + this._currentSprite = this._sprites[0]; + this._createOffscreenCanvas(); + } + + async load() { + try { + await Promise.all(this._sprites.map((sprite) => sprite.load())); + } catch (error) { + throw new TypeError('Sprites of Flipbook should be an array of image links'); + } + } + + get currentSprite() { + return this._offscreenCanvas; + } + + start() { + this.stop(); + this._updateSize(); + this._render(); + if (this._sprites.length > 1) { + this._timer = setInterval(() => { + this._currentSprite = this._sprites[this._currentSpriteIndex]; + this._updateSize(); + this._render(); + this._callEventHandlers('frameChange', [this._currentSpriteIndex + 1, this._sprites.length]); + + const nextSpriteIndex = this._currentSpriteIndex + 1; + + if (nextSpriteIndex >= this._sprites.length) this._currentSpriteIndex = 0; + else this._currentSpriteIndex = nextSpriteIndex; + }, this.options.frameDuration); + } + } + + stop() { + clearInterval(this._timer); + this._currentSpriteIndex = 0; + this._currentSprite = this._sprites[this._currentSpriteIndex]; + } + + /** + * Standard way to add event handler + * @param event + * @param handler + */ + on(event, handler) { + if (typeof event === 'string' && this._availableEvents.includes(event) && handler instanceof Function) { + this._eventHash[event].push(handler); + } + } + + get width() { + return this._offscreenCanvas.width; + } + + get height() { + return this._offscreenCanvas.height; + } + + _createOffscreenCanvas() { + this._offscreenCanvas = document.createElement('canvas'); + this._updateSize(); + this._renderer = this._offscreenCanvas.getContext('2d'); + this._renderer.imageSmoothingEnabled = false; + } + + _updateSize() { + this._offscreenCanvas.width = this._currentSprite.width; + this._offscreenCanvas.height = this._currentSprite.height; + } + + _render() { + const isMirror = this.options.mirror; + this._renderer.clearRect(0, 0, this.width, this.height); + if (isMirror) this._renderer.scale(-1, 1); + this._renderer.drawImage( + this._currentSprite.currentSprite, + 0, + 0, + this._currentSprite.width, + this._currentSprite.height, + // TODO it's hard code now. It should be passed through some options. + isMirror ? 40 : 0, + 0, + this.width * (isMirror ? -1 : 1), + this.height, + ); + } + + _callEventHandlers(event, argumentsList) { + if (this._eventHash[event].length) { + this._eventHash[event].forEach(handler => handler(...argumentsList)); + } + } +} diff --git a/src/classes/Point.js b/src/utils/classes/Point.js similarity index 100% rename from src/classes/Point.js rename to src/utils/classes/Point.js diff --git a/src/utils/classes/Sprite.js b/src/utils/classes/Sprite.js new file mode 100644 index 0000000..181d5f0 --- /dev/null +++ b/src/utils/classes/Sprite.js @@ -0,0 +1,30 @@ +export default class Sprite { + constructor(src) { + if (typeof src === 'string') this.src = src; + else throw new TypeError('Sprite source link should be a string'); + } + + async load() { + const image = new Image(); + await new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + image.src = this.src; + }); + this._image = image; + + return this; + } + + get width() { + return this._image.width; + } + + get height() { + return this._image.height; + } + + get currentSprite() { + return this._image; + } +} diff --git a/src/classes/Tile.js b/src/utils/classes/Tile.js similarity index 100% rename from src/classes/Tile.js rename to src/utils/classes/Tile.js diff --git a/src/utils/classes/index.js b/src/utils/classes/index.js new file mode 100644 index 0000000..22735c9 --- /dev/null +++ b/src/utils/classes/index.js @@ -0,0 +1,15 @@ +import Character from './Character.js'; +import Cursor from './Cursor.js'; +import Flipbook from './Flipbook.js'; +import Point from './Point.js'; +import Sprite from './Sprite.js'; +import Tile from './Tile.js'; + +export { + Cursor, + Character, + Flipbook, + Point, + Sprite, + Tile, +} diff --git "a/src/\320\241anvas/CanvasClassBuilder.js" "b/src/\320\241anvas/CanvasClassBuilder.js" new file mode 100644 index 0000000..ec618aa --- /dev/null +++ "b/src/\320\241anvas/CanvasClassBuilder.js" @@ -0,0 +1,56 @@ +import Canvas from './index.js'; + +import SelectableCanvasMixin from './mixins/selectableCanvas.js'; +import ResizeableCanvasMixin from './mixins/resizeableCanvas.js'; +import TileableCanvasMixin from './mixins/tileableCanvas.js'; +import DrawableCanvasMixin from './mixins/drawableCanvas.js'; + +const MIXINS = { + selectable: SelectableCanvasMixin, + resizable: ResizeableCanvasMixin, + tileable: TileableCanvasMixin, + drawable: DrawableCanvasMixin, +}; + +export default class CanvasClassBuilder { + klass = Canvas; + mixins = { + selectable: false, + resizable: false, + tileable: false, + drawable: false, + }; + + applySelectableMixin() { + this.mixins.selectable = true; + return this; + } + + applyResizeableMixin() { + this.mixins.resizable = true; + return this; + } + + applyTileableMixin() { + this.mixins.tileable = true; + return this; + } + + applyDrawableMixin() { + this.mixins.drawable = true; + return this; + } + + build() { + let klass = this.klass; + for (const [key, flag] of Object.entries(this.mixins)) { + if (flag) klass = MIXINS[key](klass); + } + return klass; + } + + instantiate(options) { + const klass = this.build(); + return klass.create(options); + } +} diff --git "a/src/canvases/\320\241ustomCanvas.js" "b/src/\320\241anvas/index.js" similarity index 97% rename from "src/canvases/\320\241ustomCanvas.js" rename to "src/\320\241anvas/index.js" index 8ac5431..7ccfcb3 100644 --- "a/src/canvases/\320\241ustomCanvas.js" +++ "b/src/\320\241anvas/index.js" @@ -6,7 +6,7 @@ import throttle from '../utils/throttle.js'; const customCanvas = await CustomCanvas.create({ el: document.body, size: { width: 64, height: 64 } }); customCanvas.addEventListener('render', (event) => { if (tile != null) event.ctx.drawImage(tile, 0, 0, 64, 64); }); */ -export default class CustomCanvas extends EventTarget { +export default class Canvas extends EventTarget { static _metaClassNames = []; static async create(...args) { diff --git a/src/canvases/mixins/drawable.js "b/src/\320\241anvas/mixins/drawableCanvas.js" similarity index 96% rename from src/canvases/mixins/drawable.js rename to "src/\320\241anvas/mixins/drawableCanvas.js" index b5edaab..c925f77 100644 --- a/src/canvases/mixins/drawable.js +++ "b/src/\320\241anvas/mixins/drawableCanvas.js" @@ -1,13 +1,13 @@ import buildEvent from '../../utils/buildEvent.js'; -import Cursor from '../../classes/Cursor.js'; -import Point from '../../classes/Point.js'; -import CustomCanvas from '../СustomCanvas.js'; +import Cursor from '../../utils/classes/Cursor.js'; +import Point from '../../utils/classes/Point.js'; +import CustomCanvas from '../index.js'; import TileableCanvasMixin, { - Tileable, + TileableCanvas, BACKGROUND_LAYER, ZERO_LAYER, FOREGROUND_LAYER, -} from './tileable.js'; +} from './tileableCanvas.js'; const _onMouseEnterHandler = Symbol('_onMouseEnterHandler'); const _onMouseLeaveHandler = Symbol('_onMouseLeaveHandler'); diff --git "a/src/\320\241anvas/mixins/index.js" "b/src/\320\241anvas/mixins/index.js" new file mode 100644 index 0000000..41ba4af --- /dev/null +++ "b/src/\320\241anvas/mixins/index.js" @@ -0,0 +1,11 @@ +import SelectableCanvasMixin from './selectableCanvas.js'; +import ResizeableCanvasMixin from './resizeableCanvas.js'; +import TileableCanvasMixin from './tileableCanvas.js'; +import DrawableCanvasMixin from './drawableCanvas.js'; + +export { + SelectableCanvasMixin, + ResizeableCanvasMixin, + TileableCanvasMixin, + DrawableCanvasMixin, +}; diff --git a/src/canvases/mixins/resizeable.js "b/src/\320\241anvas/mixins/resizeableCanvas.js" similarity index 94% rename from src/canvases/mixins/resizeable.js rename to "src/\320\241anvas/mixins/resizeableCanvas.js" index 16edb1f..abcae3b 100644 --- a/src/canvases/mixins/resizeable.js +++ "b/src/\320\241anvas/mixins/resizeableCanvas.js" @@ -1,4 +1,4 @@ -import CustomCanvas from '../СustomCanvas.js'; +import CustomCanvas from '../index.js'; const INCREASE_SIZE_MULTIPLIER = 2; const DECREASE_SIZE_MULTIPLIER = 1 / 2; @@ -64,4 +64,4 @@ const ResizeableCanvasMixin = (BaseClass = CustomCanvas) => { export default ResizeableCanvasMixin; -export const Resizeable = ResizeableCanvasMixin(); +export const ResizeableCanvas = ResizeableCanvasMixin(); diff --git a/src/canvases/mixins/selectable.js "b/src/\320\241anvas/mixins/selectableCanvas.js" similarity index 95% rename from src/canvases/mixins/selectable.js rename to "src/\320\241anvas/mixins/selectableCanvas.js" index 86a1e1f..69ff372 100644 --- a/src/canvases/mixins/selectable.js +++ "b/src/\320\241anvas/mixins/selectableCanvas.js" @@ -1,5 +1,5 @@ import buildEvent from '../../utils/buildEvent.js'; -import CustomCanvas from '../СustomCanvas.js'; +import CustomCanvas from '../index.js'; const _onMouseDownHandler = Symbol('_onMouseDownHandler'); const _onMouseUpHandler = Symbol('_onMouseUpHandler'); @@ -76,4 +76,4 @@ const SelectableCanvasMixin = (BaseClass = CustomCanvas) => { export default SelectableCanvasMixin; -export const Selectable = SelectableCanvasMixin(); +export const SelectableCanvas = SelectableCanvasMixin(); diff --git a/src/canvases/mixins/tileable.js "b/src/\320\241anvas/mixins/tileableCanvas.js" similarity index 97% rename from src/canvases/mixins/tileable.js rename to "src/\320\241anvas/mixins/tileableCanvas.js" index 4e13050..02ea708 100644 --- a/src/canvases/mixins/tileable.js +++ "b/src/\320\241anvas/mixins/tileableCanvas.js" @@ -1,6 +1,6 @@ import buildEvent from '../../utils/buildEvent.js'; -import CustomCanvas from '../СustomCanvas.js'; -import Point from '../../classes/Point.js'; +import CustomCanvas from '../index.js'; +import Point from '../../utils/classes/Point.js'; const _onMouseMoveHandler = Symbol('_onMouseMoveHandler'); const _onMouseOutHandler = Symbol('_onMouseOutHandler'); @@ -212,4 +212,4 @@ const TileableCanvasMixin = (BaseClass = CustomCanvas) => { export default TileableCanvasMixin; -export const Tileable = TileableCanvasMixin(); +export const TileableCanvas = TileableCanvasMixin(); diff --git a/src/canvases/test.js "b/src/\320\241anvas/test.js" similarity index 78% rename from src/canvases/test.js rename to "src/\320\241anvas/test.js" index 944a706..fb1df16 100644 --- a/src/canvases/test.js +++ "b/src/\320\241anvas/test.js" @@ -1,5 +1,5 @@ -import CustomCanvas from './src/canvases/СustomCanvas.js'; -import ResizeableCanvasMixin from './src/canvases/mixins/resizeable.js'; +import CustomCanvas from './src/Сanvas/index.js'; +import ResizeableCanvasMixin from './src/Сanvas/mixins/resizeableCanvas.js'; class TestCanvas extends ResizeableCanvasMixin(CustomCanvas) {