aboutsummaryrefslogtreecommitdiff
path: root/js/game.js
diff options
context:
space:
mode:
authorJuan J. Martinez <jjm@usebox.net>2025-02-09 17:25:59 +0000
committerJuan J. Martinez <jjm@usebox.net>2025-02-09 17:25:59 +0000
commit6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc (patch)
tree44ebe6f5cfb1eb9214c5877342c7914de07f215a /js/game.js
downloadjs-twin-shooter-6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc.tar.gz
js-twin-shooter-6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc.zip
Initial import
Diffstat (limited to 'js/game.js')
-rw-r--r--js/game.js293
1 files changed, 293 insertions, 0 deletions
diff --git a/js/game.js b/js/game.js
new file mode 100644
index 0000000..6527584
--- /dev/null
+++ b/js/game.js
@@ -0,0 +1,293 @@
+const floor = Math.floor;
+const max = Math.max;
+const min = Math.min;
+
+class Game {
+ constructor(canvas, width, height) {
+ this.canvas = canvas;
+ this.canvas.width = width;
+ this.canvas.height = height;
+ this.ctx = this.canvas.getContext("2d");
+
+ // override this with your data to be loaded:
+ //
+ // { id: "file.png" }
+ //
+ // Supports .png, .ogg and .json files.
+ //
+ // and also override dataSize with:
+ //
+ // this.dataSize = Object.keys(this.data).length
+ //
+ this.data = {};
+ this.dataSize = 0;
+
+ // override to change keys
+ this.controls = {
+ 80: "pause",
+ 32: "start",
+ 37: "left",
+ 38: "up",
+ 39: "right",
+ 40: "down",
+ 87: "f_up",
+ 83: "f_down",
+ 65: "f_left",
+ 68: "f_right"
+ };
+
+ // will store the resouces indexed by id
+ this.res = {};
+ this.resSize = 0;
+
+ // override if you want to change the limit
+ // defaults to 8 "channels"
+ this.playLimit = 8;
+ this.playCount = 0;
+
+ // test to see if a key is down or not
+ this.keys = {
+ pause: false,
+ start: false,
+ left: false,
+ up: false,
+ right: false,
+ down: false,
+ f_up: false,
+ f_down: false,
+ f_left: false,
+ f_right: false
+ };
+
+ this.gamepads = {};
+
+ this.minFps = 60;
+ this.then = -1 / this.minFps;
+
+ this.loadingError = undefined;
+ }
+
+ start() {
+ this.canvas.style.background = "rgb(21, 21, 21)";
+ this.resize();
+
+ window.onresize = (ev) => {
+ this.resize();
+ };
+
+ window.addEventListener("gamepadconnected", (e) => {
+ console.log(
+ "Gamepad connected at index %d: %s. %d buttons, %d axes.",
+ e.gamepad.index,
+ e.gamepad.id,
+ e.gamepad.buttons.length,
+ e.gamepad.axes.length,
+ );
+ this.gamepads[e.gamepad.index] = e.gamepad;
+ });
+ window.addEventListener("gamepaddisconnected", (e) => {
+ console.log(
+ "Gamepad disconnected from index %d: %s",
+ e.gamepad.index,
+ e.gamepad.id,
+ );
+ delete this.gamepads[e.gamepad.index];
+ });
+
+ this.loader();
+ }
+
+ // override this
+ init() {}
+ // override this
+ update(dt) {}
+ // override this
+ draw() {}
+
+ _update(dt) {}
+ _draw() {}
+
+ resize() {
+ const scale = floor(window.innerHeight / this.canvas.height);
+ this.canvas.style["imageRendering"] = "pixelated";
+ this.canvas.style["transformOrigin"] = "top";
+ this.canvas.style["transform"] = `scale(${scale})`;
+ }
+
+ loop(now) {
+
+ Object.keys(this.gamepads).forEach((g) => {
+ const gp = this.gamepads[g];
+
+ this.keys["f_up"] = gp.buttons[2].pressed;
+ this.keys["f_down"] = gp.buttons[0].pressed;
+ this.keys["f_left"] = gp.buttons[3].pressed;
+ this.keys["f_right"] = gp.buttons[1].pressed;
+
+ if (gp.buttons[14].pressed) {
+ this.keys["left"] = true;
+ this.keys["right"] = false;
+ } else {
+ if (gp.buttons[15].pressed) {
+ this.keys["left"] = false;
+ this.keys["right"] = true;
+ } else {
+ this.keys["left"] = false;
+ this.keys["right"] = false;
+ }
+ }
+
+ if (gp.buttons[12].pressed) {
+ this.keys["up"] = true;
+ this.keys["down"] = false;
+ } else {
+ if (gp.buttons[13].pressed) {
+ this.keys["up"] = false;
+ this.keys["down"] = true;
+ } else {
+ this.keys["up"] = false;
+ this.keys["down"] = false;
+ }
+ }
+ });
+
+ let dt = min(1000 / this.minFps, now - this.then);
+ this._update(dt/1000);
+
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.save();
+ this._draw();
+ this.ctx.restore();
+
+ this.then = now;
+ requestAnimationFrame((now) => {
+ this.loop(now)
+ });
+ }
+
+ loader() {
+ this._draw = this.drawLoading;
+ this.loop(0);
+
+ const onError = (ev) => {
+ console.log(ev);
+ this.loadingError = true;
+ };
+
+ for (const id in this.data) {
+ if (this.data[id].indexOf(".png") != -1) {
+ this.res[id] = new Image();
+ this.res[id].src = this.data[id];
+ this.res[id].onerror = onError
+ this.res[id].onload = (ev) => {
+ this.res[id].onload = null;
+ ev.currentTarget.removeEventListener("error", onError);
+ this.resSize += 1;
+ };
+ continue;
+ }
+ if (this.data[id].indexOf(".ogg") != -1) {
+ this.res[id] = new Audio();
+ this.res[id].src = this.data[id];
+ this.res[id].autoplay = false;
+ this.res[id].addEventListener("error", onError);
+ this.res[id].oncanplaythrough = (ev) => {
+ this.res[id].oncanplaythrough = null;
+ ev.currentTarget.removeEventListener("error", onError);
+ this.resSize += 1;
+ };
+ continue;
+ }
+ if (this.data[id].indexOf(".json") != -1) {
+ let xhr = new window.XMLHttpRequest();
+ xhr.open("GET", this.data[id]);
+ xhr.responseType = "json";
+ xhr.addEventListener("error", onError);
+ xhr.onload = (ev) => {
+ ev.currentTarget.removeEventListener("error", onError);
+ xhr.onload = null;
+ if (xhr.status == 200) {
+ this.res[id] = xhr.response;
+ this.resSize += 1;
+ } else {
+ console.log(ev);
+ this.loadingError = true;
+ }
+ };
+ xhr.send();
+ continue;
+ }
+ }
+ }
+
+ // won't use drawStart/drawEnd because may render regular fonts
+ drawLoading() {
+ // height of the loading bar
+ const h = 6;
+
+ if (this.loadingError == true) {
+ this.ctx.fillStyle = "rgb(255, 128, 128)";
+ this.ctx.font = "8px monospace";
+ this.ctx.fillText(
+ "ERROR Loading Resources",
+ floor(this.ctx.canvas.width * 0.1),
+ floor(this.ctx.canvas.height / 2 - h - h / 2)
+ );
+ }
+
+ this.ctx.fillStyle = "rgb(128, 128, 128)";
+ this.ctx.fillRect(floor(this.ctx.canvas.width * 0.1), floor(this.ctx.canvas.height / 2) - h, this.ctx.canvas.width - floor(this.ctx.canvas.width * 0.2), h);
+ this.ctx.fillStyle = "rgb(255, 255, 255)";
+ this.ctx.fillRect(floor(this.ctx.canvas.width * 0.1), floor(this.ctx.canvas.height / 2) - h, (
+ floor(this.ctx.canvas.resSize * (this.ctx.canvas.width - floor(this.ctx.canvas.width * 0.2)) / this.ctx.canvas.dataSize)
+ ), h);
+
+ // we don't do this on update because we want
+ // the progress bar to finish drawing
+ if (this.resSize == this.dataSize) {
+ console.log("Loader done");
+
+ document.addEventListener("keydown", (ev) => {
+ this.keyDown(ev)
+ }, false);
+ document.addEventListener("keyup", (ev) => {
+ this.keyUp(ev)
+ }, false);
+
+ this.init();
+ this._update = this.update;
+ this._draw = this.draw;
+ }
+ }
+
+ playSnd(playable, loop, clone) {
+ if (this.playCount < this.playLimit) {
+ this.playCount++;
+
+ if (clone || false) {
+ playable = playable.cloneNode(true)
+ }
+ playable.onended = (ev) => {
+ playable.onended = null;
+ this.playCount--;
+ };
+ playable.loop = loop || false;
+ playable.play();
+ }
+ }
+
+ keyDown(ev) {
+ let key = this.controls[ev.keyCode];
+ if (key != undefined) {
+ this.keys[key] = true;
+ ev.preventDefault();
+ }
+ }
+
+ keyUp(ev) {
+ let key = this.controls[ev.keyCode];
+ if (key != undefined) {
+ this.keys[key] = false;
+ }
+ }
+}