Reviewed-on: #9
This commit is contained in:
commit
1cac9b90bc
11 changed files with 368 additions and 105 deletions
163
index.js
163
index.js
|
@ -1,111 +1,78 @@
|
||||||
import { CanvasResizer } from "./modules/canvas-resizer.js";
|
import { GAME_HEIGHT, GAME_WIDTH } from "./modules/constants.js";
|
||||||
import { getLevel } from "./modules/utils.js";
|
import {
|
||||||
|
Camera,
|
||||||
|
CanvasResizer,
|
||||||
|
FpsCounter,
|
||||||
|
GameObject,
|
||||||
|
MapManagement,
|
||||||
|
} from "./modules/game-objects/index.js";
|
||||||
|
|
||||||
|
const maps = [
|
||||||
|
{
|
||||||
|
name: "overworld",
|
||||||
|
imageId: "overworld",
|
||||||
|
elementId: "level1",
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
{ name: "ocean", imageId: "overworld", elementId: "level2" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const clicableObjects = ["debug", "level1", "level2"];
|
||||||
|
|
||||||
|
class Game extends GameObject {
|
||||||
|
constructor({ canvas }) {
|
||||||
|
super();
|
||||||
|
|
||||||
const TILE_SIZE = 16;
|
|
||||||
const canvasResizer = new CanvasResizer({
|
const canvasResizer = new CanvasResizer({
|
||||||
canvas: document.getElementById("game"),
|
canvas: canvas,
|
||||||
width: 320 / 2,
|
width: GAME_WIDTH,
|
||||||
height: 240 / 2,
|
height: GAME_HEIGHT,
|
||||||
percentage: 0.9,
|
percentage: 0.9,
|
||||||
});
|
});
|
||||||
|
const camera = new Camera({
|
||||||
|
gameObjects: [new MapManagement({ maps: maps })],
|
||||||
|
});
|
||||||
|
const fpsCounter = new FpsCounter({ debug: false });
|
||||||
|
this.gameObjects = [canvasResizer, camera, fpsCounter];
|
||||||
|
|
||||||
const cols = canvasResizer.width / TILE_SIZE;
|
this.canvas = canvas;
|
||||||
const rows = canvasResizer.height / TILE_SIZE;
|
this.ctx = this.canvas.getContext("2d");
|
||||||
const ctx = canvasResizer.canvas.getContext("2d");
|
this.lastTime = 0;
|
||||||
let debug = false;
|
|
||||||
let cameraX = 0;
|
|
||||||
let cameraY = 0;
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
const endCol = cameraX + cols;
|
|
||||||
const endRow = cameraY + rows;
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCamera(dx, dy) {
|
async load() {
|
||||||
cameraX = Math.min(
|
await super.load();
|
||||||
Math.max(cameraX + dx, 0),
|
clicableObjects.forEach((elementId) => {
|
||||||
Math.floor((320 - canvasResizer.width) / TILE_SIZE)
|
document.getElementById(elementId).addEventListener("click", () => {
|
||||||
);
|
this.onMouseClick(elementId);
|
||||||
cameraY = Math.min(
|
});
|
||||||
Math.max(cameraY + dy, 0),
|
});
|
||||||
Math.floor((240 - canvasResizer.height) / TILE_SIZE)
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
await canvasResizer.load();
|
const game = new Game({ canvas: document.getElementById("game") });
|
||||||
|
await game.load();
|
||||||
let selectedLevel = { levelName: "overworld", imageName: "overworld" };
|
game.loop(0);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
5
modules/constants.js
Normal file
5
modules/constants.js
Normal file
|
@ -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;
|
68
modules/game-objects/camera.js
Normal file
68
modules/game-objects/camera.js
Normal file
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
* Creates a new instance of `CanvasResizer` class.
|
||||||
* @param {Object} config - The configuration options for the 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.
|
* @param {number} config.percentage - The percentage of the screen size to use for the canvas.
|
||||||
*/
|
*/
|
||||||
constructor({ canvas, width, height, percentage }) {
|
constructor({ canvas, width, height, percentage }) {
|
||||||
|
super();
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
@ -18,12 +21,16 @@ export class CanvasResizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
|
if (this.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
["load", "resize"].map((item) =>
|
["load", "resize"].map((item) =>
|
||||||
window.addEventListener(item, () => {
|
window.addEventListener(item, () => {
|
||||||
this._resize();
|
this._resize();
|
||||||
if (item === "load") {
|
if (item === "load") {
|
||||||
resolve();
|
resolve();
|
||||||
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
21
modules/game-objects/fps-counter.js
Normal file
21
modules/game-objects/fps-counter.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
61
modules/game-objects/game-object.js
Normal file
61
modules/game-objects/game-object.js
Normal file
|
@ -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() {}
|
||||||
|
}
|
6
modules/game-objects/index.js
Normal file
6
modules/game-objects/index.js
Normal file
|
@ -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";
|
32
modules/game-objects/map-management.js
Normal file
32
modules/game-objects/map-management.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
93
modules/game-objects/map.js
Normal file
93
modules/game-objects/map.js
Normal file
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
export async function getLevel(name) {
|
export function createCanvas(width, height) {
|
||||||
const level = await fetch("/resources/" + name + ".json");
|
const canvas = document.createElement("canvas");
|
||||||
return await level.json();
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
return { ctx, canvas };
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "serve ."
|
"start": "serve . -n"
|
||||||
},
|
},
|
||||||
"repository": "git@git.jusemon.com:jusemon/evolver.git",
|
"repository": "git@git.jusemon.com:jusemon/evolver.git",
|
||||||
"author": "Jusemon <juansmm@outlook.com>",
|
"author": "Jusemon <juansmm@outlook.com>",
|
||||||
|
|
Loading…
Reference in a new issue