From a47855dad04659f1edb732060cdebeb9bfa03d98 Mon Sep 17 00:00:00 2001
From: BadNintendo <58354850+BadNintendo@users.noreply.github.com>
Date: Sat, 20 May 2023 02:22:17 -0500
Subject: [PATCH 01/15] Uploaded Node Express based version with Socket.IO
I recently came across your work and was truly impressed by your expertise. I wanted to reach out to discuss a Node Express based version that utilizes Socket.IO, allowing an unlimited number of players to join and engage in chat functionality above the sprite.
After becoming bored with the existing implementation, I took the liberty of updating the overall look and feel, as well as making significant enhancements to the code. Although I managed to add a few features, I must admit that they did not quite align with my original vision for the project.
Nevertheless, I genuinely admire your skills and creativity, which is why I am reaching out to you. I would be thrilled to collaborate with you on further stages of development or even explore an entirely unrelated version based on this updated iteration. If you happen to come across this message and find it intriguing, I would be delighted to discuss the possibilities further.
---
app.js | 96 +++++++
public/css/game.css | 32 +++
public/index.html | 83 ++++++
public/js/bcoin.js | 45 +++
public/js/block.js | 81 ++++++
public/js/coin.js | 47 ++++
public/js/entity.js | 34 +++
public/js/fireball.js | 126 +++++++++
public/js/fireflower.js | 74 +++++
public/js/flag.js | 47 ++++
public/js/floor.js | 56 ++++
public/js/game-orginal.js | 241 ++++++++++++++++
public/js/game.js | 328 ++++++++++++++++++++++
public/js/goomba.js | 129 +++++++++
public/js/input.js | 116 ++++++++
public/js/koopa.js | 211 ++++++++++++++
public/js/levels/11.js | 226 +++++++++++++++
public/js/levels/11tunnel.js | 65 +++++
public/js/levels/level.js | 218 +++++++++++++++
public/js/mushroom.js | 118 ++++++++
public/js/outline.txt | 45 +++
public/js/pipe.js | 160 +++++++++++
public/js/player.js | 446 ++++++++++++++++++++++++++++++
public/js/prop.js | 15 +
public/js/resources.js | 62 +++++
public/js/rubble.js | 55 ++++
public/js/sprite.js | 47 ++++
public/js/star.js | 116 ++++++++
public/js/util.js | 14 +
public/sounds/aboveground_bgm.ogg | Bin 0 -> 2486172 bytes
public/sounds/breakblock.wav | Bin 0 -> 25090 bytes
public/sounds/bump.wav | Bin 0 -> 10426 bytes
public/sounds/coin.wav | Bin 0 -> 42654 bytes
public/sounds/fireball.wav | Bin 0 -> 6756 bytes
public/sounds/flagpole.wav | Bin 0 -> 52622 bytes
public/sounds/itemAppear.wav | Bin 0 -> 26728 bytes
public/sounds/jump-small.wav | Bin 0 -> 26982 bytes
public/sounds/jump-super.wav | Bin 0 -> 26712 bytes
public/sounds/kick.wav | Bin 0 -> 8948 bytes
public/sounds/mariodie.wav | Bin 0 -> 120570 bytes
public/sounds/pipe.wav | Bin 0 -> 35538 bytes
public/sounds/powerup.wav | Bin 0 -> 44580 bytes
public/sounds/stage_clear.wav | Bin 0 -> 249748 bytes
public/sounds/stomp.wav | Bin 0 -> 13070 bytes
public/sounds/underground_bgm.ogg | Bin 0 -> 363661 bytes
public/sprites/1-1 reference.png | Bin 0 -> 44619 bytes
public/sprites/enemy.png | Bin 0 -> 13494 bytes
public/sprites/enemyr.png | Bin 0 -> 13638 bytes
public/sprites/items.png | Bin 0 -> 51269 bytes
public/sprites/player.png | Bin 0 -> 75324 bytes
public/sprites/playerl.png | Bin 0 -> 75558 bytes
public/sprites/tiles.png | Bin 0 -> 36402 bytes
52 files changed, 3333 insertions(+)
create mode 100644 app.js
create mode 100644 public/css/game.css
create mode 100644 public/index.html
create mode 100644 public/js/bcoin.js
create mode 100644 public/js/block.js
create mode 100644 public/js/coin.js
create mode 100644 public/js/entity.js
create mode 100644 public/js/fireball.js
create mode 100644 public/js/fireflower.js
create mode 100644 public/js/flag.js
create mode 100644 public/js/floor.js
create mode 100644 public/js/game-orginal.js
create mode 100644 public/js/game.js
create mode 100644 public/js/goomba.js
create mode 100644 public/js/input.js
create mode 100644 public/js/koopa.js
create mode 100644 public/js/levels/11.js
create mode 100644 public/js/levels/11tunnel.js
create mode 100644 public/js/levels/level.js
create mode 100644 public/js/mushroom.js
create mode 100644 public/js/outline.txt
create mode 100644 public/js/pipe.js
create mode 100644 public/js/player.js
create mode 100644 public/js/prop.js
create mode 100644 public/js/resources.js
create mode 100644 public/js/rubble.js
create mode 100644 public/js/sprite.js
create mode 100644 public/js/star.js
create mode 100644 public/js/util.js
create mode 100644 public/sounds/aboveground_bgm.ogg
create mode 100644 public/sounds/breakblock.wav
create mode 100644 public/sounds/bump.wav
create mode 100644 public/sounds/coin.wav
create mode 100644 public/sounds/fireball.wav
create mode 100644 public/sounds/flagpole.wav
create mode 100644 public/sounds/itemAppear.wav
create mode 100644 public/sounds/jump-small.wav
create mode 100644 public/sounds/jump-super.wav
create mode 100644 public/sounds/kick.wav
create mode 100644 public/sounds/mariodie.wav
create mode 100644 public/sounds/pipe.wav
create mode 100644 public/sounds/powerup.wav
create mode 100644 public/sounds/stage_clear.wav
create mode 100644 public/sounds/stomp.wav
create mode 100644 public/sounds/underground_bgm.ogg
create mode 100644 public/sprites/1-1 reference.png
create mode 100644 public/sprites/enemy.png
create mode 100644 public/sprites/enemyr.png
create mode 100644 public/sprites/items.png
create mode 100644 public/sprites/player.png
create mode 100644 public/sprites/playerl.png
create mode 100644 public/sprites/tiles.png
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..412476a
--- /dev/null
+++ b/app.js
@@ -0,0 +1,96 @@
+// server.js
+const express = require('express');
+const http = require('http');
+const https = require('https');
+const socketIo = require('socket.io');
+const fs = require('fs');
+const path = require('path');
+
+// Initialize app
+const app = express();
+
+// Serve static files from 'public' directory
+app.use(express.static(path.join(__dirname, 'public')));
+
+// Create HTTP and HTTPS servers
+const httpServer = http.createServer(app);
+const httpsServer = https.createServer({
+ key: fs.readFileSync('./server.key', 'utf8'),
+ cert: fs.readFileSync('./server.crt', 'utf8')
+}, app);
+
+const httpIo = socketIo(httpServer);
+const httpsIo = socketIo(httpsServer);
+
+
+
+// Player state
+const players = {};
+let connectionCount = 0;
+
+// Connection handling
+httpIo.on('connection', handleConnection);
+httpsIo.on('connection', handleConnection);
+
+// Start servers
+httpServer.listen(80, () => console.log('HTTP Server listening on port 80'));
+httpsServer.listen(443, () => console.log('HTTPS Server listening on port 443'));
+
+// Handle socket connections
+function handleConnection(socket) {
+ console.log(`Player ${socket.id} connected`);
+
+ const currentPlayer = initializePlayer(socket);
+ players[socket.id] = currentPlayer;
+
+ emitPlayerData(socket);
+
+ socket.on('playerMovement', handlePlayerMovement);
+ socket.on('playerChat', handlePlayerChat);
+ socket.on('disconnect', handlePlayerDisconnect);
+}
+
+// Initialize a new player
+function initializePlayer(socket) {
+ return {
+ playerId: socket.id,
+ connectionNum: ++connectionCount,
+ gameData: { pos: [0, 0] },
+ chatMessage: '',
+ };
+}
+
+// Emit player data to clients
+function emitPlayerData(socket) {
+ socket.emit('currentPlayer', players[socket.id]);
+
+ const existingPlayers = Object.values(players);
+ socket.emit('existingPlayers', { existingPlayers });
+
+ socket.broadcast.emit('newPlayer', players[socket.id]);
+}
+
+// Handle player movement events
+function handlePlayerMovement(data) {
+ players[this.id].gameData.pos = data.pos;
+ this.broadcast.emit('playerMoved', { player: players[this.id] });
+}
+
+// Handle player chat events
+function handlePlayerChat(message) {
+ players[this.id].chatMessage = message;
+
+ setTimeout(() => {
+ players[this.id].chatMessage = '';
+ this.emit('chatUpdate', { playerId: this.id, chatMessage: '' });
+ }, 30000);
+
+ this.broadcast.emit('chatUpdate', { playerId: this.id, chatMessage: message });
+}
+
+// Handle player disconnect events
+function handlePlayerDisconnect() {
+ console.log(`Player ${this.id} disconnected`);
+ delete players[this.id];
+ this.broadcast.emit('playerDisconnected', this.id);
+}
\ No newline at end of file
diff --git a/public/css/game.css b/public/css/game.css
new file mode 100644
index 0000000..7dced92
--- /dev/null
+++ b/public/css/game.css
@@ -0,0 +1,32 @@
+html, body, ul, li {
+ margin: 0;
+ border: 0;
+ padding: 0;
+}
+
+canvas {
+ display: block;
+ width: 762;
+ margin: 0 auto;
+ background-color: blue;
+}
+
+p {
+ text-align: center;
+}
+
+body {
+ overflow: hidden;
+ height: 100%;
+}
+
+html {
+ overflow: hidden;
+ height: 100%;
+}
+
+.info {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..8685871
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,83 @@
+
+
+
+
+ Super Mario Bros
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/bcoin.js b/public/js/bcoin.js
new file mode 100644
index 0000000..34fe1cb
--- /dev/null
+++ b/public/js/bcoin.js
@@ -0,0 +1,45 @@
+(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/public/js/block.js b/public/js/block.js
new file mode 100644
index 0000000..e695f04
--- /dev/null
+++ b/public/js/block.js
@@ -0,0 +1,81 @@
+(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/public/js/coin.js b/public/js/coin.js
new file mode 100644
index 0000000..28f1758
--- /dev/null
+++ b/public/js/coin.js
@@ -0,0 +1,47 @@
+(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/public/js/entity.js b/public/js/entity.js
new file mode 100644
index 0000000..2f102b9
--- /dev/null
+++ b/public/js/entity.js
@@ -0,0 +1,34 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ var Entity = Mario.Entity = function(options) {
+ this.vel = [0,0];
+ this.acc = [0,0];
+ this.standing = true;
+ this.pos = options.pos;
+ this.sprite = options.sprite;
+ this.hitbox = options.hitbox;
+ this.left = false;
+ }
+
+ Entity.prototype.render = function(ctx, vX, vY) {
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY)
+ }
+
+ Entity.prototype.collideWall = function(wall) {
+ //the wall will always be a 16x16 block with hitbox = [0,0,16,16].
+ if (this.pos[0] > wall.pos[0]) {
+ //from the right
+ this.pos[0] = wall.pos[0] + wall.hitbox[2] - this.hitbox[0];
+ this.vel[0] = Math.max(0, this.vel[0]);
+ this.acc[0] = Math.max(0, this.acc[0]);
+ } else {
+ this.pos[0] = wall.pos[0] + wall.hitbox[0] - this.hitbox[2] - this.hitbox[0];
+ this.vel[0] = Math.min(0, this.vel[0]);
+ this.acc[0] = Math.min(0, this.acc[0]);
+ }
+ }
+
+ Entity.prototype.bump = function() {;}
+})();
diff --git a/public/js/fireball.js b/public/js/fireball.js
new file mode 100644
index 0000000..f49798b
--- /dev/null
+++ b/public/js/fireball.js
@@ -0,0 +1,126 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ var Fireball = Mario.Fireball = function(pos) {
+ this.hit = 0;
+ this.standing = false;
+
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: new Mario.Sprite('sprites/items.png', [96, 144], [8,8], 5, [0,1,2,3]),
+ hitbox: [0,0,8,8]
+ });
+ }
+
+ Mario.Util.inherits(Fireball, Mario.Entity);
+
+ Fireball.prototype.spawn = function(left) {
+ sounds.fireball.currentTime = 0;
+ sounds.fireball.play();
+ if (fireballs[0]) {
+ this.idx = 1;
+ fireballs[1] = this;
+ } else {
+ this.idx = 0;
+ fireballs[0] = this;
+ }
+ this.vel[0] = (left ? -5 : 5);
+ this.standing = false;
+ this.vel[1] = 0;
+ }
+
+ Fireball.prototype.render = function(ctx, vX, vY) {
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ }
+
+ Fireball.prototype.update = function(dt) {
+ if (this.hit == 1) {
+ this.sprite.pos = [96, 160];
+ this.sprite.size = [16,16];
+ this.sprite.frames = [0,1,2];
+ this.sprite.speed = 8;
+ this.hit += 1;
+ return;
+ } else if (this.hit == 5) {
+ delete fireballs[this.idx];
+ player.fireballs -= 1;
+ return;
+ } else if (this.hit) {
+ this.hit += 1;
+ return;
+ }
+
+ //In retrospect, the way collision is being handled is RIDICULOUS
+ //but I don't have to use some horrible kludge for this.
+ if (this.standing) {
+ this.standing = false;
+ this.vel[1] = -4;
+ }
+
+ this.acc[1] = 0.5;
+
+ this.vel[1] += this.acc[1];
+ this.pos[0] += this.vel[0];
+ this.pos[1] += this.vel[1];
+ if (this.pos[0] < vX || this.pos[0] > vX + 256) {
+ this.hit = 1;
+ }
+ this.sprite.update(dt);
+ }
+
+ Fireball.prototype.collideWall = function() {
+ if (!this.hit) this.hit = 1;
+ }
+
+ Fireball.prototype.checkCollisions = function() {
+ if (this.hit) return;
+ var h = this.pos[1] % 16 < 8 ? 1 : 2;
+ var w = this.pos[0] % 16 < 8 ? 1 : 2;
+
+ var baseX = Math.floor(this.pos[0] / 16);
+ var baseY = Math.floor(this.pos[1] / 16);
+
+ if (baseY + h > 15) {
+ delete fireballs[this.idx];
+ player.fireballs -= 1;
+ 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.flipping || enemy.pos[0] - vX > 336){ //stop checking once we get to far away dudes.
+ return;
+ } else {
+ that.isCollideWith(enemy);
+ }
+ });
+ }
+
+ Fireball.prototype.isCollideWith = function(ent) {
+ //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]))) {
+ this.hit = 1;
+ ent.bump();
+ }
+ }
+ };
+
+ Fireball.prototype.bump = function() {;}
+})();
diff --git a/public/js/fireflower.js b/public/js/fireflower.js
new file mode 100644
index 0000000..b66056d
--- /dev/null
+++ b/public/js/fireflower.js
@@ -0,0 +1,74 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ var Fireflower = Mario.Fireflower = function(pos) {
+ this.spawning = false;
+ this.waiting = 0;
+
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: level.fireFlowerSprite,
+ hitbox: [0,0,16,16]
+ });
+ }
+
+ Mario.Util.inherits(Fireflower, Mario.Entity);
+
+ Fireflower.prototype.render = function(ctx, vX, vY) {
+ if (this.spawning > 1) return;
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ }
+
+ Fireflower.prototype.spawn = function() {
+ 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;
+ }
+
+ Fireflower.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.spawning = 0;
+ }
+ }
+
+ this.vel[1] += this.acc[1];
+ this.pos[0] += this.vel[0];
+ this.pos[1] += this.vel[1];
+ this.sprite.update(dt);
+ }
+
+ Fireflower.prototype.checkCollisions = function() {
+ if (this.spawning) {return;}
+ this.isPlayerCollided();
+ }
+
+ Fireflower.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);
+ }
+ }
+ }
+
+ //This should never be called, but just in case.
+ Fireflower.prototype.bump = function() {;}
+
+})();
diff --git a/public/js/flag.js b/public/js/flag.js
new file mode 100644
index 0000000..f71bb5d
--- /dev/null
+++ b/public/js/flag.js
@@ -0,0 +1,47 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ Flag = Mario.Flag = function(pos) {
+ //afaik flags always have the same height and Y-position
+ this.pos = [pos, 49];
+ this.hitbox = [0,0,0,0];
+ this.vel = [0,0];
+ this.acc = [0,0];
+ }
+
+ Flag.prototype.collideWall = function() {;
+ }
+
+ Flag.prototype.update = function(dt){
+ if (!this.done && this.pos[1] >= 170) {
+ this.vel = [0,0];
+ this.pos[1] = 170;
+ player.exit();
+ this.done = true;
+ }
+ this.pos[1] += this.vel[1];
+ }
+
+ Flag.prototype.checkCollisions = function() {
+ this.isPlayerCollided();
+ }
+
+ Flag.prototype.isPlayerCollided = function() {
+ if (this.hit) return;
+ if (player.pos[0] + 8 >= this.pos[0]) {
+ music.overworld.pause();
+ sounds.flagpole.play();
+ setTimeout(function() {
+ music.clear.play();
+ }, 2000);
+ this.hit = true;
+ player.flag();
+ this.vel = [0, 2];
+ }
+ }
+
+ Flag.prototype.render = function() {
+ level.flagpoleSprites[2].render(ctx, this.pos[0]-8, this.pos[1], vX, vY);
+ }
+})();
diff --git a/public/js/floor.js b/public/js/floor.js
new file mode 100644
index 0000000..016d678
--- /dev/null
+++ b/public/js/floor.js
@@ -0,0 +1,56 @@
+(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/public/js/game-orginal.js b/public/js/game-orginal.js
new file mode 100644
index 0000000..2ad523a
--- /dev/null
+++ b/public/js/game-orginal.js
@@ -0,0 +1,241 @@
+var requestAnimFrame = (function(){
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback){
+ window.setTimeout(callback, 1000 / 60);
+ };
+})();
+
+//create the canvas
+var canvas = document.createElement("canvas");
+var ctx = canvas.getContext('2d');
+var updateables = [];
+var fireballs = [];
+var player = new Mario.Player([0,0]);
+
+//we might have to get the size and calculate the scaling
+//but this method should let us make it however big.
+//Cool!
+//TODO: Automatically scale the game to work and look good on widescreen.
+//TODO: fiddling with scaled sprites looks BETTER, but not perfect. Hmm.
+canvas.width = 762;
+canvas.height = 720;
+ctx.scale(3,3);
+document.body.appendChild(canvas);
+
+//viewport
+var vX = 0,
+ vY = 0,
+ vWidth = 256,
+ vHeight = 240;
+
+//load our images
+resources.load([
+ 'sprites/player.png',
+ 'sprites/enemy.png',
+ 'sprites/tiles.png',
+ 'sprites/playerl.png',
+ 'sprites/items.png',
+ 'sprites/enemyr.png',
+]);
+
+resources.onReady(init);
+var level;
+var sounds;
+var music;
+
+//initialize
+var lastTime;
+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();
+ lastTime = Date.now();
+ main();
+}
+
+var gameTime = 0;
+
+//set up the game loop
+function main() {
+ var now = Date.now();
+ var dt = (now - lastTime) / 1000.0;
+
+ update(dt);
+ render();
+
+ lastTime = now;
+ requestAnimFrame(main);
+}
+
+function update(dt) {
+ gameTime += dt;
+
+ handleInput(dt);
+ updateEntities(dt, gameTime);
+
+ checkCollisions();
+}
+
+function handleInput(dt) {
+ if (player.piping || player.dying || player.noInput) return; //don't accept input
+
+ if (input.isDown('RUN')){
+ player.run();
+ } else {
+ player.noRun();
+ }
+ if (input.isDown('JUMP')) {
+ player.jump();
+ } else {
+ //we need this to handle the timing for how long you hold it
+ player.noJump();
+ }
+
+ if (input.isDown('DOWN')) {
+ player.crouch();
+ } else {
+ player.noCrouch();
+ }
+
+ if (input.isDown('LEFT')) { // 'd' or left arrow
+ player.moveLeft();
+ }
+ else if (input.isDown('RIGHT')) { // 'k' or right arrow
+ player.moveRight();
+ } else {
+ player.noWalk();
+ }
+}
+
+//update all the moving stuff
+function updateEntities(dt, gameTime) {
+ player.update(dt, vX);
+ updateables.forEach (function(ent) {
+ ent.update(dt, gameTime);
+ });
+
+ //This should stop the jump when he switches sides on the flag.
+ 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;
+ }
+
+ if (player.powering.length !== 0 || player.dying) { return; }
+ level.items.forEach (function(ent) {
+ ent.update(dt);
+ });
+
+ level.enemies.forEach (function(ent) {
+ ent.update(dt, vX);
+ });
+
+ fireballs.forEach(function(fireball) {
+ fireball.update(dt);
+ });
+ level.pipes.forEach (function(pipe) {
+ pipe.update(dt);
+ });
+}
+
+//scan for collisions
+function checkCollisions() {
+ if (player.powering.length !== 0 || player.dying) { return; }
+ player.checkCollisions();
+
+ //Apparently for each will just skip indices where things were deleted.
+ level.items.forEach(function(item) {
+ item.checkCollisions();
+ });
+ level.enemies.forEach (function(ent) {
+ ent.checkCollisions();
+ });
+ fireballs.forEach(function(fireball){
+ fireball.checkCollisions();
+ });
+ level.pipes.forEach (function(pipe) {
+ pipe.checkCollisions();
+ });
+}
+
+//draw the game!
+function render() {
+ updateables = [];
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = level.background;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ //scenery gets drawn first to get layering right.
+ for(var i = 0; i < 15; i++) {
+ for (var j = Math.floor(vX / 16) - 1; j < Math.floor(vX / 16) + 20; j++){
+ if (level.scenery[i][j]) {
+ renderEntity(level.scenery[i][j]);
+ }
+ }
+ }
+
+ //then items
+ level.items.forEach (function (item) {
+ renderEntity(item);
+ });
+
+ level.enemies.forEach (function(enemy) {
+ renderEntity(enemy);
+ });
+
+
+
+ fireballs.forEach(function(fireball) {
+ renderEntity(fireball);
+ })
+
+ //then we draw every static object.
+ for(var i = 0; i < 15; i++) {
+ for (var j = Math.floor(vX / 16) - 1; j < Math.floor(vX / 16) + 20; j++){
+ if (level.statics[i][j]) {
+ renderEntity(level.statics[i][j]);
+ }
+ if (level.blocks[i][j]) {
+ renderEntity(level.blocks[i][j]);
+ updateables.push(level.blocks[i][j]);
+ }
+ }
+ }
+
+ //then the player
+ if (player.invincibility % 2 === 0) {
+ renderEntity(player);
+ }
+
+ //Mario goes INTO pipes, so naturally they go after.
+ level.pipes.forEach (function(pipe) {
+ renderEntity(pipe);
+ });
+}
+
+function renderEntity(entity) {
+ entity.render(ctx, vX, vY);
+}
diff --git a/public/js/game.js b/public/js/game.js
new file mode 100644
index 0000000..fb1585b
--- /dev/null
+++ b/public/js/game.js
@@ -0,0 +1,328 @@
+let socket = io();
+let otherPlayers = {};
+let chatInput = document.getElementById('chatInput'); // Chat input element
+let chatForm = document.getElementById('chatForm'); // Chat form element
+
+chatForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ player.chatMessage = chatInput.value;
+ socket.emit('playerChat', chatInput.value);
+ chatInput.value = '';
+
+ // Start a timer to clear the player's chat message after 30 seconds
+ setTimeout(() => {
+ player.chatMessage = '';
+ }, 30000);
+});
+
+socket.on('chatUpdate', (data) => {
+ if (player.playerId === data.playerId) {
+ player.chatMessage = data.chatMessage;
+ } else if (otherPlayers[data.playerId]) {
+ otherPlayers[data.playerId].chatMessage = data.chatMessage;
+ }
+});
+
+socket.on('connect', () => {
+ socket.emit('newPlayer', { pos: player.pos });
+});
+
+socket.on('currentPlayer', (data) => {
+ player.playerId = data.playerId;
+ player.title = "Player " + data.connectionNum;
+});
+
+socket.on('newPlayer', (data) => {
+ otherPlayers[data.playerId] = new Mario.Player(data.gameData.pos);
+ otherPlayers[data.playerId].title = "Player " + data.connectionNum;
+});
+
+socket.on('existingPlayers', (data) => {
+ data.existingPlayers.forEach((playerData) => {
+ otherPlayers[playerData.playerId] = new Mario.Player(playerData.gameData.pos);
+ otherPlayers[playerData.playerId].title = "Player " + playerData.connectionNum;
+ });
+});
+
+socket.on('playerDisconnected', (playerId) => {
+ delete otherPlayers[playerId];
+});
+
+socket.on('playerMoved', (data) => {
+ if (otherPlayers[data.player.playerId]) {
+ otherPlayers[data.player.playerId].pos = data.player.gameData.pos;
+ }
+});
+
+
+
+// Add this to your render function
+for (var playerId in otherPlayers) {
+ if (otherPlayers.hasOwnProperty(playerId)) {
+ renderEntity(otherPlayers[playerId]);
+ }
+}
+
+var requestAnimFrame = (function(){
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback){
+ window.setTimeout(callback, 1000 / 60);
+ };
+})();
+
+//create the canvas
+var canvas = document.createElement("canvas");
+var ctx = canvas.getContext('2d');
+var updateables = [];
+var fireballs = [];
+var player = new Mario.Player([0,0]);
+
+//we might have to get the size and calculate the scaling
+//but this method should let us make it however big.
+//Cool!
+//TODO: Automatically scale the game to work and look good on widescreen.
+//TODO: fiddling with scaled sprites looks BETTER, but not perfect. Hmm.
+canvas.width = 762;
+canvas.height = 720;
+ctx.scale(3,3);
+document.body.appendChild(canvas);
+
+//viewport
+var vX = 0,
+ vY = 0,
+ vWidth = 256,
+ vHeight = 240;
+
+//load our images
+resources.load([
+ 'sprites/player.png',
+ 'sprites/enemy.png',
+ 'sprites/tiles.png',
+ 'sprites/playerl.png',
+ 'sprites/items.png',
+ 'sprites/enemyr.png',
+]);
+
+resources.onReady(init);
+var level;
+var sounds;
+var music;
+
+//initialize
+var lastTime;
+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();
+ lastTime = Date.now();
+ main();
+}
+
+var gameTime = 0;
+
+//set up the game loop
+function main() {
+ var now = Date.now();
+ var dt = (now - lastTime) / 1000.0;
+
+ update(dt);
+ render();
+
+ lastTime = now;
+ requestAnimFrame(main);
+}
+
+function update(dt) {
+ gameTime += dt;
+
+ handleInput(dt);
+ updateEntities(dt, gameTime);
+
+ checkCollisions();
+ socket.emit('playerMovement', { pos: player.pos });
+}
+
+function handleInput(dt) {
+ if (player.piping || player.dying || player.noInput) return; //don't accept input
+
+ if (input.isDown('RUN')){
+ player.run();
+ } else {
+ player.noRun();
+ }
+ if (input.isDown('JUMP')) {
+ player.jump();
+ } else {
+ //we need this to handle the timing for how long you hold it
+ player.noJump();
+ }
+
+ if (input.isDown('DOWN')) {
+ player.crouch();
+ } else {
+ player.noCrouch();
+ }
+
+ if (input.isDown('LEFT')) { // 'd' or left arrow
+ player.moveLeft();
+ }
+ else if (input.isDown('RIGHT')) { // 'k' or right arrow
+ player.moveRight();
+ } else {
+ player.noWalk();
+ }
+}
+
+//update all the moving stuff
+function updateEntities(dt, gameTime) {
+ player.update(dt, vX);
+ updateables.forEach (function(ent) {
+ ent.update(dt, gameTime);
+ });
+
+ //This should stop the jump when he switches sides on the flag.
+ 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;
+ }
+
+ if (player.powering.length !== 0 || player.dying) { return; }
+ level.items.forEach (function(ent) {
+ ent.update(dt);
+ });
+
+ level.enemies.forEach (function(ent) {
+ ent.update(dt, vX);
+ });
+
+ fireballs.forEach(function(fireball) {
+ fireball.update(dt);
+ });
+ level.pipes.forEach (function(pipe) {
+ pipe.update(dt);
+ });
+}
+
+//scan for collisions
+function checkCollisions() {
+ if (player.powering.length !== 0 || player.dying) { return; }
+ player.checkCollisions();
+
+ //Apparently for each will just skip indices where things were deleted.
+ level.items.forEach(function(item) {
+ item.checkCollisions();
+ });
+ level.enemies.forEach (function(ent) {
+ ent.checkCollisions();
+ });
+ fireballs.forEach(function(fireball){
+ fireball.checkCollisions();
+ });
+ level.pipes.forEach (function(pipe) {
+ pipe.checkCollisions();
+ });
+}
+
+//draw the game!
+function render() {
+ updateables = [];
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = level.background;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ //scenery gets drawn first to get layering right.
+ for(var i = 0; i < 15; i++) {
+ for (var j = Math.floor(vX / 16) - 1; j < Math.floor(vX / 16) + 20; j++){
+ if (level.scenery[i][j]) {
+ renderEntity(level.scenery[i][j]);
+ }
+ }
+ }
+
+ //then items
+ level.items.forEach (function (item) {
+ renderEntity(item);
+ });
+
+ level.enemies.forEach (function(enemy) {
+ renderEntity(enemy);
+ });
+
+
+
+ fireballs.forEach(function(fireball) {
+ renderEntity(fireball);
+ })
+
+ //then we draw every static object.
+ for(var i = 0; i < 15; i++) {
+ for (var j = Math.floor(vX / 16) - 1; j < Math.floor(vX / 16) + 20; j++){
+ if (level.statics[i][j]) {
+ renderEntity(level.statics[i][j]);
+ }
+ if (level.blocks[i][j]) {
+ renderEntity(level.blocks[i][j]);
+ updateables.push(level.blocks[i][j]);
+ }
+ }
+ }
+
+ //then the player
+ // Then the player
+ // Render the player
+if (player.invincibility % 2 === 0) {
+ player.title = "Player 1"; // Set the title for the player
+ renderEntity(player);
+}
+
+// Render other players
+for (var playerId in otherPlayers) {
+ if (otherPlayers.hasOwnProperty(playerId) && otherPlayers[playerId].invincibility % 2 === 0) {
+ renderEntity(otherPlayers[playerId]);
+ }
+}
+
+
+
+
+ // Mario goes INTO pipes, so naturally they go after.
+ level.pipes.forEach (function(pipe) {
+ renderEntity(pipe);
+ });
+}
+
+function renderEntity(entity) {
+ // Render the player's chat message
+ if (entity.chatMessage) {
+ ctx.fillStyle = "#FFFFFF";
+ ctx.font = "bold 10px Arial";
+ ctx.fillText(entity.chatMessage, entity.pos[0] - vX, entity.pos[1] - vY - 5);
+ }
+
+ // Render the entity
+ entity.render(ctx, vX, vY);
+}
\ No newline at end of file
diff --git a/public/js/goomba.js b/public/js/goomba.js
new file mode 100644
index 0000000..78f420a
--- /dev/null
+++ b/public/js/goomba.js
@@ -0,0 +1,129 @@
+(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/public/js/input.js b/public/js/input.js
new file mode 100644
index 0000000..b807a97
--- /dev/null
+++ b/public/js/input.js
@@ -0,0 +1,116 @@
+(function() {
+ var pressedKeys = {};
+
+ function setKey(event, status) {
+ var code = event.keyCode;
+ var key;
+ var keyElementId;
+
+ switch (code) {
+ case 32:
+ key = 'SPACE';
+ keyElementId = 'key-space';
+ break;
+ case 37:
+ key = 'LEFT';
+ keyElementId = 'key-left';
+ break;
+ case 38:
+ key = 'UP';
+ keyElementId = 'key-up';
+ break;
+ case 39:
+ key = 'RIGHT';
+ keyElementId = 'key-right';
+ break;
+ case 40:
+ key = 'DOWN';
+ keyElementId = 'key-down';
+ break;
+ case 88:
+ key = 'JUMP';
+ keyElementId = 'key-x';
+ break;
+ case 90:
+ key = 'RUN';
+ keyElementId = 'key-z';
+ break;
+ default:
+ key = String.fromCharCode(code);
+ }
+
+ pressedKeys[key] = status;
+
+ var keyElement = document.getElementById(keyElementId);
+ if (keyElement) {
+ if (status) {
+ keyElement.classList.add('pressed');
+ // Move the player based on the button pressed
+ movePlayer(key);
+ } else {
+ keyElement.classList.remove('pressed');
+ }
+ }
+ }
+
+ function movePlayer(key) {
+ // Get the player object from the game.js file
+ var player = window.player;
+
+ // Perform player movements based on the button pressed
+ switch (key) {
+ case 'LEFT':
+ player.moveLeft();
+ break;
+ case 'RIGHT':
+ player.moveRight();
+ break;
+ case 'JUMP':
+ player.jump();
+ break;
+ case 'RUN':
+ player.run();
+ break;
+ case 'DOWN':
+ player.crouch();
+ break;
+ default:
+ break;
+ }
+ }
+
+ document.addEventListener('keydown', function(e) {
+ setKey(e, true);
+ });
+
+ document.addEventListener('keyup', function(e) {
+ setKey(e, false);
+ });
+
+ window.addEventListener('blur', function() {
+ pressedKeys = {};
+ // Reset all key visuals when window loses focus
+ var keys = document.getElementsByClassName('key');
+ for (var i = 0; i < keys.length; i++) {
+ keys[i].classList.remove('pressed');
+ }
+ });
+
+ 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;
+ // Reset all key visuals when controls are reset
+ var keys = document.getElementsByClassName('key');
+ for (var i = 0; i < keys.length; i++) {
+ keys[i].classList.remove('pressed');
+ }
+ }
+ };
+})();
diff --git a/public/js/koopa.js b/public/js/koopa.js
new file mode 100644
index 0000000..d7243b7
--- /dev/null
+++ b/public/js/koopa.js
@@ -0,0 +1,211 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ var Koopa = Mario.Koopa = function(pos, sprite, para) {
+ this.dying = false;
+ this.shell = false;
+
+ this.para = para; //para. As in, is it a paratroopa?
+
+ //So, funny story. The actual hitboxes don't reach all the way to the ground.
+ //What that means is, as long as I use them to keep things on the floor
+ //making the hitboxes accurate will make enemies sink into the ground.
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: sprite,
+ hitbox: [2,8,12,24]
+ });
+ this.vel[0] = -0.5;
+ this.idx = level.enemies.length;
+ };
+
+ Koopa.prototype.render = function(ctx, vX, vY) {
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ };
+
+ Koopa.prototype.update = function(dt, vX) {
+ if (this.turn) {
+ this.vel[0] = -this.vel[0];
+ if (this.shell) sounds.bump.play();
+ this.turn = false;
+ }
+ if (this.vel[0] != 0) {
+ this.left = (this.vel[0] < 0);
+ }
+
+ if (this.left) {
+ this.sprite.img = 'sprites/enemy.png';
+ } else {
+ this.sprite.img = 'sprites/enemyr.png';
+ }
+
+ 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];
+ }
+ }
+
+ if (this.shell) {
+ if (this.vel[0] == 0) {
+ this.shell -= 1;
+ if (this.shell < 120) {
+ this.sprite.speed = 5;
+ }
+ if (this.shell == 0) {
+ this.sprite = level.koopaSprite();
+ this.hitbox = [2,8,12,24]
+ if (this.left) {
+ this.sprite.img = 'sprites/enemyr.png';
+ this.vel[0] = 0.5;
+ this.left = false;
+ } else {
+ this.vel[0] = -0.5;
+ this.left = true;
+ }
+ this.pos[1] -= 16;
+ }
+ } else {
+ this.shell = 360;
+ this.sprite.speed = 0;
+ this.sprite.setFrame(0);
+ }
+ }
+ 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);
+ };
+
+ Koopa.prototype.collideWall = function() {
+ //This stops us from flipping twice on the same frame if we collide
+ //with multiple wall tiles simultaneously.
+ this.turn = true;
+ };
+
+ Koopa.prototype.checkCollisions = function() {
+ var h = this.shell ? 1 : 2;
+ if (this.pos[1] % 16 !== 0) {
+ h += 1;
+ }
+ 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;
+ }
+
+ if (this.flipping) {
+ 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);
+ };
+
+ Koopa.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 (ent.vel[1] > 0) {
+ player.bounce = true;
+ }
+ if (this.shell) {
+ sounds.kick.play();
+ if (this.vel[0] === 0) {
+ if (ent.left) { //I'm pretty sure this isn't the real logic.
+ this.vel[0] = -4;
+ } else {
+ this.vel[0] = 4;
+ }
+ } else {
+ if (ent.bounce) {
+ this.vel[0] = 0;
+ } else ent.damage();
+ }
+ } else if (ent.vel[1] > 0) { //then we get BOPPED.
+ this.stomp();
+ } else { //or the player gets hit
+ ent.damage();
+ }
+ } else {
+ if (this.shell && (ent instanceof Mario.Goomba)) {
+ ent.bump();
+ } else this.collideWall();
+ }
+ }
+ }
+ };
+
+ Koopa.prototype.stomp = function() {
+ //Turn this thing into a shell if it isn't already. Kick it if it is.
+ player.bounce = true;
+ if (this.para) {
+ this.para = false;
+ this.sprite.pos[0] -= 32;
+ } else {
+ sounds.stomp.play();
+ this.shell = 360;
+ this.sprite.pos[0] += 64;
+ this.sprite.pos[1] += 16;
+ this.sprite.size = [16,16];
+ this.hitbox = [2,0,12,16];
+ this.sprite.speed = 0;
+ this.frames = [0,1];
+ this.vel = [0,0];
+ this.pos[1] += 16;
+ }
+
+ };
+
+ Koopa.prototype.bump = function() {
+ sounds.kick.play();
+ if (this.flipping) return;
+ this.flipping = true;
+ this.sprite.pos = [160, 0];
+ this.sprite.size = [16,16];
+ this.hitbox = [2, 0, 12, 16];
+ this.sprite.speed = 0;
+ this.vel[0] = 0;
+ this.vel[1] = -2.5;
+ };
+})();
diff --git a/public/js/levels/11.js b/public/js/levels/11.js
new file mode 100644
index 0000000..6b0d2eb
--- /dev/null
+++ b/public/js/levels/11.js
@@ -0,0 +1,226 @@
+var oneone = Mario.oneone = function() {
+ //The things that need to be passed in are basically just dependent on what
+ //tileset we're in, so it makes more sense to just make one variable for that, so
+ //TODO: put as much of this in the Level object definition as possible.
+ level = new Mario.Level({
+ playerPos: [56,192],
+ loader: Mario.oneone,
+ background: "#7974FF",
+ scrolling: true,
+ invincibility: [144, 192, 240],
+ exit: 204,
+ floorSprite: new Mario.Sprite('sprites/tiles.png', [0,0],[16,16],0),
+ cloudSprite: new Mario.Sprite('sprites/tiles.png', [0,320],[48,32],0),
+ wallSprite: new Mario.Sprite('sprites/tiles.png', [0, 16],[16,16],0),
+ brickSprite: new Mario.Sprite('sprites/tiles.png', [16, 0], [16,16], 0),
+ brickBounceSprite: new Mario.Sprite('sprites/tiles.png',[32,0],[16,16],0),
+ rubbleSprite: function () {
+ return new Mario.Sprite('sprites/items.png', [64,0], [8,8], 3, [0,1])
+ },
+ ublockSprite: new Mario.Sprite('sprites/tiles.png', [48, 0], [16,16],0),
+ superShroomSprite: new Mario.Sprite('sprites/items.png', [0,0], [16,16], 0),
+ fireFlowerSprite: new Mario.Sprite('sprites/items.png', [0,32], [16,16], 20, [0,1,2,3]),
+ starSprite: new Mario.Sprite('sprites/items.png', [0,48], [16,16], 20, [0,1,2,3]),
+ pipeLEndSprite: new Mario.Sprite('sprites/tiles.png', [0, 128], [16,16], 0),
+ pipeREndSprite: new Mario.Sprite('sprites/tiles.png', [16, 128], [16,16], 0),
+ pipeLMidSprite: new Mario.Sprite('sprites/tiles.png', [0, 144], [16,16], 0),
+ pipeRMidSprite: new Mario.Sprite('sprites/tiles.png', [16, 144], [16,16], 0),
+
+ pipeUpMid: new Mario.Sprite('sprites/tiles.png', [0, 144], [32,16], 0),
+ pipeSideMid: new Mario.Sprite('sprites/tiles.png', [48, 128], [16,32], 0),
+ pipeLeft: new Mario.Sprite('sprites/tiles.png', [32, 128], [16,32], 0),
+ pipeTop: new Mario.Sprite('sprites/tiles.png', [0, 128], [32,16], 0),
+ qblockSprite: new Mario.Sprite('sprites/tiles.png', [384, 0], [16,16], 8, [0,0,0,0,1,2,1]),
+ bcoinSprite: function() {
+ return new Mario.Sprite('sprites/items.png', [0,112],[16,16], 20,[0,1,2,3]);
+ },
+ cloudSprites:[
+ new Mario.Sprite('sprites/tiles.png', [0,320],[16,32],0),
+ new Mario.Sprite('sprites/tiles.png', [16,320],[16,32],0),
+ new Mario.Sprite('sprites/tiles.png', [32,320],[16,32],0)
+ ],
+ hillSprites: [
+ new Mario.Sprite('sprites/tiles.png', [128,128],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [144,128],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [160,128],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [128,144],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [144,144],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [160,144],[16,16],0)
+ ],
+ bushSprite: new Mario.Sprite('sprites/tiles.png', [176, 144], [48, 16], 0),
+ bushSprites: [
+ new Mario.Sprite('sprites/tiles.png', [176,144], [16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [192,144], [16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [208,144], [16,16],0)],
+ goombaSprite: function() {
+ return new Mario.Sprite('sprites/enemy.png', [0, 16], [16,16], 3, [0,1]);
+ },
+ koopaSprite: function() {
+ return new Mario.Sprite('sprites/enemy.png', [96,0], [16,32], 2, [0,1]);
+ },
+ flagPoleSprites: [
+ new Mario.Sprite('sprites/tiles.png', [256, 128], [16,16], 0),
+ new Mario.Sprite('sprites/tiles.png', [256, 144], [16,16], 0),
+ new Mario.Sprite('sprites/items.png', [128, 32], [16,16], 0)
+ ]
+ });
+ ground = [[0,69],[71,86],[89,153],[155,212]];
+ player.pos[0] = level.playerPos[0];
+ player.pos[1] = level.playerPos[1];
+ vX = 0;
+
+ //build THE GROUND
+ ground.forEach(function(loc) {
+ level.putFloor(loc[0],loc[1]);
+ });
+
+ //build scenery
+ clouds = [[7,3],[19, 2],[56, 3],[67, 2],[87, 2],[103, 2],[152, 3],[163, 2],[200, 3]];
+ clouds.forEach(function(cloud){
+ level.putCloud(cloud[0],cloud[1]);
+ });
+
+ twoClouds = [[36,2],[132,2],[180,2]];
+ twoClouds.forEach(function(cloud){
+ level.putTwoCloud(cloud[0],cloud[1]);
+ });
+
+ threeClouds = [[27,3],[75,3],[123,3],[171,3]];
+ threeClouds.forEach(function(cloud){
+ level.putThreeCloud(cloud[0],cloud[1]);
+ });
+
+ bHills = [0,48,96,144,192]
+ bHills.forEach(function(hill) {
+ level.putBigHill(hill, 12);
+ });
+
+ sHills = [16,64,111,160];
+ sHills.forEach(function(hill) {
+ level.putSmallHill(hill, 12);
+ });
+
+ bushes = [23,71,118,167];
+ bushes.forEach(function(bush) {
+ level.putBush(bush, 12);
+ });
+
+ twoBushes = [41,89,137];
+ twoBushes.forEach(function(bush) {
+ level.putTwoBush(bush, 12);
+ });
+
+ threeBushes = [11,59,106];
+ threeBushes.forEach(function(bush) {
+ level.putThreeBush(bush, 12);
+ });
+
+ //interactable terrain
+ level.putQBlock(16, 9, new Mario.Bcoin([256, 144]));
+ level.putBrick(20, 9, null);
+ level.putQBlock(21, 9, new Mario.Mushroom([336, 144]));
+ level.putBrick(22, 9, null);
+ level.putQBlock(22, 5, new Mario.Bcoin([352, 80]));
+ level.putQBlock(23, 9, new Mario.Bcoin([368, 144]));
+ level.putBrick(24, 9, null);
+ level.putPipe(28, 13, 2);
+ level.putPipe(38, 13, 3);
+ level.putPipe(46, 13, 4);
+ level.putRealPipe(57, 9, 4, "DOWN", Mario.oneonetunnel);
+ level.putBrick(77, 9, null);
+ level.putQBlock(78, 9, new Mario.Mushroom([1248, 144]));
+ level.putBrick(79, 9, null);
+ level.putBrick(80, 5, null);
+ level.putBrick(81, 5, null);
+ level.putBrick(82, 5, null);
+ level.putBrick(83, 5, null);
+ level.putBrick(84, 5, null);
+ level.putBrick(85, 5, null);
+ level.putBrick(86, 5, null);
+ level.putBrick(87, 5, null);
+ level.putBrick(91, 5, null);
+ level.putBrick(92, 5, null);
+ level.putBrick(93, 5, null);
+ level.putQBlock(94, 5, new Mario.Bcoin([1504, 80]));
+ level.putBrick(94, 9, null);
+ level.putBrick(100, 9, new Mario.Star([1600, 144]));
+ level.putBrick(101, 9, null);
+ level.putQBlock(105, 9, new Mario.Bcoin([1680, 144]));
+ level.putQBlock(108, 9, new Mario.Bcoin([1728, 144]));
+ level.putQBlock(108, 5, new Mario.Mushroom([1728, 80]));
+ level.putQBlock(111, 9, new Mario.Bcoin([1776, 144]));
+ level.putBrick(117, 9, null);
+ level.putBrick(120, 5, null);
+ level.putBrick(121, 5, null);
+ level.putBrick(122, 5, null);
+ level.putBrick(123, 5, null);
+ level.putBrick(128, 5, null);
+ level.putQBlock(129, 5, new Mario.Bcoin([2074, 80]));
+ level.putBrick(129, 9, null);
+ level.putQBlock(130, 5, new Mario.Bcoin([2080, 80]));
+ level.putBrick(130, 9, null);
+ level.putBrick(131, 5, null);
+ level.putWall(134, 13, 1);
+ level.putWall(135, 13, 2);
+ level.putWall(136, 13, 3);
+ level.putWall(137, 13, 4);
+ level.putWall(140, 13, 4);
+ level.putWall(141, 13, 3);
+ level.putWall(142, 13, 2);
+ level.putWall(143, 13, 1);
+ level.putWall(148, 13, 1);
+ level.putWall(149, 13, 2);
+ level.putWall(150, 13, 3);
+ level.putWall(151, 13, 4);
+ level.putWall(152, 13, 4);
+ level.putWall(155, 13, 4);
+ level.putWall(156, 13, 3);
+ level.putWall(157, 13, 2);
+ level.putWall(158, 13, 1);
+ level.putPipe(163, 13, 2);
+ level.putBrick(168, 9, null);
+ level.putBrick(169, 9, null);
+ level.putQBlock(170, 9, new Mario.Bcoin([2720, 144]));
+ level.putBrick(171, 9, null);
+ level.putPipe(179, 13, 2);
+ level.putWall(181, 13, 1);
+ level.putWall(182, 13, 2);
+ level.putWall(183, 13, 3);
+ level.putWall(184, 13, 4);
+ level.putWall(185, 13, 5);
+ level.putWall(186, 13, 6);
+ level.putWall(187, 13, 7);
+ level.putWall(188, 13, 8);
+ level.putWall(189, 13, 8);
+ level.putFlagpole(198);
+
+ //and enemies
+ level.putGoomba(22, 12);
+ level.putGoomba(40, 12);
+ level.putGoomba(50, 12);
+ level.putGoomba(51, 12);
+ level.putGoomba(82, 4);
+ level.putGoomba(84, 4);
+ level.putGoomba(100, 12);
+ level.putGoomba(102, 12);
+ level.putGoomba(114, 12);
+ level.putGoomba(115, 12);
+ level.putGoomba(122, 12);
+ level.putGoomba(123, 12);
+ level.putGoomba(125, 12);
+ level.putGoomba(126, 12);
+ level.putGoomba(170, 12);
+ level.putGoomba(172, 12);
+ level.putKoopa(35, 11);
+
+ music.underground.pause();
+ // music.overworld.currentTime = 0;
+ canvas.addEventListener('touchstart', function() {
+ music.overworld.play();
+ });
+
+ canvas.addEventListener('mousedown', function() {
+ music.overworld.play();
+ });
+
+};
diff --git a/public/js/levels/11tunnel.js b/public/js/levels/11tunnel.js
new file mode 100644
index 0000000..25f2ab4
--- /dev/null
+++ b/public/js/levels/11tunnel.js
@@ -0,0 +1,65 @@
+var oneonetunnel = Mario.oneonetunnel = function() {
+ level = new Mario.Level({
+ playerPos: [40,16],
+ loader: Mario.oneonetunnel,
+ background: "#000000",
+ scrolling: false,
+ coinSprite: function() {
+ return new Mario.Sprite('sprites/items.png', [0,96],[16,16], 6,[0,0,0,0,1,2,1]);
+ },
+ floorSprite: new Mario.Sprite('sprites/tiles.png', [0,32],[16,16],0),
+ wallSprite: new Mario.Sprite('sprites/tiles.png', [32, 32],[16,16],0),
+ brickSprite: new Mario.Sprite('sprites/tiles.png', [16, 0], [16,16], 0),
+ brickBounceSprite: new Mario.Sprite('sprites/tiles.png',[32,0],[16,16],0),
+ ublockSprite: new Mario.Sprite('sprites/tiles.png', [48, 0], [16,16],0),
+ pipeLMidSprite: new Mario.Sprite('sprites/tiles.png', [0, 144], [16,16], 0),
+ pipeRMidSprite: new Mario.Sprite('sprites/tiles.png', [16, 144], [16,16], 0),
+ pipeLEndSprite: new Mario.Sprite('sprites/tiles.png', [0, 128], [16,16], 0),
+ pipeREndSprite: new Mario.Sprite('sprites/tiles.png', [16, 128], [16,16], 0),
+ pipeUpMid: new Mario.Sprite('sprites/tiles.png', [0, 144], [32,16], 0),
+ pipeSideMid: new Mario.Sprite('sprites/tiles.png', [48, 128], [16,32], 0),
+ pipeLeft: new Mario.Sprite('sprites/tiles.png', [32, 128], [16,32], 0),
+ pipeTop: new Mario.Sprite('sprites/tiles.png', [0, 128], [32,16], 0),
+
+ LPipeSprites:[
+ new Mario.Sprite('sprites/tiles.png', [32,128],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [32,144],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [48,128],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [48,144],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [64,128],[16,16],0),
+ new Mario.Sprite('sprites/tiles.png', [64,144],[16,16],0),
+ ]
+
+ });
+
+ player.pos[0] = level.playerPos[0];
+ player.pos[1] = level.playerPos[1];
+ vX = 0;
+ level.putFloor(0,16);
+ level.putWall(0,13,11);
+ walls = [4,5,6,7,8,9,10];
+ walls.forEach(function(loc){
+ level.putWall(loc,13,3);
+ level.putWall(loc,3,1);
+ });
+
+ coins = [[5,5], [6,5], [7,5], [8,5], [9,5],
+ [4,7], [5,7], [6,7], [7,7], [8,7], [9,7], [10,7],
+ [4,9], [5,9], [6,9], [7,9], [8,9], [9,9], [10,9]];
+ coins.forEach(function(pos){
+ level.putCoin(pos[0],pos[1]);
+ });
+
+ //level.putLeftPipe(13,11);
+ level.putRealPipe(13,11,3,"RIGHT", function() {
+ Mario.oneone.call();
+ player.pos = [2616, 177]
+ player.pipe("UP", function() {;});
+ });
+
+ level.putPipe(15,13,13);
+
+ music.overworld.pause();
+ music.underground.currentTime = 0;
+ music.underground.play();
+};
diff --git a/public/js/levels/level.js b/public/js/levels/level.js
new file mode 100644
index 0000000..2a7ca17
--- /dev/null
+++ b/public/js/levels/level.js
@@ -0,0 +1,218 @@
+(function() {
+ var Level = Mario.Level = function(options) {
+ this.playerPos = options.playerPos;
+ this.scrolling = options.scrolling;
+ this.loader = options.loader;
+ this.background = options.background;
+ this.exit = options.exit;
+
+ this.floorSprite = options.floorSprite;
+ this.cloudSprite = options.cloudSprite;
+ this.wallSprite = options.wallSprite;
+ this.brickSprite = options.brickSprite;
+ this.rubbleSprite = options.rubbleSprite;
+ this.brickBounceSprite = options.brickBounceSprite;
+ this.ublockSprite = options.ublockSprite;
+ this.superShroomSprite = options.superShroomSprite;
+ this.fireFlowerSprite = options.fireFlowerSprite;
+ this.starSprite = options.starSprite;
+ this.coinSprite = options.coinSprite;
+ this.bcoinSprite = options.bcoinSprite;
+ this.goombaSprite = options.goombaSprite;
+ this.koopaSprite = options.koopaSprite;
+
+ //prop pipe sprites, to be phased out
+ this.pipeLEndSprite = options.pipeLEndSprite;
+ this.pipeREndSprite = options.pipeREndSprite;
+ this.pipeLMidSprite = options.pipeLMidSprite;
+ this.pipeRMidSprite = options.pipeRMidSprite;
+
+ //real pipe sprites, use these.
+ this.pipeUpMid = options.pipeUpMid;
+ this.pipeSideMid = options.pipeSideMid;
+ this.pipeLeft = options.pipeLeft;
+ this.pipeTop = options.pipeTop;
+
+ this.flagpoleSprites = options.flagPoleSprites;
+
+ this.LPipeSprites = options.LPipeSprites;
+ this.cloudSprites = options.cloudSprites;
+ this.hillSprites = options.hillSprites;
+ this.bushSprite = options.bushSprite;
+ this.bushSprites = options.bushSprites;
+ this.qblockSprite = options.qblockSprite;
+
+ this.invincibility = options.invincibility;
+ this.statics = [];
+ this.scenery = [];
+ this.blocks = [];
+ this.enemies = [];
+ this.items = [];
+ this.pipes = [];
+
+ for (var i = 0; i < 15; i++) {
+ this.statics[i] = [];
+ this.scenery[i] = [];
+ this.blocks[i] = [];
+ }
+
+ };
+
+ Level.prototype.putFloor = function(start, end) {
+ for (var i = start; i < end; i++) {
+ this.statics[13][i] = new Mario.Floor([16*i,208], this.floorSprite);
+ this.statics[14][i] = new Mario.Floor([16*i,224], this.floorSprite);
+ }
+ };
+
+ Level.prototype.putGoomba = function(x, y) {
+ this.enemies.push(new Mario.Goomba([16*x, 16*y], this.goombaSprite() ));
+ };
+
+ Level.prototype.putKoopa = function(x, y) {
+ this.enemies.push(new Mario.Koopa([16*x, 16*y], this.koopaSprite(), false));
+ };
+
+ Level.prototype.putWall = function(x, y, height) {
+ //y is the bottom of the wall in this case.
+ for (var i = y-height; i < y; i++) {
+ this.statics[i][x] = new Mario.Floor([16*x, 16*i], this.wallSprite);
+ }
+ };
+
+ Level.prototype.putPipe = function(x, y, height) {
+ for (var i = y - height; i < y; i++) {
+ if (i === y - height) {
+ this.statics[i][x] = new Mario.Floor([16*x, 16*i], this.pipeLEndSprite);
+ this.statics[i][x+1] = new Mario.Floor([16*x+16, 16*i], this.pipeREndSprite);
+ } else {
+ this.statics[i][x] = new Mario.Floor([16*x, 16*i], this.pipeLMidSprite);
+ this.statics[i][x+1] = new Mario.Floor([16*x+16, 16*i], this.pipeRMidSprite);
+ }
+ }
+ };
+
+ //sometimes, pipes don't go straight up and down.
+ Level.prototype.putLeftPipe = function(x,y) {
+ this.statics[y][x] = new Mario.Floor([16*x, 16*y], this.LPipeSprites[0]);
+ this.statics[y+1][x] = new Mario.Floor([16*x,16*(y+1)], this.LPipeSprites[1]);
+ this.statics[y][x+1] = new Mario.Floor([16*(x+1),16*y], this.LPipeSprites[2]);
+ this.statics[y+1][x+1] = new Mario.Floor([16*(x+1),16*(y+1)], this.LPipeSprites[3]);
+ this.statics[y][x+2] = new Mario.Floor([16*(x+2),16*y], this.LPipeSprites[4]);
+ this.statics[y+1][x+2] = new Mario.Floor([16*(x+2),16*(y+1)], this.LPipeSprites[5]);
+ };
+
+ Level.prototype.putCoin = function(x, y) {
+ this.items.push(new Mario.Coin(
+ [x*16, y*16],
+ this.coinSprite()
+ ));
+ };
+
+ Level.prototype.putCloud = function(x, y) {
+ this.scenery[y][x] = new Mario.Prop([x*16, y*16], this.cloudSprite);
+ };
+
+ Level.prototype.putQBlock = function(x, y, item) {
+ this.blocks[y][x] = new Mario.Block( {
+ pos: [x*16, y*16],
+ item: item,
+ sprite: this.qblockSprite,
+ usedSprite: this.ublockSprite
+ });
+ };
+
+ Level.prototype.putBrick = function(x,y,item) {
+ this.blocks[y][x] = new Mario.Block({
+ pos: [x*16, y*16],
+ item: item,
+ sprite: this.brickSprite,
+ bounceSprite: this.brickBounceSprite,
+ usedSprite: this.ublockSprite,
+ breakable: !item
+ });
+ };
+
+ Level.prototype.putBigHill = function(x, y) {
+ var px = x*16, py = y*16;
+ this.scenery[y][x] = new Mario.Prop([px, py], this.hillSprites[0]);
+ this.scenery[y][x+1] = new Mario.Prop([px+16, py], this.hillSprites[3]);
+ this.scenery[y-1][x+1] = new Mario.Prop([px+16, py-16], this.hillSprites[0]);
+ this.scenery[y][x+2] = new Mario.Prop([px+32, py], this.hillSprites[4]);
+ this.scenery[y-1][x+2] = new Mario.Prop([px+32, py-16], this.hillSprites[3]);
+ this.scenery[y-2][x+2] = new Mario.Prop([px+32, py-32], this.hillSprites[1]);
+ this.scenery[y][x+3] = new Mario.Prop([px+48, py], this.hillSprites[5]);
+ this.scenery[y-1][x+3] = new Mario.Prop([px+48, py-16], this.hillSprites[2]);
+ this.scenery[y][x+4] = new Mario.Prop([px+64, py], this.hillSprites[2]);
+ };
+
+ Level.prototype.putBush = function(x, y) {
+ this.scenery[y][x] = new Mario.Prop([x*16, y*16], this.bushSprite);
+ };
+
+ Level.prototype.putThreeBush = function(x,y) {
+ px = x*16;
+ py = y*16;
+ this.scenery[y][x] = new Mario.Prop([px, py], this.bushSprites[0]);
+ this.scenery[y][x+1] = new Mario.Prop([px+16, py], this.bushSprites[1]);
+ this.scenery[y][x+2] = new Mario.Prop([px+32, py], this.bushSprites[1]);
+ this.scenery[y][x+3] = new Mario.Prop([px+48, py], this.bushSprites[1]);
+ this.scenery[y][x+4] = new Mario.Prop([px+64, py], this.bushSprites[2]);
+ };
+
+ Level.prototype.putTwoBush = function(x,y) {
+ px = x*16;
+ py = y*16;
+ this.scenery[y][x] = new Mario.Prop([px, py], this.bushSprites[0]);
+ this.scenery[y][x+1] = new Mario.Prop([px+16, py], this.bushSprites[1]);
+ this.scenery[y][x+2] = new Mario.Prop([px+32, py], this.bushSprites[1]);
+ this.scenery[y][x+3] = new Mario.Prop([px+48, py], this.bushSprites[2]);
+ };
+
+ Level.prototype.putSmallHill = function(x, y) {
+ var px = x*16, py = y*16;
+ this.scenery[y][x] = new Mario.Prop([px, py], this.hillSprites[0]);
+ this.scenery[y][x+1] = new Mario.Prop([px+16, py], this.hillSprites[3]);
+ this.scenery[y-1][x+1] = new Mario.Prop([px+16, py-16], this.hillSprites[1]);
+ this.scenery[y][x+2] = new Mario.Prop([px+32, py], this.hillSprites[2]);
+ };
+
+ Level.prototype.putTwoCloud = function(x,y) {
+ px = x*16;
+ py = y*16;
+ this.scenery[y][x] = new Mario.Prop([px, py], this.cloudSprites[0]);
+ this.scenery[y][x+1] = new Mario.Prop([px+16, py], this.cloudSprites[1]);
+ this.scenery[y][x+2] = new Mario.Prop([px+32, py], this.cloudSprites[1]);
+ this.scenery[y][x+3] = new Mario.Prop([px+48, py], this.cloudSprites[2]);
+ };
+
+ Level.prototype.putThreeCloud = function(x,y) {
+ px = x*16;
+ py = y*16;
+ this.scenery[y][x] = new Mario.Prop([px, py], this.cloudSprites[0]);
+ this.scenery[y][x+1] = new Mario.Prop([px+16, py], this.cloudSprites[1]);
+ this.scenery[y][x+2] = new Mario.Prop([px+32, py], this.cloudSprites[1]);
+ this.scenery[y][x+3] = new Mario.Prop([px+48, py], this.cloudSprites[1]);
+ this.scenery[y][x+4] = new Mario.Prop([px+64, py], this.cloudSprites[2]);
+ };
+
+ Level.prototype.putRealPipe = function(x, y, length, direction, destination) {
+ px = x*16;
+ py = y*16;
+ this.pipes.push(new Mario.Pipe({
+ pos: [px, py],
+ length: length,
+ direction: direction,
+ destination: destination
+ }));
+ }
+
+ Level.prototype.putFlagpole = function(x) {
+ this.statics[12][x] = new Mario.Floor([16*x, 192], this.wallSprite);
+ for (i=3; i < 12; i++) {
+ this.scenery[i][x] = new Mario.Prop([16*x, 16*i], this.flagpoleSprites[1])
+ }
+ this.scenery[2][x] = new Mario.Prop([16*x, 32], this.flagpoleSprites[0]);
+ this.items.push(new Mario.Flag(16*x));
+ }
+})();
diff --git a/public/js/mushroom.js b/public/js/mushroom.js
new file mode 100644
index 0000000..97e7233
--- /dev/null
+++ b/public/js/mushroom.js
@@ -0,0 +1,118 @@
+(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/public/js/outline.txt b/public/js/outline.txt
new file mode 100644
index 0000000..346b1a7
--- /dev/null
+++ b/public/js/outline.txt
@@ -0,0 +1,45 @@
+Enemy:
+ #collideWith
+ checks collision. Bounces off of walls and other enemies. Kills marios.
+ gets killed by block bonk from below
+
+ Specific enemy behaviors:
+ koopas get replaced with shells
+ shells get kicked if touched from the side, instead of killing
+
+Mario:
+ #collideWith
+ gets expelled from walls, bonks blocks, destroys blocks as big Mario
+ note: collision with blocks is determined by which one is over the center
+ if you're slightly to the side, you slip past it.
+ Crazy-ass sprite shifting if you're in star mode!
+
+
+Item
+ #collideWith
+ item pickup logic
+
+Mushroom
+ #update
+ movement logic for mushrooms
+ get impulse from being block bonked
+ this can be implemented by giving them the appropriate accel
+ just if they overlap a block from the bottom.
+ of course, only AFTER they finish sliding out of the block.
+
+Water levels:
+ make a separate class for water Marios
+ less gravity
+ swimming sprite
+ fireballs are the same
+ jump works when not on the ground
+ different enemies
+
+
+TODO: Make level loader use hashes instead of arrays where possible.
+Should be a free performance gain.
+
+Should also make item deletion less weird.
+
+TODO: Make gravity exist higher up instead of having a magic number.
+Note from the future: Nope. Different objects have different gravity.
diff --git a/public/js/pipe.js b/public/js/pipe.js
new file mode 100644
index 0000000..8e1ab90
--- /dev/null
+++ b/public/js/pipe.js
@@ -0,0 +1,160 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+
+ //there are too many possible configurations of pipe to capture in a reasonable
+ //set of simple variables. Joints, etc. are just too much.
+ //To that end, the pipe class handles simple pipes, and we'll put together
+ //anything more complex with individual props. OK? OK.
+ Pipe = Mario.Pipe = function(options) {
+ this.pos = options.pos
+
+ //NOTE: direction is the direction you move INTO the pipe.
+ this.direction = options.direction
+ this.destination = options.destination
+ this.length = options.length;
+
+ if (this.direction === "UP" || this.direction === "DOWN") {
+ this.hitbox = [0,0, 32, this.length * 16];
+ this.midsection = level.pipeUpMid;
+ this.endsection = level.pipeTop;
+ } else {
+ this.hitbox = [0,0, 16*this.length, 32];
+ this.midsection = level.pipeSideMid;
+ this.endsection = level.pipeLeft;
+ }
+ }
+
+ Pipe.prototype.checkPipe = function() {
+ if (this.destination === undefined || !input.isDown(this.direction)) return;
+
+ var h = player.power===0 ? 16 : 32;
+ var x = Math.floor(player.pos[0]);
+ var y = Math.floor(player.pos[1]);
+ switch (this.direction) {
+ case 'RIGHT': if (x === this.pos[0]-16 &&
+ y >= this.pos[1] &&
+ y+h <= this.pos[1]+32) {
+ player.pipe(this.direction, this.destination)
+ }
+ break;
+ case 'LEFT': if (x === this.pos[0]+16*this.length &&
+ y >= this.pos[1] &&
+ y+h <= this.pos[1]+32) {
+ player.pipe(this.direction, this.destination)
+ }
+ break;
+ case 'UP': if (y === this.pos[1] + 16*this.length &&
+ x >= this.pos[0] &&
+ x+16 <= this.pos[0]+32) {
+ player.pipe(this.direction, this.destination)
+ }
+ break;
+ case 'DOWN': if (y+h === this.pos[1] &&
+ x >= this.pos[0] &&
+ x+16 <= this.pos[0]+32) {
+ player.pipe(this.direction, this.destination);
+ }
+ break;
+ }
+ }
+
+ //Note to self: next time, decide on a convention for which thing checks for collisions
+ //and stick to it. This is a pain.
+ Pipe.prototype.checkCollisions = function() {
+ var that = this;
+ level.enemies.forEach (function(ent) {
+ that.isCollideWith(ent);
+ });
+
+ level.items.forEach (function(ent) {
+ that.isCollideWith(ent);
+ });
+
+ fireballs.forEach(function(ent){
+ that.isCollideWith(ent)
+ });
+
+ if (!player.piping) this.isCollideWith(player);
+ }
+
+ Pipe.prototype.isCollideWith = function (ent) {
+ //long story short: because we scan every item, and and one 'rubble' item is four things with separate positions
+ //we'll crash without this line as soon as we destroy a block. OOPS.
+ if (ent.pos === undefined) return;
+
+
+ //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 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]) {
+ 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) {
+ ent.jumping = 0;
+ }
+ } else {
+ //entity is hitting it from the side, we're a wall
+ ent.collideWall(this);
+ }
+ }
+ }
+ }
+
+ //we COULD try to write some shenanigans so that the check gets put into the
+ //collision code, but there won't ever be more than a handful of pipes in a level
+ //so the performance hit of scanning all of them is miniscule.
+ Pipe.prototype.update = function(dt) {
+ if (this.destination) this.checkPipe();
+ }
+
+ //http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array
+ //I honestly have no idea if javascript does this, but I feel like it makes sense
+ //stylistically to prefer branching outside of loops when possible as convention
+
+ //TODO: edit the spritesheet so UP and LEFT pipes aren't backwards.
+ Pipe.prototype.render = function(ctx, vX, vY) {
+ switch (this.direction) {
+ case "DOWN":
+ this.endsection.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ for (var i = 1; i < this.length; i++) {
+ this.midsection.render(ctx, this.pos[0], this.pos[1]+i*16, vX, vY)
+ }
+ break;
+ case "UP":
+ this.endsection.render(ctx, this.pos[0], this.pos[1]+16*(this.length-1), vX, vY)
+ for (var i=0; i < this.length - 1; i++) {
+ this.midsection.render(ctx, this.pos[0], this.pos[1]+i*16, vX, vY)
+ }
+ break;
+ case "RIGHT":
+ this.endsection.render(ctx, this.pos[0], this.pos[1], vX, vY)
+ for (var i = 1; i < this.length; i++) {
+ this.midsection.render(ctx, this.pos[0]+16*i, this.pos[1], vX, vY)
+ }
+ break;
+ case "LEFT":
+ this.endsection.render(ctx, this.pos[0]+16*(this.length-1), this.pos[1], vX, vY)
+ for (var i = 0; i < this.legth-1; i++) {
+ this.midsection.render(ctx, this.pos[0], this.pos[1]+i*16, vX, vY)
+ }
+ break;
+ }
+ }
+})();
diff --git a/public/js/player.js b/public/js/player.js
new file mode 100644
index 0000000..b085de1
--- /dev/null
+++ b/public/js/player.js
@@ -0,0 +1,446 @@
+(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;
+
+ if (this.starTime) {
+ var index;
+ if (this.starTime > 60)
+ index = Math.floor(this.starTime / 2) % 3;
+ else index = Math.floor(this.starTime / 8) % 3;
+
+ this.sprite.pos[1] = level.invincibility[index];
+ if (this.power == 0) {
+ this.sprite.pos[1] += 32;
+ }
+ this.starTime -= 1;
+ if (this.starTime == 0) {
+ switch(this.power) {
+ case 0: this.sprite.pos[1] = 32; break;
+ case 1: this.sprite.pos[1] = 0; break;
+ case 2: this.sprite.pos[1] = 96; break;
+ }
+ }
+ }
+ //okay cool, now set the sprite
+ if (this.crouching) {
+ this.sprite.pos[0] = 176;
+ this.sprite.speed = 0;
+ return;
+ }
+
+ if (this.jumping) {
+ this.sprite.pos[0] = 160;
+ this.sprite.speed = 0;
+ } else if (this.standing) {
+ if (Math.abs(this.vel[0]) > 0) {
+ if (this.vel[0] * this.acc[0] >= 0) {
+ this.sprite.pos[0] = 96;
+ this.sprite.frames = [0,1,2];
+ if (this.vel[0] < 0.2) {
+ this.sprite.speed = 5;
+ } else {
+ this.sprite.speed = Math.abs(this.vel[0]) * 8;
+ }
+ } else if ((this.vel[0] > 0 && this.left) || (this.vel[0] < 0 && !this.left)){
+ this.sprite.pos[0] = 144;
+ this.sprite.speed = 0;
+ }
+ } else {
+ this.sprite.pos[0] = 80;
+ this.sprite.speed = 0;
+ }
+ if (this.shooting) {
+ this.sprite.pos[0] += 160;
+ this.shooting -= 1;
+ }
+ }
+
+ if (this.flagging) {
+ this.sprite.pos[0] = 192;
+ this.sprite.frames = [0,1];
+ this.sprite.speed = 10;
+ if (this.vel[1] === 0) this.sprite.frames = [0];
+ }
+
+ //which way are we facing?
+ if (this.left) {
+ this.sprite.img = 'sprites/playerl.png';
+ } else {
+ this.sprite.img = 'sprites/player.png';
+ }
+ };
+
+ Player.prototype.update = function(dt, vX) {
+ if (this.powering.length !== 0) {
+ var next = this.powering.shift();
+ if (next == 5) return;
+ this.sprite.pos = this.powerSprites[next];
+ this.sprite.size = this.powerSizes[next];
+ this.pos[1] += this.shift[next];
+ if (this.powering.length === 0) {
+ delete level.items[this.touchedItem];
+ }
+ return;
+ }
+
+ if (this.invincibility) {
+ this.invincibility -= Math.round(dt * 60);
+ }
+
+ if (this.waiting) {
+ this.waiting -= dt;
+ if (this.waiting <= 0) {
+ this.waiting = 0;
+ } else return;
+ }
+
+ if (this.bounce) {
+ this.bounce = false;
+ this.standing = false;
+ this.vel[1] = -3;
+ }
+
+ if (this.pos[0] <= vX) {
+ this.pos[0] = vX;
+ this.vel[0] = Math.max(this.vel[0], 0);
+ }
+
+ if (Math.abs(this.vel[0]) > this.maxSpeed) {
+ this.vel[0] -= 0.05 * this.vel[0] / Math.abs(this.vel[0]);
+ this.acc[0] = 0;
+ }
+
+ if (this.dying){
+ if (this.pos[1] < this.targetPos[1]) {
+ this.vel[1] = 1;
+ }
+ this.dying -= 1 * dt;
+ if (this.dying <= 0) {
+ player = new Mario.Player(level.playerPos);
+ level.loader.call();
+ input.reset();
+ }
+ }
+ else {
+ this.acc[1] = 0.25
+ if (this.pos[1] > 240) {
+ this.die();
+ }
+ }
+
+ if (this.piping) {
+ this.acc = [0,0];
+ var pos = [Math.round(this.pos[0]), Math.round(this.pos[1])]
+ if (pos[0] === this.targetPos[0] && pos[1] === this.targetPos[1]) {
+ this.piping = false;
+ this.pipeLoc.call();
+ }
+ }
+
+ if (this.flagging) {
+ this.acc = [0,0];
+ }
+
+ if (this.exiting) {
+ this.left = false;
+ this.flagging = false;
+ this.vel[0] = 1.5;
+ if (this.pos[0] >= this.targetPos[0]) {
+ this.sprite.size = [0,0];
+ this.vel = [0,0];
+ window.setTimeout(function() {
+ player.sprite.size = player.power===0 ? [16,16] : [16,32];
+ player.exiting = false;
+ player.noInput = false;
+ level.loader();
+ if (player.power !== 0) player.pos[1] -= 16;
+ music.overworld.currentTime = 0;
+ }, 5000);
+ }
+ }
+
+ //approximate acceleration
+ this.vel[0] += this.acc[0];
+ this.vel[1] += this.acc[1];
+ this.pos[0] += this.vel[0];
+ this.pos[1] += this.vel[1];
+
+ this.setAnimation();
+ this.sprite.update(dt);
+ };
+
+ Player.prototype.checkCollisions = function() {
+ if (this.piping || this.dying) return;
+ //x-axis first!
+ var h = this.power > 0 ? 2 : 1;
+ var w = 1;
+ if (this.pos[1] % 16 !== 0) {
+ h += 1;
+ }
+ if (this.pos[0] % 16 !== 0) {
+ w += 1;
+ }
+ var baseX = Math.floor(this.pos[0] / 16);
+ var baseY = Math.floor(this.pos[1] / 16);
+
+ for (var i = 0; i < h; i++) {
+ if (baseY + i < 0 || baseY + i >= 15) continue;
+ for (var j = 0; j < w; j++) {
+ if (baseY < 0) { i++;}
+ 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);
+ }
+ }
+ }
+ };
+
+ Player.prototype.powerUp = function(idx) {
+ sounds.powerup.play();
+ this.powering = [0,5,2,5,1,5,2,5,1,5,2,5,3,5,1,5,2,5,3,5,1,5,4];
+ this.touchedItem = idx;
+
+ if (this.power === 0) {
+ this.sprite.pos[0] = 80;
+ var newy = this.sprite.pos[1] - 32;
+ this.powerSprites = [[80, newy+32], [80, newy+32], [320, newy], [80, newy], [128, newy]];
+ this.powerSizes = [[16,16],[16,16],[16,32],[16,32],[16,32]];
+ this.shift = [0,16,-16,0,-16];
+ this.power = 1;
+ this.hitbox = [0,0,16,32];
+ } else if (this.power == 1) {
+ var curx = this.sprite.pos[0];
+ this.powerSprites = [[curx, 96], [curx, level.invincibility[0]],
+ [curx, level.invincibility[1]], [curx, level.invincibility[2]],
+ [curx, 96]];
+ this.powerSizes[[16,32],[16,32],[16,32],[16,32],[16,32]];
+ this.shift = [0,0,0,0,0];
+ this.power = 2;
+ } else {
+ this.powering = [];
+ delete level.items[idx];
+ //no animation, but we play the sound and you get 5000 points.
+ }
+ };
+
+ Player.prototype.damage = function() {
+ if (this.power === 0) { //if you're already small, you dead!
+ this.die();
+ } else { //otherwise, you get turned into small mario
+ sounds.pipe.play();
+ this.powering = [0,5,1,5,2,5,1,5,2,5,1,5,2,5,1,5,2,5,1,5,2,5,3];
+ this.shift = [0,16,-16,16];
+ this.sprite.pos = [160, 0];
+ this.powerSprites = [[160,0], [240, 32], [240, 0], [160, 32]];
+ this.powerSizes = [[16, 32], [16,16], [16,32], [16,16]];
+ this.invincibility = 120;
+ this.power = 0;
+ this.hitbox = [0,0,16,16];
+ }
+ };
+
+ Player.prototype.die = function () {
+ //TODO: rewrite the way sounds work to emulate the channels of an NES.
+ music.overworld.pause();
+ music.underground.pause();
+ music.overworld.currentTime = 0;
+ music.death.play();
+ this.noWalk();
+ this.noRun();
+ this.noJump();
+
+ this.acc[0] = 0;
+ this.sprite.pos = [176, 32];
+ this.sprite.speed = 0;
+ this.power = 0;
+ this.waiting = 0.5;
+ this.dying = 2;
+
+ if (this.pos[1] < 240) { //falling into a pit doesn't do the animation.
+ this.targetPos = [this.pos[0], this.pos[1]-128];
+ this.vel = [0,-5];
+ } else {
+ this.vel = [0,0];
+ this.targetPos = [this.pos[0], this.pos[1] - 16];
+ }
+ };
+
+ Player.prototype.star = function(idx) {
+ delete level.items[idx];
+ this.starTime = 660;
+ }
+
+ Player.prototype.pipe = function(direction, destination) {
+ sounds.pipe.play();
+ this.piping = true;
+ this.pipeLoc = destination;
+ switch(direction) {
+ case "LEFT":
+ this.vel = [-1,0];
+ this.targetPos = [Math.round(this.pos[0]-16), Math.round(this.pos[1])]
+ break;
+ case "RIGHT":
+ this.vel = [1,0];
+ this.targetPos = [Math.round(this.pos[0]+16), Math.round(this.pos[1])]
+ break;
+ case "DOWN":
+ this.vel = [0,1];
+ this.targetPos = [Math.round(this.pos[0]), Math.round(this.pos[1]+this.hitbox[3])]
+ break;
+ case "UP":
+ this.vel = [0,-1];
+ this.targetPos = [Math.round(this.pos[0]), Math.round(this.pos[1]-this.hitbox[3])]
+ break;
+ }
+ }
+
+ Player.prototype.flag = function() {
+ this.noInput = true;
+ this.flagging = true;
+ this.vel = [0, 2];
+ this.acc = [0, 0];
+ }
+
+ Player.prototype.exit = function() {
+ this.pos[0] += 16;
+ this.targetPos[0] = level.exit * 16;
+ this.left = true;
+ this.setAnimation();
+ this.waiting = 1;
+ this.exiting = true;
+ }
+})();
diff --git a/public/js/prop.js b/public/js/prop.js
new file mode 100644
index 0000000..8ab2b6c
--- /dev/null
+++ b/public/js/prop.js
@@ -0,0 +1,15 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ //props do even less than entities, so they don't need to inherit really
+ var Prop = Mario.Prop = function(pos, sprite) {
+ this.pos = pos;
+ this.sprite = sprite;
+ }
+
+ //but we will be using the same Render, more or less.
+ Prop.prototype.render = function(ctx, vX, vY) {
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ }
+})();
diff --git a/public/js/resources.js b/public/js/resources.js
new file mode 100644
index 0000000..c3add6d
--- /dev/null
+++ b/public/js/resources.js
@@ -0,0 +1,62 @@
+//simple resource loader
+(function() {
+ var resourceCache = {};
+ var loading = [];
+ var readyCallbacks = [];
+
+ // Load an image url or an array of image urls
+ function load(urlOrArr) {
+ if(urlOrArr instanceof Array) {
+ urlOrArr.forEach(function(url) {
+ _load(url);
+ });
+ }
+ else {
+ _load(urlOrArr);
+ }
+ }
+
+ function _load(url) {
+ if(resourceCache[url]) {
+ return resourceCache[url];
+ }
+ else {
+ var img = new Image();
+ img.onload = function() {
+ resourceCache[url] = img;
+
+ if(isReady()) {
+ readyCallbacks.forEach(function(func) { func(); });
+ }
+ };
+ resourceCache[url] = false;
+ img.src = url;
+ }
+ }
+
+ function get(url) {
+ return resourceCache[url];
+ }
+
+ function isReady() {
+ var ready = true;
+ for(var k in resourceCache) {
+ if(resourceCache.hasOwnProperty(k) &&
+ !resourceCache[k]) {
+ ready = false;
+ }
+ }
+ return ready;
+ }
+
+ function onReady(func) {
+ readyCallbacks.push(func);
+ }
+
+ window.resources = {
+ load: load,
+ get: get,
+ onReady: onReady,
+ isReady: isReady
+ };
+})();
diff --git a/public/js/rubble.js b/public/js/rubble.js
new file mode 100644
index 0000000..98f6e89
--- /dev/null
+++ b/public/js/rubble.js
@@ -0,0 +1,55 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ //TODO: make each rubble an entity, use that render and write in Entity.update
+ var Rubble = Mario.Rubble = function() {
+ this.sprites = [];
+ this.poss = [];
+ this.vels = [];
+ }
+
+ Rubble.prototype.spawn = function(pos) {
+ this.idx = level.items.length;
+ level.items.push(this);
+ this.sprites[0] = level.rubbleSprite();
+ this.sprites[1] = level.rubbleSprite();
+ this.sprites[2] = level.rubbleSprite();
+ this.sprites[3] = level.rubbleSprite();
+ this.poss[0] = pos;
+ this.poss[1] = [ pos[0] + 8, pos[1] ];
+ this.poss[2] = [ pos[0], pos[1] + 8 ];
+ this.poss[3] = [ pos[0] + 8, pos[1] + 8 ];
+ this.vels[0] = [-1.25, -5];
+ this.vels[1] = [1.25, -5];
+ this.vels[2] = [-1.25, -3];
+ this.vels[3] = [1.25, -3];
+ }
+
+ Rubble.prototype.update = function(dt) {
+ for(var i = 0; i < 4; i++) {
+ if (this.sprites[i]===undefined) continue;
+ this.vels[i][1] += .3;
+ this.poss[i][0] += this.vels[i][0];
+ this.poss[i][1] += this.vels[i][1];
+ this.sprites[i].update(dt);
+ if (this.poss[i][1] > 256) {
+ delete this.sprites[i];
+ }
+ }
+ if (this.sprites.every(function (el) {return !el})) {
+ delete level.items[this.idx];
+ }
+ }
+
+ //You might argue that things that can't collide are more like scenery
+ //but these move and need to be deleted, and i'd rather deal with the 1d array.
+ Rubble.prototype.checkCollisions = function() {;}
+
+ Rubble.prototype.render = function() {
+ for(var i = 0; i < 4; i++) {
+ if (this.sprites[i] === undefined) continue;
+ this.sprites[i].render(ctx, this.poss[i][0], this.poss[i][1], vX, vY);
+ }
+ }
+})();
diff --git a/public/js/sprite.js b/public/js/sprite.js
new file mode 100644
index 0000000..94b8d6c
--- /dev/null
+++ b/public/js/sprite.js
@@ -0,0 +1,47 @@
+(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/public/js/star.js b/public/js/star.js
new file mode 100644
index 0000000..dc27b1d
--- /dev/null
+++ b/public/js/star.js
@@ -0,0 +1,116 @@
+(function() {
+ if (typeof Mario === 'undefined')
+ window.Mario = {};
+
+ var Star = Mario.Star = function(pos) {
+ this.spawning = false;
+ this.waiting = 0;
+
+ Mario.Entity.call(this, {
+ pos: pos,
+ sprite: level.starSprite,
+ hitbox: [0,0,16,16]
+ });
+ }
+
+ Mario.Util.inherits(Star, Mario.Entity);
+
+ Star.prototype.render = function(ctx, vX, vY) {
+ if (this.spawning > 1) return;
+ this.sprite.render(ctx, this.pos[0], this.pos[1], vX, vY);
+ }
+
+ Star.prototype.spawn = function() {
+ 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;
+ }
+
+ Star.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.standing) {
+ this.standing = false;
+ this.vel[1] = -3;
+ }
+
+ 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);
+ }
+ }
+
+ Star.prototype.collideWall = function() {
+ this.vel[0] = -this.vel[0];
+ }
+
+ Star.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.
+ Star.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.star(this.idx);
+ }
+ }
+ }
+
+ Star.prototype.bump = function() {
+ this.vel[1] = -2;
+ }
+
+})();
diff --git a/public/js/util.js b/public/js/util.js
new file mode 100644
index 0000000..99b928e
--- /dev/null
+++ b/public/js/util.js
@@ -0,0 +1,14 @@
+(function() {
+ if (typeof Mario === 'undefined') {
+ window.Mario = {};
+ }
+
+ var Util = Mario.Util = {};
+
+ Util.inherits = function(subclass, superclass) {
+ function Surrogate() {};
+
+ Surrogate.prototype = superclass.prototype;
+ subclass.prototype = new Surrogate();
+ }
+})()
\ No newline at end of file
diff --git a/public/sounds/aboveground_bgm.ogg b/public/sounds/aboveground_bgm.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..4920b63ea76eee2b40a558139f17ea7d1d9826a2
GIT binary patch
literal 2486172
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