diff --git a/app.js b/app.js
new file mode 100644
index 0000000..e4eb348
--- /dev/null
+++ b/app.js
@@ -0,0 +1,145 @@
+const express = require('express'); // Import Express framework
+const http = require('http'); // Import HTTP module
+const socketIo = require('socket.io'); // Import Socket.IO for WebSocket functionality
+const fs = require('fs'); // Import file system module
+const path = require('path'); // Import path module for file path handling
+
+// Initialize the Express application
+const app = express();
+
+// Set the view engine to EJS for rendering dynamic content
+app.set('view engine', 'ejs');
+
+
+// Serve static files from the 'public' directory
+app.use(express.static(path.join(__dirname, 'public')));
+
+// Create HTTP server
+const httpServer = http.createServer(app);
+
+// Set keep-alive timeout for HTTP server
+httpServer.keepAliveTimeout = 120000; // 120 seconds
+
+// Initialize Socket.IO with options for WebSocket
+const httpIo = socketIo(httpServer, {
+ transports: ['websocket'], // Only use WebSocket transport
+ allowUpgrades: false, // Disable upgrades
+ pingInterval: 5000, // Ping every 5 seconds
+ pingTimeout: 10000, // Timeout for ping response after 10 seconds
+ cors: {
+ origin: '*', // Allow all origins for CORS
+ }
+});
+
+// Player management
+const players = {}; // Object to store connected players
+let connectionCount = 0; // Count of active connections
+
+// Handle new socket connections
+httpIo.on('connection', handleConnection);
+
+// Serve the main EJS page for Game Dev Central Hub
+app.get('/', (req, res) => {
+ // Load metadata from a JSON file (if needed) or set dynamically for game dev context
+ const metaTags = {
+ title: 'Game Dev Central Hub',
+ description: 'Create your own pixel-based side scroller games and share them with others!',
+ keywords: 'game dev, pixel art, side scroller, multiplayer, socket.io'
+ };
+
+ // Render the main EJS page
+ res.render('index', { metaTags });
+});
+
+// Start HTTP server listening on port 80
+httpServer.listen(80, () => console.log('HTTP Server listening on port 80'));
+
+/**
+ * Handle new socket connections
+ * @param {Socket} socket - The connected socket instance
+ */
+function handleConnection(socket) {
+ console.log(`Player ${socket.id} connected`); // Log new connection
+
+ // Initialize new player and add to the players object
+ const currentPlayer = initializePlayer(socket);
+ players[socket.id] = currentPlayer;
+
+ // Emit player data to the newly connected player
+ emitPlayerData(socket);
+
+ // Set up event listeners for player actions
+ socket.on('playerMovement', handlePlayerMovement);
+ socket.on('playerChat', handlePlayerChat);
+ socket.on('disconnect', handlePlayerDisconnect);
+}
+
+/**
+ * Initialize a new player object
+ * @param {Socket} socket - The socket instance for the connected player
+ * @returns {Object} - The initialized player object
+ */
+function initializePlayer(socket) {
+ return {
+ playerId: socket.id, // Unique ID for the player
+ connectionNum: ++connectionCount, // Increment connection count
+ gameData: { pos: [0, 0] }, // Initial player position
+ chatMessage: '', // Store last chat message
+ };
+}
+
+/**
+ * Emit current player data to the client
+ * @param {Socket} socket - The socket instance for the connected player
+ */
+function emitPlayerData(socket) {
+ socket.emit('currentPlayer', players[socket.id]); // Send current player data
+
+ // Send existing players data
+ const existingPlayers = Object.values(players);
+ socket.emit('existingPlayers', { existingPlayers });
+
+ // Notify other clients of the new player
+ socket.broadcast.emit('newPlayer', players[socket.id]);
+}
+
+/**
+ * Handle player movement events
+ * @param {Object} data - The movement data from the client
+ */
+function handlePlayerMovement(data) {
+ // Update player's position based on the received data
+ players[this.id].gameData.pos = data.pos;
+
+ // Broadcast the new position to other players
+ this.broadcast.emit('playerMoved', { player: players[this.id] });
+}
+
+/**
+ * Handle player chat events
+ * @param {string} message - The chat message sent by the player
+ */
+function handlePlayerChat(message) {
+ // Update player's last chat message
+ players[this.id].chatMessage = message;
+
+ // Clear chat message after 30 seconds
+ setTimeout(() => {
+ players[this.id].chatMessage = '';
+ this.emit('chatUpdate', { playerId: this.id, chatMessage: '' });
+ }, 30000);
+
+ // Broadcast the chat message to other players
+ this.broadcast.emit('chatUpdate', { playerId: this.id, chatMessage: message });
+}
+
+/**
+ * Handle player disconnection events
+ */
+function handlePlayerDisconnect() {
+ console.log(`Player ${this.id} disconnected`);
+ delete players[this.id]; // Remove player from the object
+
+ // Notify other clients of the player's disconnection
+ this.broadcast.emit('playerDisconnected', { playerId: this.id });
+}
\ No newline at end of file
diff --git a/js/bcoin.js b/js/bcoin.js
deleted file mode 100644
index 34fe1cb..0000000
--- a/js/bcoin.js
+++ /dev/null
@@ -1,45 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- var Bcoin = Mario.Bcoin = function(pos) {
- Mario.Entity.call(this, {
- pos: pos,
- sprite: level.bcoinSprite(),
- hitbox: [0,0,16,16]
- });
- }
-
- Mario.Util.inherits(Bcoin, Mario.Entity);
-
- //I'm not sure whether it makes sense to use an array for vel and acc here
- //in order to keep with convention, or to just use a single value, since
- //it's literally impossible for these to move left or right.
- Bcoin.prototype.spawn = function() {
- sounds.coin.currentTime = 0.05;
- sounds.coin.play();
- this.idx = level.items.length;
- level.items.push(this);
- this.active = true;
- this.vel = -12;
- this.targetpos = this.pos[1] - 32;
- }
-
- Bcoin.prototype.update = function(dt) {
- if (!this.active) return;
-
- if (this.vel > 0 && this.pos[1] >= this.targetpos) {
- player.coins += 1;
- //spawn a score thingy.
- delete level.items[this.idx];
- }
-
- this.acc = 0.75;
- this.vel += this.acc;
- this.pos[1] += this.vel;
- this.sprite.update(dt);
- }
-
- Bcoin.prototype.checkCollisions = function() {;}
-
-})();
diff --git a/js/block.js b/js/block.js
deleted file mode 100644
index e695f04..0000000
--- a/js/block.js
+++ /dev/null
@@ -1,81 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- //TODO: clean up the logic for sprite switching.
- //TODO: There's a weird bug with the collision logic. Look into it.
-
- var Block = Mario.Block = function(options) {
- this.item = options.item;
- this.usedSprite = options.usedSprite;
- this.bounceSprite = options.bounceSprite;
- this.breakable = options.breakable;
-
- Mario.Entity.call(this, {
- pos: options.pos,
- sprite: options.sprite,
- hitbox: [0,0,16,16]
- });
-
- this.standing = true;
- }
-
- Mario.Util.inherits(Block, Mario.Floor);
-
- Block.prototype.break = function() {
- sounds.breakBlock.play();
- (new Mario.Rubble()).spawn(this.pos);
- var x = this.pos[0] / 16, y = this.pos[1] / 16;
- delete level.blocks[y][x];
- }
-
- Block.prototype.bonk = function(power) {
- sounds.bump.play();
- if (power > 0 && this.breakable) {
- this.break();
- } else if (this.standing){
- this.standing = false;
- if (this.item) {
- this.item.spawn();
- this.item = null;
- }
- this.opos = [];
- this.opos[0] = this.pos[0];
- this.opos[1] = this.pos[1];
- if (this.bounceSprite) {
- this.osprite = this.sprite;
- this.sprite = this.bounceSprite;
- } else {
- this.sprite = this.usedSprite;
- }
-
- this.vel[1] = -2;
- }
- }
-
- Block.prototype.update = function(dt, gameTime) {
- if (!this.standing) {
- if (this.pos[1] < this.opos[1] - 8) {
- this.vel[1] = 2;
- }
- if (this.pos[1] > this.opos[1]) {
- this.vel[1] = 0;
- this.pos = this.opos;
- if (this.osprite) {
- this.sprite = this.osprite;
- }
- this.standing = true;
- }
- } else {
- if (this.sprite === this.usedSprite) {
- var x = this.pos[0] / 16, y = this.pos[1] / 16;
- level.statics[y][x] = new Mario.Floor(this.pos, this.usedSprite);
- delete level.blocks[y][x];
- }
- }
-
- this.pos[1] += this.vel[1];
- this.sprite.update(dt, gameTime);
- }
-
-})();
diff --git a/js/coin.js b/js/coin.js
deleted file mode 100644
index 28f1758..0000000
--- a/js/coin.js
+++ /dev/null
@@ -1,47 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- var Coin = Mario.Coin = function(pos, sprite) {
- Mario.Entity.call(this, {
- pos: pos,
- sprite: sprite,
- hitbox: [0,0,16,16]
- });
- this.idx = level.items.length
- }
-
- Mario.Util.inherits(Coin, Mario.Entity);
-
- Coin.prototype.isPlayerCollided = function() {
- //the first two elements of the hitbox array are an offset, so let's do this now.
- var hpos1 = [this.pos[0] + this.hitbox[0], this.pos[1] + this.hitbox[1]];
- var hpos2 = [player.pos[0] + player.hitbox[0], player.pos[1] + player.hitbox[1]];
-
- //if the hitboxes actually overlap
- if (!(hpos1[0] > hpos2[0]+player.hitbox[2] || (hpos1[0]+this.hitbox[2] < hpos2[0]))) {
- if (!(hpos1[1] > hpos2[1]+player.hitbox[3] || (hpos1[1]+this.hitbox[3] < hpos2[1]))) {
- this.collect();
- }
- }
- }
-
- Coin.prototype.render = function(ctx, vX, vY) {
- this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
- }
-
- //money is not affected by gravity, you see.
- Coin.prototype.update = function(dt) {
- this.sprite.update(dt);
- }
- Coin.prototype.checkCollisions = function() {
- this.isPlayerCollided();
- }
-
- Coin.prototype.collect = function() {
- sounds.coin.currentTime = 0.05;
- sounds.coin.play();
- player.coins += 1;
- delete level.items[this.idx]
- }
-})();
diff --git a/js/floor.js b/js/floor.js
deleted file mode 100644
index 016d678..0000000
--- a/js/floor.js
+++ /dev/null
@@ -1,56 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- var Floor = Mario.Floor = function(pos, sprite) {
-
- Mario.Entity.call(this, {
- pos: pos,
- sprite: sprite,
- hitbox: [0,0,16,16]
- });
- }
-
- Mario.Util.inherits(Floor, Mario.Entity);
-
- Floor.prototype.isCollideWith = function (ent) {
- //the first two elements of the hitbox array are an offset, so let's do this now.
- var hpos1 = [Math.floor(this.pos[0] + this.hitbox[0]), Math.floor(this.pos[1] + this.hitbox[1])];
- var hpos2 = [Math.floor(ent.pos[0] + ent.hitbox[0]), Math.floor(ent.pos[1] + ent.hitbox[1])];
-
- //if the hitboxes actually overlap
- if (!(hpos1[0] > hpos2[0]+ent.hitbox[2] || (hpos1[0]+this.hitbox[2] < hpos2[0]))) {
- if (!(hpos1[1] > hpos2[1]+ent.hitbox[3] || (hpos1[1]+this.hitbox[3] < hpos2[1]))) {
- if (!this.standing) {
- ent.bump();
- } else {
- //if the entity is over the block, it's basically floor
- var center = hpos2[0] + ent.hitbox[2] / 2;
- if (Math.abs(hpos2[1] + ent.hitbox[3] - hpos1[1]) <= ent.vel[1]) {
- if (level.statics[(this.pos[1] / 16) - 1][this.pos[0] / 16]) {return};
- ent.vel[1] = 0;
- ent.pos[1] = hpos1[1] - ent.hitbox[3] - ent.hitbox[1];
- ent.standing = true;
- if (ent instanceof Mario.Player) {
- ent.jumping = 0;
- }
- } else if (Math.abs(hpos2[1] - hpos1[1] - this.hitbox[3]) > ent.vel[1] &&
- center + 2 >= hpos1[0] && center - 2 <= hpos1[0] + this.hitbox[2]) {
- //ent is under the block.
- ent.vel[1] = 0;
- ent.pos[1] = hpos1[1] + this.hitbox[3];
- if (ent instanceof Mario.Player) {
- this.bonk(ent.power);
- ent.jumping = 0;
- }
- } else {
- //entity is hitting it from the side, we're a wall
- ent.collideWall(this);
- }
- }
- }
- }
- }
-
- Floor.prototype.bonk = function() {;}
-})();
diff --git a/js/goomba.js b/js/goomba.js
deleted file mode 100644
index 78f420a..0000000
--- a/js/goomba.js
+++ /dev/null
@@ -1,129 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- //TODO: On console the hitbox is smaller. Measure it and edit this.
-
- var Goomba = Mario.Goomba = function(pos, sprite) {
- this.dying = false;
- Mario.Entity.call(this, {
- pos: pos,
- sprite: sprite,
- hitbox: [0,0,16,16]
- });
- this.vel[0] = -0.5;
- this.idx = level.enemies.length;
- };
-
- Goomba.prototype.render = function(ctx, vX, vY) {
- this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
- };
-
- Goomba.prototype.update = function(dt, vX) {
- if (this.pos[0] - vX > 336) { //if we're too far away, do nothing.
- return;
- } else if (this.pos[0] - vX < -32) {
- delete level.enemies[this.idx];
- }
-
- if (this.dying) {
- this.dying -= 1;
- if (!this.dying) {
- delete level.enemies[this.idx];
- }
- }
- this.acc[1] = 0.2;
- this.vel[1] += this.acc[1];
- this.pos[0] += this.vel[0];
- this.pos[1] += this.vel[1];
- this.sprite.update(dt);
- };
-
- Goomba.prototype.collideWall = function() {
- this.vel[0] = -this.vel[0];
- };
-
- Goomba.prototype.checkCollisions = function() {
- if (this.flipping) {
- return;
- }
-
- var h = this.pos[1] % 16 === 0 ? 1 : 2;
- var w = this.pos[0] % 16 === 0 ? 1 : 2;
-
- var baseX = Math.floor(this.pos[0] / 16);
- var baseY = Math.floor(this.pos[1] / 16);
-
- if (baseY + h > 15) {
- delete level.enemies[this.idx];
- return;
- }
-
- for (var i = 0; i < h; i++) {
- for (var j = 0; j < w; j++) {
- if (level.statics[baseY + i][baseX + j]) {
- level.statics[baseY + i][baseX + j].isCollideWith(this);
- }
- if (level.blocks[baseY + i][baseX + j]) {
- level.blocks[baseY + i][baseX + j].isCollideWith(this);
- }
- }
- }
- var that = this;
- level.enemies.forEach(function(enemy){
- if (enemy === that) { //don't check collisions with ourselves.
- return;
- } else if (enemy.pos[0] - vX > 336){ //stop checking once we get to far away dudes.
- return;
- } else {
- that.isCollideWith(enemy);
- }
- });
- this.isCollideWith(player);
- };
-
- Goomba.prototype.isCollideWith = function(ent) {
- if (ent instanceof Mario.Player && (this.dying || ent.invincibility)) {
- return;
- }
-
- //the first two elements of the hitbox array are an offset, so let's do this now.
- var hpos1 = [this.pos[0] + this.hitbox[0], this.pos[1] + this.hitbox[1]];
- var hpos2 = [ent.pos[0] + ent.hitbox[0], ent.pos[1] + ent.hitbox[1]];
-
- //if the hitboxes actually overlap
- if (!(hpos1[0] > hpos2[0]+ent.hitbox[2] || (hpos1[0]+this.hitbox[2] < hpos2[0]))) {
- if (!(hpos1[1] > hpos2[1]+ent.hitbox[3] || (hpos1[1]+this.hitbox[3] < hpos2[1]))) {
- if (ent instanceof Mario.Player) { //if we hit the player
- if (ent.vel[1] > 0) { //then the goomba dies
- this.stomp();
- } else if (ent.starTime) {
- this.bump();
- } else { //or the player gets hit
- ent.damage();
- }
- } else {
- this.collideWall();
- }
- }
- }
- };
-
- Goomba.prototype.stomp = function() {
- sounds.stomp.play();
- player.bounce = true;
- this.sprite.pos[0] = 32;
- this.sprite.speed = 0;
- this.vel[0] = 0;
- this.dying = 10;
- };
-
- Goomba.prototype.bump = function() {
- sounds.kick.play();
- this.sprite.img = 'sprites/enemyr.png';
- this.flipping = true;
- this.pos[1] -= 1;
- this.vel[0] = 0;
- this.vel[1] = -2.5;
- };
-})();
diff --git a/js/input.js b/js/input.js
deleted file mode 100644
index 79ae296..0000000
--- a/js/input.js
+++ /dev/null
@@ -1,54 +0,0 @@
-(function() {
- var pressedKeys = {};
-
- function setKey(event, status) {
- var code = event.keyCode;
- var key;
-
- switch(code) {
- case 32:
- key = 'SPACE'; break;
- case 37:
- key = 'LEFT'; break;
- case 38:
- key = 'UP'; break;
- case 39:
- key = 'RIGHT'; break;
- case 40:
- key = 'DOWN'; break;
- case 88:
- key = 'JUMP'; break;
- case 90:
- key = 'RUN'; break;
- default:
- key = String.fromCharCode(code);
- }
-
- pressedKeys[key] = status;
- }
-
- document.addEventListener('keydown', function(e) {
- setKey(e, true);
- });
-
- document.addEventListener('keyup', function(e) {
- setKey(e, false);
- });
-
- window.addEventListener('blur', function() {
- pressedKeys = {};
- });
-
- window.input = {
- isDown: function(key) {
- return pressedKeys[key.toUpperCase()];
- },
- reset: function() {
- pressedKeys['RUN'] = false;
- pressedKeys['LEFT'] = false;
- pressedKeys['RIGHT'] = false;
- pressedKeys['DOWN'] = false;
- pressedKeys['JUMP'] = false;
- }
- };
-})();
diff --git a/js/mushroom.js b/js/mushroom.js
deleted file mode 100644
index 97e7233..0000000
--- a/js/mushroom.js
+++ /dev/null
@@ -1,118 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- var Mushroom = Mario.Mushroom = function(pos) {
- this.spawning = false;
- this.waiting = 0;
-
- Mario.Entity.call(this, {
- pos: pos,
- sprite: level.superShroomSprite,
- hitbox: [0,0,16,16]
- });
- }
-
- Mario.Util.inherits(Mushroom, Mario.Entity);
-
- Mushroom.prototype.render = function(ctx, vX, vY) {
- if (this.spawning > 1) return;
- this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
- }
-
- Mushroom.prototype.spawn = function() {
- if (player.power > 0) {
- //replace this with a fire flower
- var ff = new Mario.Fireflower(this.pos)
- ff.spawn();
- return;
- }
- sounds.itemAppear.play();
- this.idx = level.items.length;
- level.items.push(this);
- this.spawning = 12;
- this.targetpos = [];
- this.targetpos[0] = this.pos[0];
- this.targetpos[1] = this.pos[1] - 16;
- }
-
- Mushroom.prototype.update = function(dt) {
- if (this.spawning > 1) {
- this.spawning -= 1;
- if (this.spawning == 1) this.vel[1] = -.5;
- return;
- }
- if (this.spawning) {
- if (this.pos[1] <= this.targetpos[1]) {
- this.pos[1] = this.targetpos[1];
- this.vel[1] = 0;
- this.waiting = 5;
- this.spawning = 0;
- this.vel[0] = 1;
- }
- } else {
- this.acc[1] = 0.2;
- }
-
- if (this.waiting) {
- this.waiting -= 1;
- } else {
- this.vel[1] += this.acc[1];
- this.pos[0] += this.vel[0];
- this.pos[1] += this.vel[1];
- this.sprite.update(dt);
- }
- }
-
- Mushroom.prototype.collideWall = function() {
- this.vel[0] = -this.vel[0];
- }
-
- Mushroom.prototype.checkCollisions = function() {
- if(this.spawning) {
- return;
- }
- var h = this.pos[1] % 16 == 0 ? 1 : 2;
- var w = this.pos[0] % 16 == 0 ? 1 : 2;
-
- var baseX = Math.floor(this.pos[0] / 16);
- var baseY = Math.floor(this.pos[1] / 16);
-
- if (baseY + h > 15) {
- delete level.items[this.idx];
- return;
- }
-
- for (var i = 0; i < h; i++) {
- for (var j = 0; j < w; j++) {
- if (level.statics[baseY + i][baseX + j]) {
- level.statics[baseY + i][baseX + j].isCollideWith(this);
- }
- if (level.blocks[baseY + i][baseX + j]) {
- level.blocks[baseY + i][baseX + j].isCollideWith(this);
- }
- }
- }
-
- this.isPlayerCollided();
- }
-
- //we have access to player everywhere, so let's just do this.
- Mushroom.prototype.isPlayerCollided = function() {
- //the first two elements of the hitbox array are an offset, so let's do this now.
- var hpos1 = [this.pos[0] + this.hitbox[0], this.pos[1] + this.hitbox[1]];
- var hpos2 = [player.pos[0] + player.hitbox[0], player.pos[1] + player.hitbox[1]];
-
- //if the hitboxes actually overlap
- if (!(hpos1[0] > hpos2[0]+player.hitbox[2] || (hpos1[0]+this.hitbox[2] < hpos2[0]))) {
- if (!(hpos1[1] > hpos2[1]+player.hitbox[3] || (hpos1[1]+this.hitbox[3] < hpos2[1]))) {
- player.powerUp(this.idx);
- }
- }
- }
-
- Mushroom.prototype.bump = function() {
- this.vel[1] = -2;
- }
-
-})();
diff --git a/js/sprite.js b/js/sprite.js
deleted file mode 100644
index 94b8d6c..0000000
--- a/js/sprite.js
+++ /dev/null
@@ -1,47 +0,0 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- var Sprite = Mario.Sprite = function(img, pos, size, speed, frames, once) {
- this.pos = pos;
- this.size = size;
- this.speed = speed;
- this._index = 0;
- this.img = img;
- this.once = once;
- this.frames = frames;
- }
-
- Sprite.prototype.update = function(dt, gameTime) {
- if (gameTime && gameTime == this.lastUpdated) return;
- this._index += this.speed*dt;
- if (gameTime) this.lastUpdated = gameTime;
- }
-
- Sprite.prototype.setFrame = function(frame) {
- this._index = frame;
- }
-
- Sprite.prototype.render = function(ctx, posx, posy, vX, vY) {
- var frame;
-
- if (this.speed > 0) {
- var max = this.frames.length;
- var idx = Math.floor(this._index);
- frame = this.frames[idx % max];
-
- if (this.once && idx >= max) {
- this.done = true;
- return;
- }
- } else {
- frame = 0;
- }
-
- var x = this.pos[0];
- var y = this.pos[1];
-
- x += frame*this.size[0];
- ctx.drawImage(resources.get(this.img), x + (1/3),y + (1/3), this.size[0] - (2/3), this.size[1] - (2/3), Math.round(posx - vX), Math.round(posy - vY), this.size[0],this.size[1]);
- }
-})();
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..eb065ef
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "Crazy Runner MMORPG",
+ "version": "1.0.0",
+ "description": "A MMORPG where you are free to be your character!",
+ "main": "app.js",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "dependencies": {
+ "ejs": "^3.1.10",
+ "express": "^4.18.2",
+ "openssl": "^2.0.0",
+ "openssl-nodejs": "^1.0.5",
+ "socket.io": "^4.5.0"
+ }
+}
diff --git a/css/game.css b/public/css/game.css
similarity index 100%
rename from css/game.css
rename to public/css/game.css
diff --git a/index.html b/public/index.html
similarity index 57%
rename from index.html
rename to public/index.html
index c1b06bf..8685871 100644
--- a/index.html
+++ b/public/index.html
@@ -2,16 +2,59 @@
-
-
Super Mario Bros
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -36,9 +79,5 @@
-
-
Move: Arrow Keys, Jump: X, Run: Z
-
My Links
Portfolio LinkedIn github
-
diff --git a/public/js/bcoin.js b/public/js/bcoin.js
new file mode 100644
index 0000000..41b9ee1
--- /dev/null
+++ b/public/js/bcoin.js
@@ -0,0 +1,65 @@
+(function() {
+ // Ensure the Mario namespace exists
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Bcoin entity in the game, a collectible coin that bounces.
+ * @param {Array
} pos - The initial position of the Bcoin in [x, y] format.
+ */
+ var Bcoin = Mario.Bcoin = function(pos) {
+ Mario.Entity.call(this, {
+ pos: pos, // Position of the Bcoin
+ sprite: level.bcoinSprite(), // Sprite representation of the Bcoin
+ hitbox: [0, 0, 16, 16] // Hitbox dimensions [offsetX, offsetY, width, height]
+ });
+ this.active = false; // Indicates if the Bcoin is currently active
+ this.vel = 0; // Vertical velocity for the Bcoin
+ this.acc = 0; // Vertical acceleration for the Bcoin
+ };
+
+ // Inherit from Mario.Entity
+ Mario.Util.inherits(Bcoin, Mario.Entity);
+
+ /**
+ * Spawns the Bcoin, making it active and playing the collection sound.
+ */
+ Bcoin.prototype.spawn = function() {
+ sounds.coin.currentTime = 0.05; // Reset coin sound playback
+ sounds.coin.play(); // Play coin collection sound
+ this.idx = level.items.length; // Index of the Bcoin in level items
+ level.items.push(this); // Add Bcoin to the level items
+ this.active = true; // Mark the Bcoin as active
+ this.vel = -12; // Initial upward velocity
+ this.targetpos = this.pos[1] - 32; // Target position for bouncing
+ };
+
+ /**
+ * Updates the Bcoin's position based on its velocity and acceleration.
+ * @param {number} dt - The time delta since the last update.
+ */
+ Bcoin.prototype.update = function(dt) {
+ if (!this.active) return; // Exit if the Bcoin is not active
+
+ // Check if Bcoin has reached its target position
+ if (this.vel > 0 && this.pos[1] >= this.targetpos) {
+ player.coins += 1; // Increment player's coin count
+ delete level.items[this.idx]; // Remove Bcoin from level items
+ return; // Exit after collecting the coin
+ }
+
+ this.acc = 0.75; // Define constant vertical acceleration
+ this.vel += this.acc; // Update velocity with acceleration
+ this.pos[1] += this.vel; // Update the position based on velocity
+ this.sprite.update(dt); // Update sprite animation
+ };
+
+ /**
+ * Checks for collisions with other entities (not implemented).
+ */
+ Bcoin.prototype.checkCollisions = function() {
+ // Collision logic can be implemented here if needed
+ };
+
+})();
\ No newline at end of file
diff --git a/public/js/block.js b/public/js/block.js
new file mode 100644
index 0000000..7f8c96f
--- /dev/null
+++ b/public/js/block.js
@@ -0,0 +1,96 @@
+(function() {
+ // Ensure Mario namespace exists
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Block entity in the game.
+ * @param {Object} options - Configuration options for the block.
+ * @param {Object} options.item - The item to spawn when the block is interacted with.
+ * @param {Object} options.usedSprite - The sprite to display when the block has been used.
+ * @param {Object} options.bounceSprite - The sprite to display when the block is bounced on.
+ * @param {boolean} options.breakable - Indicates if the block can be broken.
+ */
+ var Block = Mario.Block = function(options) {
+ this.item = options.item; // Item to spawn
+ this.usedSprite = options.usedSprite; // Sprite after being used
+ this.bounceSprite = options.bounceSprite; // Sprite for bouncing
+ this.breakable = options.breakable; // Is the block breakable?
+
+ // Initialize the entity properties
+ Mario.Entity.call(this, {
+ pos: options.pos, // Position of the block
+ sprite: options.sprite, // Current sprite of the block
+ hitbox: [0, 0, 16, 16] // Hitbox dimensions [offsetX, offsetY, width, height]
+ });
+
+ this.standing = true; // Indicates if the block is in a standing state
+ };
+
+ // Inherit from Mario.Floor
+ Mario.Util.inherits(Block, Mario.Floor);
+
+ /**
+ * Breaks the block, playing sound and spawning rubble.
+ */
+ Block.prototype.break = function() {
+ sounds.breakBlock.play(); // Play block breaking sound
+ (new Mario.Rubble()).spawn(this.pos); // Spawn rubble at block's position
+ var x = this.pos[0] / 16, y = this.pos[1] / 16;
+ delete level.blocks[y][x]; // Remove block from level
+ };
+
+ /**
+ * Handles the interaction when the block is bonked.
+ * @param {number} power - The power level of the interaction.
+ */
+ Block.prototype.bonk = function(power) {
+ sounds.bump.play(); // Play bump sound
+ if (power > 0 && this.breakable) {
+ this.break(); // Break the block if it is breakable
+ } else if (this.standing) {
+ this.standing = false; // Update standing state
+ if (this.item) {
+ this.item.spawn(); // Spawn the item if it exists
+ this.item = null; // Clear item reference
+ }
+ this.opos = [this.pos[0], this.pos[1]]; // Store original position
+ this.osprite = this.sprite; // Store original sprite
+
+ // Switch to bounce or used sprite
+ this.sprite = this.bounceSprite ? this.bounceSprite : this.usedSprite;
+
+ this.vel[1] = -2; // Set vertical velocity for bounce
+ }
+ };
+
+ /**
+ * Updates the block's state and position based on the elapsed time.
+ * @param {number} dt - The time delta since the last update.
+ * @param {number} gameTime - The total game time.
+ */
+ Block.prototype.update = function(dt, gameTime) {
+ if (!this.standing) {
+ if (this.pos[1] < this.opos[1] - 8) {
+ this.vel[1] = 2; // Adjust velocity to fall down
+ }
+ if (this.pos[1] > this.opos[1]) {
+ this.vel[1] = 0; // Stop vertical movement
+ this.pos = this.opos; // Reset position to original
+ this.sprite = this.osprite || this.sprite; // Restore original sprite if it exists
+ this.standing = true; // Set standing state back to true
+ }
+ } else {
+ if (this.sprite === this.usedSprite) {
+ var x = this.pos[0] / 16, y = this.pos[1] / 16;
+ level.statics[y][x] = new Mario.Floor(this.pos, this.usedSprite); // Replace block with floor
+ delete level.blocks[y][x]; // Remove block from level
+ }
+ }
+
+ this.pos[1] += this.vel[1]; // Update vertical position
+ this.sprite.update(dt, gameTime); // Update sprite animation
+ };
+
+})();
\ No newline at end of file
diff --git a/public/js/coin.js b/public/js/coin.js
new file mode 100644
index 0000000..62671b2
--- /dev/null
+++ b/public/js/coin.js
@@ -0,0 +1,74 @@
+(function() {
+ // Ensure the Mario namespace exists
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Coin entity in the game.
+ * @param {Array} pos - The initial position of the coin in [x, y] format.
+ * @param {Object} sprite - The sprite representation of the coin.
+ */
+ var Coin = Mario.Coin = function(pos, sprite) {
+ Mario.Entity.call(this, {
+ pos: pos, // Position of the coin
+ sprite: sprite, // Coin's sprite
+ hitbox: [0, 0, 16, 16] // Hitbox dimensions [offsetX, offsetY, width, height]
+ });
+ this.idx = level.items.length; // Index of the coin in level items
+ };
+
+ // Inherit from Mario.Entity
+ Mario.Util.inherits(Coin, Mario.Entity);
+
+ /**
+ * Checks for collision with the player.
+ * If a collision is detected, the coin is collected.
+ */
+ Coin.prototype.isPlayerCollided = function() {
+ // Calculate hitbox positions for the coin and player
+ var hpos1 = [this.pos[0] + this.hitbox[0], this.pos[1] + this.hitbox[1]];
+ var hpos2 = [player.pos[0] + player.hitbox[0], player.pos[1] + player.hitbox[1]];
+
+ // Check for overlap between the coin's and player's hitboxes
+ if (!(hpos1[0] > hpos2[0] + player.hitbox[2] || (hpos1[0] + this.hitbox[2] < hpos2[0])) &&
+ !(hpos1[1] > hpos2[1] + player.hitbox[3] || (hpos1[1] + this.hitbox[3] < hpos2[1]))) {
+ this.collect(); // Collect the coin if there's a collision
+ }
+ };
+
+ /**
+ * Renders the coin on the given canvas context.
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
+ * @param {number} vX - The horizontal velocity for rendering.
+ * @param {number} vY - The vertical velocity for rendering.
+ */
+ Coin.prototype.render = function(ctx, vX, vY) {
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY); // Render the coin sprite
+ };
+
+ /**
+ * Updates the coin's state. Coins are not affected by gravity.
+ * @param {number} dt - The time delta since the last update.
+ */
+ Coin.prototype.update = function(dt) {
+ this.sprite.update(dt); // Update the sprite animation
+ };
+
+ /**
+ * Checks for collisions with the player.
+ */
+ Coin.prototype.checkCollisions = function() {
+ this.isPlayerCollided(); // Check for player collision
+ };
+
+ /**
+ * Collects the coin, updating the player's score and playing a sound.
+ */
+ Coin.prototype.collect = function() {
+ sounds.coin.currentTime = 0.05; // Reset coin sound playback
+ sounds.coin.play(); // Play coin collection sound
+ player.coins += 1; // Increment player's coin count
+ delete level.items[this.idx]; // Remove coin from level items
+ };
+})();
\ No newline at end of file
diff --git a/js/entity.js b/public/js/entity.js
similarity index 100%
rename from js/entity.js
rename to public/js/entity.js
diff --git a/js/fireball.js b/public/js/fireball.js
similarity index 100%
rename from js/fireball.js
rename to public/js/fireball.js
diff --git a/js/fireflower.js b/public/js/fireflower.js
similarity index 100%
rename from js/fireflower.js
rename to public/js/fireflower.js
diff --git a/js/flag.js b/public/js/flag.js
similarity index 100%
rename from js/flag.js
rename to public/js/flag.js
diff --git a/public/js/floor.js b/public/js/floor.js
new file mode 100644
index 0000000..ea0e905
--- /dev/null
+++ b/public/js/floor.js
@@ -0,0 +1,88 @@
+(function() {
+ // Ensure the Mario namespace exists
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Floor entity in the game.
+ * @param {Array} pos - The initial position of the Floor in [x, y] format.
+ * @param {Object} sprite - The sprite representation of the Floor.
+ */
+ var Floor = Mario.Floor = function(pos, sprite) {
+ Mario.Entity.call(this, {
+ pos: pos, // Position of the Floor
+ sprite: sprite, // Sprite representation of the Floor
+ hitbox: [0, 0, 16, 16] // Hitbox dimensions [offsetX, offsetY, width, height]
+ });
+ };
+
+ // Inherit from Mario.Entity
+ Mario.Util.inherits(Floor, Mario.Entity);
+
+ /**
+ * Checks for collision between this Floor and another entity.
+ * @param {Mario.Entity} ent - The entity to check for collision with.
+ */
+ Floor.prototype.isCollideWith = function(ent) {
+ // Calculate hitbox positions for both entities
+ var hpos1 = [Math.floor(this.pos[0] + this.hitbox[0]), Math.floor(this.pos[1] + this.hitbox[1])];
+ var hpos2 = [Math.floor(ent.pos[0] + ent.hitbox[0]), Math.floor(ent.pos[1] + ent.hitbox[1])];
+
+ // Check if hitboxes overlap
+ if (!(hpos1[0] > hpos2[0] + ent.hitbox[2] || (hpos1[0] + this.hitbox[2] < hpos2[0])) &&
+ !(hpos1[1] > hpos2[1] + ent.hitbox[3] || (hpos1[1] + this.hitbox[3] < hpos2[1]))) {
+
+ // Handle collisions based on relative position
+ if (!this.standing) {
+ ent.bump(); // Entity bumps if the floor is not standing
+ } else {
+ this.handleEntityCollision(ent, hpos1, hpos2);
+ }
+ }
+ };
+
+ /**
+ * Handles collision response when an entity collides with the Floor.
+ * @param {Mario.Entity} ent - The entity that is colliding with the Floor.
+ * @param {Array} hpos1 - The hitbox position of the Floor.
+ * @param {Array} hpos2 - The hitbox position of the colliding entity.
+ */
+ Floor.prototype.handleEntityCollision = function(ent, hpos1, hpos2) {
+ var center = hpos2[0] + ent.hitbox[2] / 2;
+
+ // Check if the entity is landing on the floor
+ if (Math.abs(hpos2[1] + ent.hitbox[3] - hpos1[1]) <= ent.vel[1]) {
+ // Prevent standing on top of a solid static block
+ if (level.statics[(this.pos[1] / 16) - 1][this.pos[0] / 16]) return;
+ ent.vel[1] = 0; // Reset vertical velocity
+ ent.pos[1] = hpos1[1] - ent.hitbox[3] - ent.hitbox[1]; // Position entity on top of the floor
+ ent.standing = true; // Set entity as standing
+ if (ent instanceof Mario.Player) {
+ ent.jumping = 0; // Reset jump status for player
+ }
+ }
+ // Check if the entity is below the floor
+ else if (Math.abs(hpos2[1] - hpos1[1] - this.hitbox[3]) > ent.vel[1] &&
+ center + 2 >= hpos1[0] && center - 2 <= hpos1[0] + this.hitbox[2]) {
+ ent.vel[1] = 0; // Reset vertical velocity
+ ent.pos[1] = hpos1[1] + this.hitbox[3]; // Position entity above the floor
+ if (ent instanceof Mario.Player) {
+ this.bonk(ent.power); // Handle player bonking the floor
+ ent.jumping = 0; // Reset jump status for player
+ }
+ }
+ // Handle side collision
+ else {
+ ent.collideWall(this); // Treat floor as a wall
+ }
+ };
+
+ /**
+ * Placeholder for additional bonking logic.
+ */
+ Floor.prototype.bonk = function() {
+ // Implementation can be added later
+ };
+ })();
+
\ No newline at end of file
diff --git a/js/game.js b/public/js/game-orginal.js
similarity index 100%
rename from js/game.js
rename to public/js/game-orginal.js
diff --git a/public/js/game.js b/public/js/game.js
new file mode 100644
index 0000000..720b0f8
--- /dev/null
+++ b/public/js/game.js
@@ -0,0 +1,361 @@
+const socket = io({
+ path: '/socket.io',
+ reconnectionAttempts: 3,
+ reconnectionDelay: 5000,
+ transports: ['websocket']
+});
+
+let otherPlayers = {};
+let updateables = [];
+let fireballs = [];
+let playerInitialized = false;
+
+/* Player instance created at initial position [0, 0] */
+player = new Mario.Player([0, 0]);
+
+/* References to chat input field and form elements */
+const chatInput = document.getElementById('chatInput');
+const chatForm = document.getElementById('chatForm');
+
+/**
+ * Chat form submission handler
+ * Emits player’s chat message and clears the input
+ * @param {Event} e - Submit event object for form submission
+ */
+chatForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ if (player) {
+ player.chatMessage = chatInput.value;
+ socket.emit('playerChat', chatInput.value);
+ chatInput.value = '';
+
+ /* Clears player's chat message after 30 seconds */
+ setTimeout(() => {
+ if (player) player.chatMessage = '';
+ }, 30000);
+ }
+});
+
+/**
+ * Updates chat messages from other players
+ * @param {Object} data - Contains playerId and chatMessage
+ */
+socket.on('chatUpdate', (data) => {
+ if (player?.playerId === data.playerId) {
+ player.chatMessage = data.chatMessage;
+ } else if (otherPlayers[data.playerId]) {
+ otherPlayers[data.playerId].chatMessage = data.chatMessage;
+ }
+});
+
+/* Emitted when the client connects to the server, sends player's initial position */
+socket.on('connect', () => {
+ if (!playerInitialized) {
+ socket.emit('newPlayer', { pos: player?.pos });
+ playerInitialized = true;
+ }
+});
+
+/**
+ * Receives data for the current player upon connection
+ * @param {Object} data - Contains playerId and connectionNum
+ */
+socket.on('currentPlayer', (data) => {
+ if (player) {
+ player.playerId = data.playerId;
+ player.title = "Player " + data.connectionNum;
+ if (!playerInitialized) {
+ player.pos = [0, 0]; // Or another starting position
+ playerInitialized = true; // Set to true to avoid re-initialization
+ }
+ }
+});
+
+/**
+ * Adds a new player to otherPlayers when notified by the server
+ * @param {Object} data - Contains playerId, connectionNum, and gameData
+ */
+socket.on('newPlayer', (data) => {
+ if (data.gameData.pos[0] !== 0 || data.gameData.pos[1] !== 0) {
+ otherPlayers[data.playerId] = new Mario.Player(data.gameData.pos);
+ otherPlayers[data.playerId].title = "Player " + data.connectionNum;
+ }
+});
+
+/**
+ * Populates existing players in the game when joining
+ * @param {Object} data - Contains an array of existingPlayers
+ */
+socket.on('existingPlayers', (data) => {
+ data.existingPlayers.forEach((playerData) => {
+ if (playerData.gameData.pos[0] !== 0 || playerData.gameData.pos[1] !== 0) {
+ otherPlayers[playerData.playerId] = new Mario.Player(playerData.gameData.pos);
+ otherPlayers[playerData.playerId].title = "Player " + playerData.connectionNum;
+ }
+ });
+});
+
+/**
+ * Removes a player from the game upon disconnection
+ * @param {String} playerId - Unique identifier of the disconnected player
+ */
+socket.on('playerDisconnected', (playerId) => {
+ delete otherPlayers[playerId];
+});
+
+/**
+ * Updates positions of players when they move
+ * @param {Object} data - Contains player's playerId and gameData position
+ */
+socket.on('playerMoved', (data) => {
+ if (otherPlayers[data.player.playerId]) {
+ otherPlayers[data.player.playerId].pos = data.player.gameData.pos;
+ }
+});
+
+/* Renders all other players in the game */
+Object.keys(otherPlayers).forEach(playerId => renderEntity(otherPlayers[playerId]));
+
+/**
+ * Creates a cross-browser compatible request animation frame
+ * Ensures 60 FPS for smooth transitions in animations
+ */
+const requestAnimFrame = (function () {
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ (callback => window.setTimeout(callback, 1000 / 60));
+})();
+
+/* Canvas setup for rendering game graphics */
+const canvas = document.createElement("canvas"),
+ ctx = canvas.getContext('2d');
+canvas.width = 762;
+canvas.height = 720;
+ctx.scale(3, 3);
+document.body.appendChild(canvas);
+
+/**
+ * Viewport dimensions defining visible game area
+ * @vX, @vY - viewport origin coordinates
+ * @vWidth, @vHeight - viewport width and height
+ */
+let vX = 0, vY = 0, vWidth = 256, vHeight = 240;
+
+/* Load essential game resources: sprites, images, sounds */
+resources.load([
+ 'sprites/player.png',
+ 'sprites/enemy.png',
+ 'sprites/tiles.png',
+ 'sprites/playerl.png',
+ 'sprites/items.png',
+ 'sprites/enemyr.png'
+]);
+resources.onReady(init);
+
+/**
+ * Initializes the game elements, music, and sound effects
+ * Sets up game background music and sound effects for various actions
+ */
+let level, sounds, music, lastTime, gameTime = 0;
+function init() {
+ music = {
+ overworld: new Audio('sounds/aboveground_bgm.ogg'),
+ underground: new Audio('sounds/underground_bgm.ogg'),
+ clear: new Audio('sounds/stage_clear.wav'),
+ death: new Audio('sounds/mariodie.wav')
+ };
+
+ sounds = {
+ smallJump: new Audio('sounds/jump-small.wav'),
+ bigJump: new Audio('sounds/jump-super.wav'),
+ breakBlock: new Audio('sounds/breakblock.wav'),
+ bump: new Audio('sounds/bump.wav'),
+ coin: new Audio('sounds/coin.wav'),
+ fireball: new Audio('sounds/fireball.wav'),
+ flagpole: new Audio('sounds/flagpole.wav'),
+ kick: new Audio('sounds/kick.wav'),
+ pipe: new Audio('sounds/pipe.wav'),
+ itemAppear: new Audio('sounds/itemappear.wav'),
+ powerup: new Audio('sounds/powerup.wav'),
+ stomp: new Audio('sounds/stomp.wav')
+ };
+ Mario.oneone(); // Start level setup
+ lastTime = Date.now();
+ main(); // Begin main loop
+}
+
+/**
+ * Main game loop; continuously updates game state and re-renders the game
+ * Calls itself recursively using requestAnimFrame to ensure smooth animation
+ */
+function main() {
+ const now = Date.now(),
+ dt = (now - lastTime) / 1000.0;
+ update(dt);
+ render();
+ lastTime = now;
+ requestAnimFrame(main);
+}
+
+/**
+ * Updates game state based on time delta (dt)
+ * @param {Number} dt - Time delta in seconds since last update
+ */
+function update(dt) {
+ gameTime += dt;
+ handleInput(dt); // Process user input for player actions
+ updateEntities(dt, gameTime); // Refresh all game entities based on dt
+ checkCollisions(); // Detect and respond to collisions
+ socket.emit('playerMovement', { pos: player?.pos }); // Send player position to server
+}
+
+/**
+ * Handles player input for movement and actions
+ * @param {Number} dt - Time delta for handling input at a consistent speed
+ */
+function handleInput(dt) {
+ if (player && !player.piping && !player.dying && !player.noInput) {
+ if (input.isDown('RUN')) player.run();
+ else player.noRun();
+ if (input.isDown('JUMP')) player.jump();
+ else player.noJump();
+ if (input.isDown('DOWN')) player.crouch();
+ else player.noCrouch();
+ if (input.isDown('LEFT')) player.moveLeft();
+ else if (input.isDown('RIGHT')) player.moveRight();
+ else player.noWalk();
+ }
+}
+
+/**
+ * Updates all active game entities, including player, items, enemies, and fireballs
+ * @param {Number} dt - Time delta for updating entity states
+ * @param {Number} gameTime - Total elapsed game time, used for timed events
+ */
+function updateEntities(dt, gameTime) {
+ player?.update(dt, vX); // Update player with dt and viewport x-axis position
+ updateables.forEach(ent => ent.update(dt, gameTime)); // Refresh updateable entities
+
+ /* Handles viewport shifting as player moves */
+ if (player.exiting) {
+ if (player.pos[0] > vX + 96)
+ vX = player.pos[0] - 96;
+ } else if (level.scrolling && player.pos[0] > vX + 80) {
+ vX = player.pos[0] - 80;
+ }
+
+ /* Return early if player is powering up or dying */
+ if (player.powering.length !== 0 || player.dying) return;
+
+ /* Update each item, enemy, fireball, and pipe in the game */
+ level.items.forEach(ent => ent.update(dt));
+ level.enemies.forEach(ent => ent.update(dt, vX));
+ fireballs.forEach(fireball => fireball.update(dt));
+ level.pipes.forEach(pipe => pipe.update(dt));
+}
+
+/**
+ * Scans for and manages collisions between player, items, enemies, fireballs, and pipes
+ */
+function checkCollisions() {
+ if (player.powering.length !== 0 || player.dying) return; // Skip if player is powering up or dying
+ player.checkCollisions(); // Check collisions for the player
+
+ /* Check collisions for each game entity type */
+ level.items.forEach(item => item.checkCollisions());
+ level.enemies.forEach(ent => ent.checkCollisions());
+ fireballs.forEach(fireball => fireball.checkCollisions());
+ level.pipes.forEach(pipe => pipe.checkCollisions());
+}
+
+/**
+ * Renders the entire game scene, clearing the previous frame and layering elements
+ */
+function render() {
+ updateables = []; // Reset updateable entities for the frame
+ ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas for next render
+ ctx.fillStyle = level.background; // Fill canvas background with level color
+ ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw filled rectangle covering the entire canvas
+
+ /**
+ * Render scenery for layering; first draw static items in viewable range
+ * Loops through the scenery array to render each entity
+ */
+ for (let i = 0; i < 15; i++) {
+ for (let j = Math.floor(vX / 16) - 1; j < Math.floor(vX / 16) + 20; j++) {
+ if (level.scenery[i][j]) { // Check if scenery exists at current indices
+ renderEntity(level.scenery[i][j]); // Render scenery entity
+ }
+ }
+ }
+
+ /**
+ * Render dynamic items in the scene, such as collectibles and enemies
+ * Uses forEach to iterate over items and enemies to render each entity
+ */
+ level.items.forEach(item => renderEntity(item)); // Render each item in the level
+ level.enemies.forEach(enemy => renderEntity(enemy)); // Render each enemy in the level
+ fireballs.forEach(fireball => renderEntity(fireball)); // Render each fireball in the level
+
+ /**
+ * Render static objects like blocks and other level details
+ * Loops through statics and blocks arrays for rendering
+ */
+ for (let i = 0; i < 15; i++) {
+ for (let j = Math.floor(vX / 16) - 1; j < Math.floor(vX / 16) + 20; j++) {
+ if (level.statics[i][j]) { // Check if static object exists at current indices
+ renderEntity(level.statics[i][j]); // Render static entity
+ }
+ if (level.blocks[i][j]) { // Check if block exists at current indices
+ renderEntity(level.blocks[i][j]); // Render block entity
+ updateables.push(level.blocks[i][j]); // Add block to updateables for next frame
+ }
+ }
+ }
+
+ /**
+ * Render the player character
+ * Conditional check for player's invincibility state
+ */
+ if (player.invincibility % 2 === 0) {
+ player.title = "Player 1"; // Set the title for the player
+ renderEntity(player); // Render the player entity
+ }
+
+ /**
+ * Render other players in the game
+ * Iterates through other players and checks their invincibility state
+ */
+ for (var playerId in otherPlayers) {
+ if (otherPlayers.hasOwnProperty(playerId) && otherPlayers[playerId].invincibility % 2 === 0) {
+ renderEntity(otherPlayers[playerId]); // Render other player entity
+ }
+ }
+
+ /**
+ * Render pipes; pipes are rendered last as Mario interacts with them
+ * Loops through pipes in the level and renders each
+ */
+ level.pipes.forEach(function (pipe) {
+ renderEntity(pipe); // Render pipe entity
+ });
+}
+
+/**
+* Renders a given game entity on the canvas
+* @param {Object} entity - The entity to render, which can be a player, enemy, item, etc.
+*/
+function renderEntity(entity) {
+ // Render the player's chat message above the entity if it exists
+ if (entity.chatMessage) {
+ ctx.fillStyle = "#FFFFFF"; // Set color for chat message text
+ ctx.font = "bold 10px Arial"; // Set font style and size
+ ctx.fillText(entity.chatMessage, entity.pos[0] - vX, entity.pos[1] - vY - 5); // Render chat message text
+ }
+
+ // Render the entity itself using its own render method
+ entity.render(ctx, vX, vY); // Calls the render method on the entity, passing in the context and viewport offsets
+}
\ No newline at end of file
diff --git a/public/js/goomba.js b/public/js/goomba.js
new file mode 100644
index 0000000..3bd67f1
--- /dev/null
+++ b/public/js/goomba.js
@@ -0,0 +1,164 @@
+(function() {
+ // Namespace for Mario module
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Goomba enemy.
+ * @constructor
+ * @param {Array} pos - Initial position of the Goomba [x, y].
+ * @param {Object} sprite - The sprite object used for rendering the Goomba.
+ */
+ var Goomba = Mario.Goomba = function(pos, sprite) {
+ this.dying = false; // Indicates if the Goomba is in the dying state.
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: sprite,
+ hitbox: [0, 0, 16, 16] // Hitbox dimensions [offsetX, offsetY, width, height].
+ });
+ this.vel = [-0.5, 0]; // Initial velocity [velocityX, velocityY].
+ this.idx = level.enemies.length; // Index of the Goomba in the enemies array.
+ };
+
+ // Inherit from Mario.Entity
+ Goomba.prototype = Object.create(Mario.Entity.prototype);
+ Goomba.prototype.constructor = Goomba;
+
+ /**
+ * Renders the Goomba on the canvas.
+ * @param {CanvasRenderingContext2D} ctx - The rendering context of the canvas.
+ * @param {number} vX - The horizontal viewport offset.
+ * @param {number} vY - The vertical viewport offset (not used in current implementation).
+ */
+ Goomba.prototype.render = function(ctx, vX, vY) {
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ };
+
+ /**
+ * Updates the Goomba's position and state.
+ * @param {number} dt - The delta time since the last update.
+ * @param {number} vX - The horizontal viewport offset.
+ */
+ Goomba.prototype.update = function(dt, vX) {
+ if (this.pos[0] - vX > 336) return; // Out of view
+ if (this.pos[0] - vX < -32) {
+ delete level.enemies[this.idx]; // Remove if too far left
+ return;
+ }
+
+ if (this.dying) {
+ this.dying -= 1;
+ if (!this.dying) {
+ delete level.enemies[this.idx]; // Remove when done dying
+ }
+ return;
+ }
+
+ this.acc[1] = 0.2; // Gravity effect
+ this.vel[1] += this.acc[1];
+ this.pos[0] += this.vel[0]; // Update position based on velocity
+ this.pos[1] += this.vel[1];
+ this.sprite.update(dt); // Update sprite state
+ };
+
+ /**
+ * Handles collision with walls by reversing horizontal velocity.
+ */
+ Goomba.prototype.collideWall = function() {
+ this.vel[0] = -this.vel[0];
+ };
+
+ /**
+ * Checks for collisions with other entities and the environment.
+ */
+ Goomba.prototype.checkCollisions = function() {
+ if (this.flipping) return; // Ignore if flipping
+
+ const height = this.pos[1] % 16 === 0 ? 1 : 2; // Determine height for collision checking
+ const width = this.pos[0] % 16 === 0 ? 1 : 2;
+
+ const baseX = Math.floor(this.pos[0] / 16);
+ const baseY = Math.floor(this.pos[1] / 16);
+
+ // Remove if off the bottom of the screen
+ if (baseY + height > 15) {
+ delete level.enemies[this.idx];
+ return;
+ }
+
+ // Check for collisions with static and block entities
+ for (let i = 0; i < height; i++) {
+ for (let j = 0; j < width; j++) {
+ if (level.statics[baseY + i][baseX + j]) {
+ level.statics[baseY + i][baseX + j].isCollideWith(this);
+ }
+ if (level.blocks[baseY + i][baseX + j]) {
+ level.blocks[baseY + i][baseX + j].isCollideWith(this);
+ }
+ }
+ }
+
+ // Check collisions with other enemies and the player
+ const self = this;
+ level.enemies.forEach(function(enemy) {
+ if (enemy === self || enemy.pos[0] - vX > 336) return; // Skip self and out-of-view enemies
+ self.isCollideWith(enemy);
+ });
+ this.isCollideWith(player);
+ };
+
+ /**
+ * Checks for collision with another entity.
+ * @param {Mario.Entity} ent - The entity to check collision against.
+ */
+ Goomba.prototype.isCollideWith = function(ent) {
+ if (ent instanceof Mario.Player && (this.dying || ent.invincibility)) {
+ return; // Ignore collisions if invincible
+ }
+
+ const hpos1 = [this.pos[0] + this.hitbox[0], this.pos[1] + this.hitbox[1]]; // Hitbox position of Goomba
+ const hpos2 = [ent.pos[0] + ent.hitbox[0], ent.pos[1] + ent.hitbox[1]]; // Hitbox position of entity
+
+ // Check for overlapping hitboxes
+ if (!(hpos1[0] > hpos2[0] + ent.hitbox[2] || hpos1[0] + this.hitbox[2] < hpos2[0]) &&
+ !(hpos1[1] > hpos2[1] + ent.hitbox[3] || hpos1[1] + this.hitbox[3] < hpos2[1])) {
+ if (ent instanceof Mario.Player) {
+ if (ent.vel[1] > 0) { //then the goomba dies
+ this.stomp();
+ } else if (ent.starTime) {
+ this.bump();
+ } else { //or the player gets hit
+ ent.damage();
+ }
+ } else {
+ this.collideWall();
+ }
+ }
+ };
+
+ Goomba.prototype.stomp = function() {
+ sounds.stomp.play();
+ player.bounce = true;
+ this.sprite.pos[0] = 32;
+ this.sprite.speed = 0;
+ this.vel[0] = 0;
+ this.dying = 10;
+ };
+
+ Goomba.prototype.bump = function() {
+ sounds.kick.play();
+ this.sprite.img = 'sprites/enemyr.png';
+ this.flipping = true;
+ this.pos[1] -= 1;
+ this.vel[0] = 0;
+ this.vel[1] = -2.5;
+ };
+
+ // Rendering and updating loop
+ function gameLoop(dt, vX) {
+ goomba.update(dt, vX);
+ goomba.render(ctx, vX, 0); // Assuming ctx is your rendering context
+ }
+
+})();
diff --git a/public/js/input.js b/public/js/input.js
new file mode 100644
index 0000000..b724da0
--- /dev/null
+++ b/public/js/input.js
@@ -0,0 +1,112 @@
+// input.js - Client-side module for handling keyboard input
+(function () {
+ // Object to keep track of pressed keys
+ const pressedKeys = {};
+
+ // Key mappings for easy reference
+ const keyMappings = {
+ 32: { name: 'RUN', elementId: 'key-space' }, // Space for Run
+ 37: { name: 'LEFT', elementId: 'key-left' },
+ 38: { name: 'JUMP', elementId: 'key-up' }, // Up for Jump
+ 39: { name: 'RIGHT', elementId: 'key-right' },
+ 40: { name: 'DOWN', elementId: 'key-down' },
+ 88: { name: 'JUMP', elementId: 'key-x' }, // X for Jump
+ 90: { name: 'RUN', elementId: 'key-z' } // Z for Run
+ };
+
+ /**
+ * Sets the status of a key (pressed or released)
+ * @param {KeyboardEvent} event - The keyboard event object
+ * @param {boolean} status - The status of the key (true for pressed, false for released)
+ */
+ function setKey(event, status) {
+ const { keyCode } = event; // Destructure keyCode from event
+ const keyData = keyMappings[keyCode]; // Get key data from mappings
+ const key = keyData ? keyData.name : String.fromCharCode(keyCode); // Get key name or use char code
+
+ // Update pressed keys status
+ pressedKeys[key] = status;
+
+ // Update the visual representation of the key
+ updateKeyVisual(keyData?.elementId, status);
+ if (status) movePlayer(key);
+ }
+
+ /**
+ * Updates the visual representation of the key pressed
+ * @param {string} elementId - The DOM element ID for the key
+ * @param {boolean} status - The status of the key (pressed or released)
+ */
+ function updateKeyVisual(elementId, status) {
+ if (!elementId) return; // Exit if no elementId provided
+
+ const keyElement = document.getElementById(elementId);
+ if (keyElement) {
+ keyElement.classList.toggle('pressed', status); // Add/remove pressed class based on status
+ }
+ }
+
+ /**
+ * Moves the player based on the pressed key
+ * @param {string} key - The key that was pressed
+ */
+ function movePlayer(key) {
+ const player = window.player; // Access the player object from the global scope
+
+ // Map keys to player actions
+ const actions = {
+ 'LEFT': player.moveLeft,
+ 'RIGHT': player.moveRight,
+ 'JUMP': player.jump,
+ 'RUN': player.run,
+ 'DOWN': player.crouch,
+ 'UP': player.climb // Optional: Make player climb
+ };
+
+ const action = actions[key];
+ if (action) action.call(player); // Execute the action if it exists
+ }
+
+ // Event listener for keydown events
+ document.addEventListener('keydown', (e) => setKey(e, true));
+
+ // Event listener for keyup events
+ document.addEventListener('keyup', (e) => setKey(e, false));
+
+ // Event listener for when the window loses focus
+ window.addEventListener('blur', () => {
+ resetKeys(); // Reset pressed keys
+ });
+
+ /**
+ * Resets all pressed keys and visual representations
+ */
+ function resetKeys() {
+ Object.keys(pressedKeys).forEach(key => {
+ pressedKeys[key] = false; // Set all keys as released
+ });
+
+ // Reset all key visuals
+ const keys = document.getElementsByClassName('key');
+ Array.from(keys).forEach(key => key.classList.remove('pressed'));
+ }
+
+ // Expose input functions to the global scope
+ window.input = {
+ /**
+ * Checks if a specific key is currently pressed
+ * @param {string} key - The key to check
+ * @returns {boolean} - True if the key is pressed, false otherwise
+ */
+ isDown(key) {
+ return pressedKeys[key.toUpperCase()] || false; // Return pressed status or false
+ },
+
+ /**
+ * Resets all key states and visuals
+ */
+ reset() {
+ resetKeys(); // Call reset function
+ }
+ };
+})();
\ No newline at end of file
diff --git a/js/koopa.js b/public/js/koopa.js
similarity index 100%
rename from js/koopa.js
rename to public/js/koopa.js
diff --git a/js/levels/11.js b/public/js/levels/11.js
similarity index 96%
rename from js/levels/11.js
rename to public/js/levels/11.js
index a8192e8..16fb993 100644
--- a/js/levels/11.js
+++ b/public/js/levels/11.js
@@ -202,18 +202,25 @@ var oneone = Mario.oneone = function() {
level.putGoomba(82, 4);
level.putGoomba(84, 4);
level.putGoomba(100, 12);
- level.putGoomba(102, 12);
+ level.putGoomba(108, 12);
level.putGoomba(114, 12);
- level.putGoomba(115, 12);
+ level.putGoomba(120, 12);
level.putGoomba(122, 12);
- level.putGoomba(123, 12);
+ level.putGoomba(128, 12);
level.putGoomba(125, 12);
- level.putGoomba(126, 12);
+ level.putGoomba(133, 12);
level.putGoomba(170, 12);
- level.putGoomba(172, 12);
+ level.putGoomba(178, 12);
level.putKoopa(35, 11);
music.underground.pause();
// music.overworld.currentTime = 0;
- music.overworld.play();
+ canvas.addEventListener('touchstart', function() {
+ music.overworld.play();
+ });
+
+ canvas.addEventListener('mousedown', function() {
+ music.overworld.play();
+ });
+
};
diff --git a/js/levels/11tunnel.js b/public/js/levels/11tunnel.js
similarity index 100%
rename from js/levels/11tunnel.js
rename to public/js/levels/11tunnel.js
diff --git a/js/levels/level.js b/public/js/levels/level.js
similarity index 100%
rename from js/levels/level.js
rename to public/js/levels/level.js
diff --git a/public/js/mushroom.js b/public/js/mushroom.js
new file mode 100644
index 0000000..45229b6
--- /dev/null
+++ b/public/js/mushroom.js
@@ -0,0 +1,154 @@
+(function() {
+ // Ensure Mario namespace exists
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Mushroom entity in the game.
+ * @param {Array} pos - The initial position of the mushroom in [x, y] format.
+ */
+ var Mushroom = Mario.Mushroom = function(pos) {
+ this.spawning = false; // Indicates if the mushroom is currently spawning
+ this.waiting = 0; // Time to wait before updating
+
+ // Call the parent Entity constructor
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: level.superShroomSprite, // The mushroom's sprite
+ hitbox: [0, 0, 16, 16] // Hitbox dimensions [offsetX, offsetY, width, height]
+ });
+ };
+
+ // Inherit from Mario.Entity
+ Mario.Util.inherits(Mushroom, Mario.Entity);
+
+ /**
+ * Renders the mushroom on the given canvas context.
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
+ * @param {number} vX - The horizontal velocity for rendering.
+ * @param {number} vY - The vertical velocity for rendering.
+ */
+ Mushroom.prototype.render = function(ctx, vX, vY) {
+ if (this.spawning > 1) return; // Don't render if it's still spawning
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ };
+
+ /**
+ * Spawns the mushroom, possibly transforming it into a Fireflower.
+ */
+ Mushroom.prototype.spawn = function() {
+ if (player.power > 0) {
+ // Replace the mushroom with a fire flower
+ var ff = new Mario.Fireflower(this.pos);
+ ff.spawn();
+ return;
+ }
+ sounds.itemAppear.play(); // Play the item appearance sound
+ this.idx = level.items.length;
+ level.items.push(this); // Add mushroom to the level items
+ this.spawning = 12; // Set spawning duration
+ this.targetpos = [this.pos[0], this.pos[1] - 16]; // Target position for spawning
+ };
+
+ /**
+ * Updates the mushroom's position and state based on the elapsed time.
+ * @param {number} dt - The time delta since the last update.
+ */
+ Mushroom.prototype.update = function(dt) {
+ if (this.spawning > 1) {
+ this.spawning -= 1; // Countdown spawning timer
+ if (this.spawning === 1) this.vel[1] = -0.5; // Adjust vertical velocity
+ return;
+ }
+
+ if (this.spawning) {
+ if (this.pos[1] <= this.targetpos[1]) {
+ this.pos[1] = this.targetpos[1]; // Snap to target position
+ this.vel[1] = 0;
+ this.waiting = 5; // Set waiting time
+ this.spawning = 0; // Reset spawning state
+ this.vel[0] = 1; // Initialize horizontal velocity
+ }
+ } else {
+ this.acc[1] = 0.2; // Apply gravity when not spawning
+ }
+
+ if (this.waiting) {
+ this.waiting -= 1; // Countdown waiting timer
+ } else {
+ // Update position based on velocity and acceleration
+ this.vel[1] += this.acc[1];
+ this.pos[0] += this.vel[0];
+ this.pos[1] += this.vel[1];
+ this.sprite.update(dt); // Update sprite animation
+ }
+ };
+
+ /**
+ * Handles collision with walls, reversing horizontal velocity.
+ */
+ Mushroom.prototype.collideWall = function() {
+ this.vel[0] = -this.vel[0]; // Reverse horizontal velocity
+ };
+
+ /**
+ * Checks for collisions with static objects and the player.
+ */
+ Mushroom.prototype.checkCollisions = function() {
+ if (this.spawning) return; // Skip collision check if spawning
+
+ var h = this.pos[1] % 16 === 0 ? 1 : 2; // Height based on vertical position
+ var w = this.pos[0] % 16 === 0 ? 1 : 2; // Width based on horizontal position
+
+ var baseX = Math.floor(this.pos[0] / 16);
+ var baseY = Math.floor(this.pos[1] / 16);
+
+ // Remove mushroom if out of bounds
+ if (baseY + h > 15) {
+ delete level.items[this.idx];
+ return;
+ }
+
+ // Check collisions with static and block objects
+ for (var i = 0; i < h; i++) {
+ for (var j = 0; j < w; j++) {
+ if (level.statics[baseY + i][baseX + j]) {
+ level.statics[baseY + i][baseX + j].isCollideWith(this);
+ }
+ if (level.blocks[baseY + i][baseX + j]) {
+ level.blocks[baseY + i][baseX + j].isCollideWith(this);
+ }
+ }
+ }
+
+ this.isPlayerCollided(); // Check for player collisions
+ };
+
+ /**
+ * Checks for collision with the player.
+ */
+ Mushroom.prototype.isPlayerCollided = function() {
+ var hpos1 = [this.pos[0] + this.hitbox[0], this.pos[1] + this.hitbox[1]];
+ var hpos2 = [player.pos[0] + player.hitbox[0], player.pos[1] + player.hitbox[1]];
+
+ // Check if hitboxes overlap
+ if (!(hpos1[0] > hpos2[0] + player.hitbox[2] || (hpos1[0] + this.hitbox[2] < hpos2[0]))) {
+ if (!(hpos1[1] > hpos2[1] + player.hitbox[3] || (hpos1[1] + this.hitbox[3] < hpos2[1]))) {
+ player.powerUp(this.idx);
+ }
+ }
+ };
+
+
+ Mushroom.prototype.bump = function() {
+ this.vel[1] = -2;
+ }
+
+ // Example usage of the Mushroom module
+ //const mushroom = new Mario.Mushroom([100, 150]); // Create a new mushroom at position (100, 150)
+ //mushroom.spawn(); // Spawn the mushroom
+ // In the game loop, update and render the mushroom:
+ // mushroom.update(deltaTime);
+ // mushroom.render(context, cameraX, cameraY);
+})();
diff --git a/js/outline.txt b/public/js/outline.txt
similarity index 100%
rename from js/outline.txt
rename to public/js/outline.txt
diff --git a/js/pipe.js b/public/js/pipe.js
similarity index 100%
rename from js/pipe.js
rename to public/js/pipe.js
diff --git a/js/player.js b/public/js/player.js
similarity index 54%
rename from js/player.js
rename to public/js/player.js
index b085de1..a8b104e 100644
--- a/js/player.js
+++ b/public/js/player.js
@@ -1,138 +1,191 @@
-(function() {
- if (typeof Mario === 'undefined')
- window.Mario = {};
-
- var Player = Mario.Player = function(pos) {
- //I know, I know, there are a lot of variables tracking Mario's state.
- //Maybe these can be consolidated some way? We'll see once they're all in.
- this.power = 0;
- this.coins = 0;
- this.powering = [];
- this.bounce = false;
- this.jumping = 0;
- this.canJump = true;
- this.invincibility = 0;
- this.crouching = false;
- this.fireballs = 0;
- this.runheld = false;
- this.noInput = false;
- this.targetPos = [];
-
- Mario.Entity.call(this, {
- pos: pos,
- sprite: new Mario.Sprite('sprites/player.png', [80,32],[16,16],0),
- hitbox: [0,0,16,16]
- });
- };
-
- Mario.Util.inherits(Player, Mario.Entity);
-
- Player.prototype.run = function() {
- this.maxSpeed = 2.5;
- if (this.power == 2 && !this.runheld) {
- this.shoot();
- }
- this.runheld = true;
- }
-
- Player.prototype.shoot = function() {
- if (this.fireballs >= 2) return; //Projectile limit!
- this.fireballs += 1;
- var fb = new Mario.Fireball([this.pos[0]+8,this.pos[1]]); //I hate you, Javascript.
- fb.spawn(this.left);
- this.shooting = 2;
- }
-
- Player.prototype.noRun = function() {
- this.maxSpeed = 1.5;
- this.moveAcc = 0.07;
- this.runheld = false;
- }
-
- Player.prototype.moveRight = function() {
- //we're on the ground
- if (this.vel[1] === 0 && this.standing) {
- if (this.crouching) {
- this.noWalk();
- return;
- }
- this.acc[0] = this.moveAcc;
- this.left = false;
- } else {
- this.acc[0] = this.moveAcc;
- }
- };
-
- Player.prototype.moveLeft = function() {
- if (this.vel[1] === 0 && this.standing) {
- if (this.crouching) {
- this.noWalk();
- return;
- }
- this.acc[0] = -this.moveAcc;
- this.left = true;
- } else {
- this.acc[0] = -this.moveAcc;
- }
- };
-
- Player.prototype.noWalk = function() {
- this.maxSpeed = 0;
- if (this.vel[0] === 0) return;
-
- if (Math.abs(this.vel[0]) <= 0.1) {
- this.vel[0] = 0;
- this.acc[0] = 0;
- }
-
- };
-
- Player.prototype.crouch = function() {
- if (this.power === 0) {
- this.crouching = false;
- return;
- }
-
- if (this.standing) this.crouching = true;
- }
-
- Player.prototype.noCrouch = function() {
- this.crouching = false;
- }
-
- Player.prototype.jump = function() {
- if (this.vel[1] > 0) {
- return;
- }
- if (this.jumping) {
- this.jumping -= 1;
- } else if (this.standing && this.canJump) {
- this.jumping = 20;
- this.canJump = false;
- this.standing = false;
- this.vel[1] = -6;
- if (this.power === 0) {
- sounds.smallJump.currentTime = 0;
- sounds.smallJump.play();
- } else {
- sounds.bigJump.currentTime = 0;
- sounds.bigJump.play();
- }
- }
- };
-
- Player.prototype.noJump = function() {
- this.canJump = true;
- if (this.jumping) {
- if (this.jumping <= 16) {
- this.vel[1] = 0;
- this.jumping = 0;
- } else this.jumping -= 1;
- }
- };
-
- Player.prototype.setAnimation = function() {
- if (this.dying) return;
+// Mario Player Module
+(function () {
+ // Ensure the Mario namespace is defined
+ if (typeof Mario === 'undefined') window.Mario = {};
+
+ /**
+ * Sprite Size Details:
+ * The player sprite uses a 16x16 pixel size for individual frames.
+ * The sprite sheet 'sprites/player.png' contains various animations:
+ * - Small Mario: 16x16 pixels
+ * - Big Mario: 16x16 pixels (scaled)
+ * - Fire Mario: 16x16 pixels (scaled)
+ * This size should be used consistently for any sprite design to ensure proper alignment.
+ */
+
+ /**
+ * Represents the Player in the game
+ * @constructor
+ * @param {Array} pos - Initial position of the player in the format [x, y].
+ */
+ Player = Mario.Player = function (pos) {
+ // Player's state variables
+ this.power = 0; // Current power level (0: small, 1: big, 2: fire)
+ this.coins = 0; // Number of coins collected
+ this.powering = []; // Power-ups currently active
+ this.bounce = false; // Bounce state
+ this.jumping = 0; // Jumping state counter
+ this.canJump = true; // Indicates if the player can jump
+ this.invincibility = 0; // Duration of invincibility
+ this.crouching = false; // Crouching state
+ this.fireballs = 0; // Number of fireballs available
+ this.runheld = false; // Indicates if the run key is held
+ this.noInput = false; // Indicates if the player is not accepting input
+ this.targetPos = []; // Target position for movement
+
+ // Call the parent Entity constructor
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: new Mario.Sprite('sprites/player.png', [80, 32], [16, 16], 0),
+ hitbox: [0, 0, 16, 16]
+ });
+ };
+
+ // Inherit from Mario.Entity
+ Mario.Util.inherits(Player, Mario.Entity);
+
+ /**
+ * Enables running state for the player
+ */
+ Player.prototype.run = function () {
+ this.maxSpeed = 2.5; // Set maximum speed for running
+ if (this.power === 2 && !this.runheld) { // Check for fire power
+ this.shoot(); // Trigger shooting action
+ }
+ this.runheld = true; // Set runheld flag
+ };
+
+ /**
+ * Triggers shooting a fireball
+ */
+ Player.prototype.shoot = function () {
+ if (this.fireballs >= 2) return; // Limit to 2 fireballs
+ this.fireballs += 1; // Increment fireball count
+ var fb = new Mario.Fireball([this.pos[0] + 8, this.pos[1]]); // Create a fireball
+ fb.spawn(this.left); // Spawn the fireball
+ this.shooting = 2; // Set shooting state
+ };
+
+ /**
+ * Disables running state for the player
+ */
+ Player.prototype.noRun = function () {
+ this.maxSpeed = 1.5; // Set maximum speed for normal movement
+ this.moveAcc = 0.07; // Set movement acceleration
+ this.runheld = false; // Reset runheld flag
+ };
+
+ /**
+ * Moves the player to the right
+ */
+ Player.prototype.moveRight = function () {
+ // Check if on the ground
+ if (this.vel[1] === 0 && this.standing) {
+ if (this.crouching) { // If crouching, disable walking
+ this.noWalk();
+ return;
+ }
+ this.acc[0] = this.moveAcc; // Set acceleration for right movement
+ this.left = false; // Indicate direction
+ } else {
+ this.acc[0] = this.moveAcc; // Maintain right acceleration
+ }
+ };
+
+ /**
+ * Moves the player to the left
+ */
+ Player.prototype.moveLeft = function () {
+ if (this.vel[1] === 0 && this.standing) {
+ if (this.crouching) { // If crouching, disable walking
+ this.noWalk();
+ return;
+ }
+ this.acc[0] = -this.moveAcc; // Set acceleration for left movement
+ this.left = true; // Indicate direction
+ } else {
+ this.acc[0] = -this.moveAcc; // Maintain left acceleration
+ }
+ };
+
+ /**
+ * Stops player movement when not walking
+ */
+ Player.prototype.noWalk = function () {
+ this.maxSpeed = 0; // Set max speed to 0
+ if (this.vel[0] === 0) return; // Exit if already stopped
+
+ // Slow down the player
+ if (Math.abs(this.vel[0]) <= 0.1) {
+ this.vel[0] = 0; // Stop horizontal velocity
+ this.acc[0] = 0; // Reset acceleration
+ }
+ };
+
+ /**
+ * Makes the player crouch
+ */
+ Player.prototype.crouch = function () {
+ if (this.power === 0) { // If no power, cannot crouch
+ this.crouching = false;
+ return;
+ }
+ if (this.standing) this.crouching = true; // Crouch if standing
+ };
+
+ /**
+ * Stops the crouching state
+ */
+ Player.prototype.noCrouch = function () {
+ this.crouching = false; // Reset crouching state
+ };
+
+ /**
+ * Executes the jump action
+ */
+ Player.prototype.jump = function () {
+ if (this.vel[1] > 0) { // Prevent jumping while falling
+ return;
+ }
+ if (this.jumping) {
+ this.jumping -= 1; // Reduce jump counter if already jumping
+ } else if (this.standing && this.canJump) {
+ this.jumping = 20; // Set jump state
+ this.canJump = false; // Prevent further jumps
+ this.standing = false; // Mark player as not standing
+ this.vel[1] = -6; // Set upward velocity
+
+ // Play jump sound based on power level
+ if (this.power === 0) {
+ sounds.smallJump.currentTime = 0; // Reset sound
+ sounds.smallJump.play(); // Play small jump sound
+ } else {
+ sounds.bigJump.currentTime = 0; // Reset sound
+ sounds.bigJump.play(); // Play big jump sound
+ }
+ }
+ };
+
+ /**
+ * Executes the actions needed when the jump is released
+ */
+ Player.prototype.noJump = function () {
+ this.canJump = true; // Allow jumping again
+ if (this.jumping) {
+ if (this.jumping <= 16) {
+ this.vel[1] = 0; // Stop upward velocity
+ this.jumping = 0; // Reset jump counter
+ } else {
+ this.jumping -= 1; // Decrease jump counter
+ }
+ }
+ };
+
+ /**
+ * Updates the player's animation based on the current state
+ */
+ Player.prototype.setAnimation = function () {
+ if (this.dying) return; // Exit if player is dying
if (this.starTime) {
var index;
diff --git a/js/prop.js b/public/js/prop.js
similarity index 100%
rename from js/prop.js
rename to public/js/prop.js
diff --git a/js/resources.js b/public/js/resources.js
similarity index 100%
rename from js/resources.js
rename to public/js/resources.js
diff --git a/js/rubble.js b/public/js/rubble.js
similarity index 100%
rename from js/rubble.js
rename to public/js/rubble.js
diff --git a/public/js/sprite.js b/public/js/sprite.js
new file mode 100644
index 0000000..7d2b601
--- /dev/null
+++ b/public/js/sprite.js
@@ -0,0 +1,86 @@
+(function() {
+ // Ensure the Mario namespace exists
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ /**
+ * Represents a Sprite in the game.
+ * @param {string} img - The image source for the sprite.
+ * @param {Array} pos - The position of the sprite in [x, y] format.
+ * @param {Array} size - The size of the sprite in [width, height] format.
+ * @param {number} speed - The speed at which the sprite frames should change.
+ * @param {Array} frames - An array of frame indices for the sprite animation.
+ * @param {boolean} once - Indicates if the animation should only play once.
+ */
+ var Sprite = Mario.Sprite = function(img, pos, size, speed, frames, once) {
+ this.pos = pos; // Position of the sprite
+ this.size = size; // Size of the sprite
+ this.speed = speed; // Frame change speed
+ this._index = 0; // Current frame index
+ this.img = img; // Image source
+ this.once = once; // Play once flag
+ this.frames = frames; // Animation frames
+ this.lastUpdated = null; // Last update timestamp
+ this.done = false; // Indicates if animation is finished
+ };
+
+ /**
+ * Updates the current frame index based on the elapsed time.
+ * @param {number} dt - Delta time since the last update.
+ * @param {number} gameTime - Current game time for synchronization.
+ */
+ Sprite.prototype.update = function(dt, gameTime) {
+ if (gameTime && gameTime === this.lastUpdated) return; // Skip if already updated
+
+ this._index += this.speed * dt; // Increment frame index by speed
+ if (gameTime) this.lastUpdated = gameTime; // Store last updated time
+ };
+
+ /**
+ * Sets the current frame index manually.
+ * @param {number} frame - The frame index to set.
+ */
+ Sprite.prototype.setFrame = function(frame) {
+ this._index = frame; // Set the specified frame index
+ };
+
+ /**
+ * Renders the current sprite frame on the provided canvas context.
+ * @param {CanvasRenderingContext2D} ctx - The canvas context to render to.
+ * @param {number} posx - The x position to render the sprite.
+ * @param {number} posy - The y position to render the sprite.
+ * @param {number} vX - The horizontal velocity (for camera movement).
+ * @param {number} vY - The vertical velocity (for camera movement).
+ */
+ Sprite.prototype.render = function(ctx, posx, posy, vX, vY) {
+ var frame;
+
+ // Determine the current frame based on speed and index
+ if (this.speed > 0) {
+ var max = this.frames.length;
+ var idx = Math.floor(this._index);
+ frame = this.frames[idx % max]; // Cycle through frames
+
+ if (this.once && idx >= max) {
+ this.done = true; // Mark animation as finished if playing once
+ return;
+ }
+ } else {
+ frame = 0; // Default frame if speed is zero
+ }
+
+ // Calculate the rendering position
+ var x = this.pos[0] + frame * this.size[0] + (1 / 3);
+ var y = this.pos[1] + (1 / 3);
+
+ // Render the sprite on the canvas
+ ctx.drawImage(
+ resources.get(this.img),
+ x, y,
+ this.size[0] - (2 / 3), this.size[1] - (2 / 3),
+ Math.round(posx - vX), Math.round(posy - vY),
+ this.size[0], this.size[1]
+ );
+ };
+})();
\ No newline at end of file
diff --git a/js/star.js b/public/js/star.js
similarity index 100%
rename from js/star.js
rename to public/js/star.js
diff --git a/js/util.js b/public/js/util.js
similarity index 100%
rename from js/util.js
rename to public/js/util.js
diff --git a/sounds/aboveground_bgm.ogg b/public/sounds/aboveground_bgm.ogg
similarity index 100%
rename from sounds/aboveground_bgm.ogg
rename to public/sounds/aboveground_bgm.ogg
diff --git a/sounds/breakblock.wav b/public/sounds/breakblock.wav
similarity index 100%
rename from sounds/breakblock.wav
rename to public/sounds/breakblock.wav
diff --git a/sounds/bump.wav b/public/sounds/bump.wav
similarity index 100%
rename from sounds/bump.wav
rename to public/sounds/bump.wav
diff --git a/sounds/coin.wav b/public/sounds/coin.wav
similarity index 100%
rename from sounds/coin.wav
rename to public/sounds/coin.wav
diff --git a/sounds/fireball.wav b/public/sounds/fireball.wav
similarity index 100%
rename from sounds/fireball.wav
rename to public/sounds/fireball.wav
diff --git a/sounds/flagpole.wav b/public/sounds/flagpole.wav
similarity index 100%
rename from sounds/flagpole.wav
rename to public/sounds/flagpole.wav
diff --git a/sounds/itemAppear.wav b/public/sounds/itemAppear.wav
similarity index 100%
rename from sounds/itemAppear.wav
rename to public/sounds/itemAppear.wav
diff --git a/sounds/jump-small.wav b/public/sounds/jump-small.wav
similarity index 100%
rename from sounds/jump-small.wav
rename to public/sounds/jump-small.wav
diff --git a/sounds/jump-super.wav b/public/sounds/jump-super.wav
similarity index 100%
rename from sounds/jump-super.wav
rename to public/sounds/jump-super.wav
diff --git a/sounds/kick.wav b/public/sounds/kick.wav
similarity index 100%
rename from sounds/kick.wav
rename to public/sounds/kick.wav
diff --git a/sounds/mariodie.wav b/public/sounds/mariodie.wav
similarity index 100%
rename from sounds/mariodie.wav
rename to public/sounds/mariodie.wav
diff --git a/sounds/pipe.wav b/public/sounds/pipe.wav
similarity index 100%
rename from sounds/pipe.wav
rename to public/sounds/pipe.wav
diff --git a/sounds/powerup.wav b/public/sounds/powerup.wav
similarity index 100%
rename from sounds/powerup.wav
rename to public/sounds/powerup.wav
diff --git a/sounds/stage_clear.wav b/public/sounds/stage_clear.wav
similarity index 100%
rename from sounds/stage_clear.wav
rename to public/sounds/stage_clear.wav
diff --git a/sounds/stomp.wav b/public/sounds/stomp.wav
similarity index 100%
rename from sounds/stomp.wav
rename to public/sounds/stomp.wav
diff --git a/sounds/underground_bgm.ogg b/public/sounds/underground_bgm.ogg
similarity index 100%
rename from sounds/underground_bgm.ogg
rename to public/sounds/underground_bgm.ogg
diff --git a/sprites/1-1 reference.png b/public/sprites/1-1 reference.png
similarity index 100%
rename from sprites/1-1 reference.png
rename to public/sprites/1-1 reference.png
diff --git a/sprites/enemy.png b/public/sprites/enemy.png
similarity index 100%
rename from sprites/enemy.png
rename to public/sprites/enemy.png
diff --git a/sprites/enemyr.png b/public/sprites/enemyr.png
similarity index 100%
rename from sprites/enemyr.png
rename to public/sprites/enemyr.png
diff --git a/public/sprites/items.png b/public/sprites/items.png
new file mode 100644
index 0000000..be34358
Binary files /dev/null and b/public/sprites/items.png differ
diff --git a/public/sprites/player.png b/public/sprites/player.png
new file mode 100644
index 0000000..376326b
Binary files /dev/null and b/public/sprites/player.png differ
diff --git a/public/sprites/playerl.png b/public/sprites/playerl.png
new file mode 100644
index 0000000..85ca94f
Binary files /dev/null and b/public/sprites/playerl.png differ
diff --git a/public/sprites/tiles.png b/public/sprites/tiles.png
new file mode 100644
index 0000000..5f255a7
Binary files /dev/null and b/public/sprites/tiles.png differ
diff --git a/sprites/items.png b/sprites/items.png
deleted file mode 100644
index 387219d..0000000
Binary files a/sprites/items.png and /dev/null differ
diff --git a/sprites/player.png b/sprites/player.png
deleted file mode 100644
index b8ee739..0000000
Binary files a/sprites/player.png and /dev/null differ
diff --git a/sprites/playerl.png b/sprites/playerl.png
deleted file mode 100644
index 077b502..0000000
Binary files a/sprites/playerl.png and /dev/null differ
diff --git a/sprites/tiles.png b/sprites/tiles.png
deleted file mode 100644
index 31d7a8f..0000000
Binary files a/sprites/tiles.png and /dev/null differ
diff --git a/todo.txt b/todo.txt
deleted file mode 100644
index 2d03379..0000000
--- a/todo.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-to really finish 1-1
-timer
- so we can have the sped up music, it's cute
- how fast does the Mario timer go, anyway?
-HUD
- so we can see the dang timer
-
-README: Explain more about the engine
-README: Organize better
-README: Add a screenshot with a link
-Add live links to LinkedIn, Github, and Portfolio site
diff --git a/views/index.ejs b/views/index.ejs
new file mode 100644
index 0000000..6d4e521
--- /dev/null
+++ b/views/index.ejs
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+ <%= metaTags.title %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file