diff --git a/index.js b/index.js index 1286ba9..0e0a732 100644 --- a/index.js +++ b/index.js @@ -1,111 +1,78 @@ -import { CanvasResizer } from "./modules/canvas-resizer.js"; -import { getLevel } from "./modules/utils.js"; +import { GAME_HEIGHT, GAME_WIDTH } from "./modules/constants.js"; +import { + Camera, + CanvasResizer, + FpsCounter, + GameObject, + MapManagement, +} from "./modules/game-objects/index.js"; -const TILE_SIZE = 16; -const canvasResizer = new CanvasResizer({ - canvas: document.getElementById("game"), - width: 320 / 2, - height: 240 / 2, - percentage: 0.9, -}); +const maps = [ + { + name: "overworld", + imageId: "overworld", + elementId: "level1", + selected: true, + }, + { name: "ocean", imageId: "overworld", elementId: "level2" }, +]; -const cols = canvasResizer.width / TILE_SIZE; -const rows = canvasResizer.height / TILE_SIZE; -const ctx = canvasResizer.canvas.getContext("2d"); -let debug = false; -let cameraX = 0; -let cameraY = 0; +const clicableObjects = ["debug", "level1", "level2"]; -async function drawLevel({ levelName, imageName }) { - const levelImage = document.getElementById(imageName); - const level = await getLevel(levelName); - const layer = level.layers[0]; - const { data, width, height } = layer; // Obtenemos la capa del tilemap, con su numero de columnas y filas +class Game extends GameObject { + constructor({ canvas }) { + super(); - const endCol = cameraX + cols; - const endRow = cameraY + rows; + const canvasResizer = new CanvasResizer({ + canvas: canvas, + width: GAME_WIDTH, + height: GAME_HEIGHT, + percentage: 0.9, + }); + const camera = new Camera({ + gameObjects: [new MapManagement({ maps: maps })], + }); + const fpsCounter = new FpsCounter({ debug: false }); + this.gameObjects = [canvasResizer, camera, fpsCounter]; - for (let row = cameraY; row <= endRow; row++) { - for (let col = cameraX; col <= endCol; col++) { - if (row < 0 || col < 0 || row >= height || col >= width) continue; // Omitimos tiles fuera del rango del tilemap + this.canvas = canvas; + this.ctx = this.canvas.getContext("2d"); + this.lastTime = 0; + } - if (debug) { - ctx.strokeRect( - (col - cameraX) * TILE_SIZE, - (row - cameraY) * TILE_SIZE, - TILE_SIZE, - TILE_SIZE - ); - } - const tile = data[row * width + col]; - ctx.drawImage( - levelImage, - ((tile - 1) * TILE_SIZE) % levelImage.width, - Math.floor(((tile - 1) * TILE_SIZE) / levelImage.width) * TILE_SIZE, - TILE_SIZE, - TILE_SIZE, - (col - cameraX) * TILE_SIZE, - (row - cameraY) * TILE_SIZE, - TILE_SIZE, - TILE_SIZE - ); - } + async load() { + await super.load(); + clicableObjects.forEach((elementId) => { + document.getElementById(elementId).addEventListener("click", () => { + this.onMouseClick(elementId); + }); + }); + document.addEventListener("keydown", (event) => { + this.onKeyPressed(event.key); + }); + document.addEventListener("keyup", (event) => { + this.onKeyReleased(event.key); + }); + } + + clear(ctx) { + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + loop(time) { + const delta = (time - this.lastTime) / 1000; + this.lastTime = time; + this.update(delta); + this.clear(this.ctx); + this.render(this.ctx); + requestAnimationFrame(this.loop.bind(this)); } } -function moveCamera(dx, dy) { - cameraX = Math.min( - Math.max(cameraX + dx, 0), - Math.floor((320 - canvasResizer.width) / TILE_SIZE) - ); - cameraY = Math.min( - Math.max(cameraY + dy, 0), - Math.floor((240 - canvasResizer.height) / TILE_SIZE) - ); -} - async function run() { - await canvasResizer.load(); - - let selectedLevel = { levelName: "overworld", imageName: "overworld" }; - - const debugButton = document.getElementById("debug"); - debugButton.addEventListener("click", async () => { - debug = !debug; - await drawLevel(selectedLevel); - }); - - const level1Button = document.getElementById("level1"); - level1Button.addEventListener("click", async () => { - selectedLevel = { levelName: "overworld", imageName: "overworld" }; - await drawLevel(selectedLevel); - }); - - const level2Button = document.getElementById("level2"); - level2Button.addEventListener("click", async () => { - selectedLevel = { levelName: "ocean", imageName: "overworld" }; - await drawLevel(selectedLevel); - }); - - window.addEventListener("keydown", (event) => { - switch (event.key) { - case "ArrowUp": - moveCamera(0, -1); - break; - case "ArrowDown": - moveCamera(0, 1); - break; - case "ArrowLeft": - moveCamera(-1, 0); - break; - case "ArrowRight": - moveCamera(1, 0); - break; - } - drawLevel(selectedLevel); - }); - - await drawLevel(selectedLevel); + const game = new Game({ canvas: document.getElementById("game") }); + await game.load(); + game.loop(0); } run(); diff --git a/modules/constants.js b/modules/constants.js new file mode 100644 index 0000000..950ec2c --- /dev/null +++ b/modules/constants.js @@ -0,0 +1,5 @@ +export const TILE_SIZE = 16; +export const GAME_WIDTH = 160; +export const GAME_HEIGHT = 120; +export const COLS = GAME_WIDTH / TILE_SIZE; +export const ROWS = GAME_HEIGHT / TILE_SIZE; diff --git a/modules/game-objects/camera.js b/modules/game-objects/camera.js new file mode 100644 index 0000000..5e24e70 --- /dev/null +++ b/modules/game-objects/camera.js @@ -0,0 +1,68 @@ +import { TILE_SIZE } from "../constants.js"; +import { GameObject } from "./game-object.js"; + +export class Camera extends GameObject { + constructor({ + gameObjects = [], + x = 0, + y = 0, + width = 160, + height = 120, + speed = 1, + }) { + super({ x, y }); + this.gameObjects = gameObjects; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.speed = speed; + this.keys = [ + { key: "ArrowUp", pressed: false, value: [0, -1] }, + { key: "ArrowDown", pressed: false, value: [0, 1] }, + { key: "ArrowLeft", pressed: false, value: [-1, 0] }, + { key: "ArrowRight", pressed: false, value: [1, 0] }, + ]; + this.availableKeys = this.keys.reduce( + (acc, item) => ({ ...acc, [item.key]: item }), + {} + ); + } + + update(delta) { + this.keys.forEach((item) => { + if (item.pressed) { + this.moveCamera(...item.value, delta); + } + }); + } + + onKeyPressed(key) { + if (!this.availableKeys[key]) return; + this.availableKeys[key].pressed = true; + } + + onKeyReleased(key) { + if (!this.availableKeys[key]) return; + this.availableKeys[key].pressed = false; + } + + render(ctx) { + this.gameObjects.forEach((item) => + item.render(ctx, this.x, this.y, this.width, this.height) + ); + } + + moveCamera(dx, dy, delta) { + const [item] = this.gameObjects; + const { height, width } = item.selected ?? item; + this.x = Math.min( + Math.max(this.x + dx * Math.floor(delta * this.speed * 100), 0), + width * TILE_SIZE - this.width + ); + this.y = Math.min( + Math.max(this.y + dy * Math.floor(delta * this.speed * 100), 0), + height * TILE_SIZE - this.height + ); + } +} diff --git a/modules/canvas-resizer.js b/modules/game-objects/canvas-resizer.js similarity index 88% rename from modules/canvas-resizer.js rename to modules/game-objects/canvas-resizer.js index 63658ab..fb20302 100644 --- a/modules/canvas-resizer.js +++ b/modules/game-objects/canvas-resizer.js @@ -1,4 +1,6 @@ -export class CanvasResizer { +import { GameObject } from "./game-object.js"; + +export class CanvasResizer extends GameObject { /** * Creates a new instance of `CanvasResizer` class. * @param {Object} config - The configuration options for the class. @@ -8,6 +10,7 @@ export class CanvasResizer { * @param {number} config.percentage - The percentage of the screen size to use for the canvas. */ constructor({ canvas, width, height, percentage }) { + super(); this.canvas = canvas; this.width = width; this.height = height; @@ -18,12 +21,16 @@ export class CanvasResizer { } load() { + if (this.loaded) { + return; + } return new Promise((resolve) => { ["load", "resize"].map((item) => window.addEventListener(item, () => { this._resize(); if (item === "load") { resolve(); + this.loaded = true; } }) ); diff --git a/modules/game-objects/fps-counter.js b/modules/game-objects/fps-counter.js new file mode 100644 index 0000000..3d4b266 --- /dev/null +++ b/modules/game-objects/fps-counter.js @@ -0,0 +1,21 @@ +import { GameObject } from "./game-object.js"; + +export class FpsCounter extends GameObject { + constructor({ debug = false }) { + super({ debug }); + this.fps = 0; + } + + update(delta) { + this.fps = Math.floor(1 / delta); + } + + render(ctx) { + if (!this.debug) { + return; + } + ctx.fillStyle = "Red"; + ctx.font = "normal 12pt Arial"; + ctx.fillText(this.fps + " fps", 5, 15); + } +} diff --git a/modules/game-objects/game-object.js b/modules/game-objects/game-object.js new file mode 100644 index 0000000..932f923 --- /dev/null +++ b/modules/game-objects/game-object.js @@ -0,0 +1,61 @@ +export class GameObject { + constructor(options = {}) { + const { + x = 0, + y = 0, + width = 0, + height = 0, + debug = false, + gameObjects = [], + } = options; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.gameObjects = gameObjects; + this.debug = debug; + } + + async load() { + await Promise.all( + this.gameObjects.map((item) => { + item.load(); + }) + ); + } + + update(delta) { + this.gameObjects.forEach((item) => { + item.update(delta); + }); + } + + render(ctx, ...args) { + this.gameObjects.forEach((item) => { + item.render(ctx, ...args); + }); + } + + onKeyPressed(key) { + this.gameObjects.forEach((item) => { + item.onKeyPressed(key); + }); + } + + onKeyReleased(key) { + this.gameObjects.forEach((item) => { + item.onKeyReleased(key); + }); + } + + onMouseClick(elementId) { + if (elementId === "debug") { + this.debug = !this.debug; + } + this.gameObjects.forEach((item) => { + item.onMouseClick(elementId); + }); + } + + onCollide() {} +} diff --git a/modules/game-objects/index.js b/modules/game-objects/index.js new file mode 100644 index 0000000..fbeecb0 --- /dev/null +++ b/modules/game-objects/index.js @@ -0,0 +1,6 @@ +export { Camera } from "./camera.js"; +export { CanvasResizer } from "./canvas-resizer.js"; +export { FpsCounter } from "./fps-counter.js"; +export { GameObject } from "./game-object.js"; +export { MapManagement } from "./map-management.js"; +export { Map } from "./map.js"; diff --git a/modules/game-objects/map-management.js b/modules/game-objects/map-management.js new file mode 100644 index 0000000..fd4c952 --- /dev/null +++ b/modules/game-objects/map-management.js @@ -0,0 +1,32 @@ +import { GameObject } from "./game-object.js"; +import { Map } from "./map.js"; + +export class MapManagement extends GameObject { + constructor({ maps = [] }) { + super(); + this.gameObjects = maps.map((item) => new Map(item)); + this.elementsId = this.gameObjects.map((item) => item.elementId); + } + + get selected() { + return this.gameObjects.find((item) => item.selected); + } + + set selected(name) { + this.gameObjects.forEach((item) => { + item.selected = item.name === name; + }); + } + + onMouseClick(elementId) { + super.onMouseClick(elementId); + if ( + !this.elementsId.includes(elementId) || + this.selected.elementId === elementId + ) { + return; + } + const map = this.gameObjects.find((item) => item.elementId === elementId); + this.selected = map.name; + } +} diff --git a/modules/game-objects/map.js b/modules/game-objects/map.js new file mode 100644 index 0000000..6bf6eff --- /dev/null +++ b/modules/game-objects/map.js @@ -0,0 +1,93 @@ +import { TILE_SIZE } from "../constants.js"; +import { createCanvas } from "../utils.js"; +import { GameObject } from "./game-object.js"; + +export class Map extends GameObject { + constructor({ name, imageId, elementId, selected = false, debug = false }) { + super({ debug }); + this.name = name; + this.imageId = imageId; + this.image = document.getElementById(imageId); + this.imageWidth = this.image.width; + this.imageHeight = this.image.height; + this.selected = selected; + this.elementId = elementId; + this.debug = debug; + } + + async load() { + const levelConfig = await fetch("/resources/" + this.name + ".json"); + this.levelConfig = await levelConfig.json(); + const layer = this.levelConfig.layers[0]; + const { data, height, width } = layer; + this.width = width; + this.height = height; + this.data = data; + } + + get level() { + if (this._level) { + return this._level; + } + const { ctx, canvas } = createCanvas( + this.width * TILE_SIZE, + this.height * TILE_SIZE + ); + + for (let row = 0; row < this.height; row++) { + for (let col = 0; col < this.width; col++) { + if (row < 0 || col < 0 || row >= this.height || col >= this.width) + continue; + + if (this.debug) { + ctx.strokeRect( + col * TILE_SIZE, + row * TILE_SIZE, + TILE_SIZE, + TILE_SIZE + ); + } + + const tile = this.data[row * this.width + col] - 1; + ctx.drawImage( + this.image, + (tile * TILE_SIZE) % this.imageWidth, + Math.floor((tile * TILE_SIZE) / this.imageWidth) * TILE_SIZE, + TILE_SIZE, + TILE_SIZE, + col * TILE_SIZE, + row * TILE_SIZE, + TILE_SIZE, + TILE_SIZE + ); + } + } + + return (this._level = canvas); + } + + onMouseClick(elementId) { + if (elementId === "debug") { + this.debug = !this.debug; + this._level = null; + } + } + + render(ctx, sourceX, sourceY, width, height) { + if (!this.levelConfig || !this.selected) { + return; + } + + ctx.drawImage( + this.level, + sourceX, + sourceY, + width, + height, + 0, + 0, + width, + height + ); + } +} diff --git a/modules/utils.js b/modules/utils.js index c32ff70..077b7d8 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -1,4 +1,7 @@ -export async function getLevel(name) { - const level = await fetch("/resources/" + name + ".json"); - return await level.json(); -} +export function createCanvas(width, height) { + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + return { ctx, canvas }; +} diff --git a/package.json b/package.json index e28be77..3199538 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { - "start": "serve ." + "start": "serve . -n" }, "repository": "git@git.jusemon.com:jusemon/evolver.git", "author": "Jusemon ", @@ -12,4 +12,4 @@ "devDependencies": { "serve": "^14.2.3" } -} +} \ No newline at end of file