A Simple Side-Scrolling Platformer Game in JavaScript

The JavaScript code shown below implements a very simple side-scrolling platformer game in JavaScript.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/sidescroller.html.

Use the A and D keys to move left and right, and W or the spacebar to jump. The object is to get from the left side to the right side of the level without getting hit by any of the bad guys.

Platformer

<html>
<body>

<div id="divMain"></div>

<script type='text/javascript'>

// main

function main()
{
  var level = new Level
	(
		"Level 1",
		new Coords(300, 100), // size
		new Coords(0, 1), // accelerationDueToGravity
		.15, // velocityMin
		.4, // friction
		// edges
		[
			new Edge( [ new Coords(5, 50), new Coords(25, 50) ] ),
			new Edge( [ new Coords(25, 50), new Coords(45, 45) ] ),
			new Edge( [ new Coords(45, 45), new Coords(55, 50) ] ),
			new Edge( [ new Coords(35, 60), new Coords(65, 60) ] ),		
			new Edge( [ new Coords(65, 50), new Coords(85, 50) ] ),
			new Edge( [ new Coords(85, 40), new Coords(115, 40) ] ),
			new Edge( [ new Coords(105, 30), new Coords(185, 30) ] ),
			new Edge( [ new Coords(185, 40), new Coords(265, 40) ] ),
			new Edge( [ new Coords(280, 60), new Coords(295, 60) ] ),
			new Edge( [ new Coords(240, 70), new Coords(275, 70) ] ),
			new Edge( [ new Coords(200, 80), new Coords(220, 80) ] ),
		]
	);

	var bodyDefnPlayer = new BodyDefn
	(
		"Player",
		1, // accelerationRun
		.2, // accelerationFly
		1, // velocityMaxRun
		6, // accelerationJump
		8, // velocityMaxFlying
		new IntelligenceDefnHuman(),
		new Face
		([
			new Edge([ new Coords(0, 0), new Coords(0, -12) ]),
			new Edge([ new Coords(0, -12), new Coords(0, 0) ]),
		])
	);

	var bodyForPlayer = new Body
	(
		"Player0",
		bodyDefnPlayer,
		new Coords(10, 10)
	);

	var bodyDefnEnemy = new BodyDefn
	(
		"Enemy",
		.4, // accelerationRun
		0, // accelerationFly
		.5, // velocityMaxRun
		6, // accelerationJump
		8, // velocityMaxFlying
		new IntelligenceDefnPatroller(),
		new Face
		([
			new Edge([ new Coords(-4, 0), new Coords(-3, -6) ]),
			new Edge([ new Coords(-3, -6), new Coords(3, -6) ]),
			new Edge([ new Coords(3, -6), new Coords(4, 0) ]),
			new Edge([ new Coords(4, 0), new Coords(-4, 0) ]),
		])
	);

	var levelRun = new LevelRun
	(
		level,
		// movers
		[
			bodyForPlayer,
			new Body
			(
				"Enemy0",
				bodyDefnEnemy,
				new Coords(100, 10)
			),
			new Body
			(
				"Enemy1",
				bodyDefnEnemy,
				new Coords(130, 10)
			),
			new Body
			(
				"Enemy2",
				bodyDefnEnemy,
				new Coords(150, 10)
			),
			new Body
			(
				"Enemy1",
				bodyDefnEnemy,
				new Coords(150, 10)
			),
		]
	);

	var displayHelper = new DisplayHelper
	(
		new Coords(100, 100) // viewSize
	);

	Globals.Instance.initialize
	(
		displayHelper,
		levelRun
	)
}

// classes

function Body(name, defn, pos)
{
	this.name = name;
	this.defn = defn;
	this.pos = pos;

	this.vel = new Coords(0, 0);
	this.edgeBeingStoodOn = null;
	if (this.defn.face != null)
	{
		this.face = this.defn.face.clone();
	}

	if (this.defn.intelligenceDefn != null)
	{
		this.intelligence = new Intelligence
		(
			this.defn.intelligenceDefn
		);
	}

	this.collisionMapCellsOccupied = [];

	this.integrity = 1;
}

function BodyDefn
(
	name, 
	accelerationRun,
	accelerationFly,
	velocityMaxRunning,
	accelerationJump,
	velocityMaxFlying,
	intelligenceDefn,
	face
)
{
	this.name = name;
	this.accelerationRun = accelerationRun;
	this.accelerationFly = accelerationFly;
	this.velocityMaxRunning = velocityMaxRunning;
	this.accelerationJump = accelerationJump;
	this.velocityMaxFlying = velocityMaxFlying;
	this.intelligenceDefn = intelligenceDefn;
	this.face = face;
}

function Bounds(min, max)
{
	this.min = min;
	this.max = max;

	this.minAndMax = [ this.min, this.max ];
}
{
	Bounds.prototype.overwriteWithBoundsOfCoordsMany = function(coordsSetToFindBoundsOf)
	{
		this.min.overwriteWith(coordsSetToFindBoundsOf[0]);
		this.max.overwriteWith(this.min);

		for (var i = 0; i < coordsSetToFindBoundsOf.length; i++)
		{
			var coordsToCheck = coordsSetToFindBoundsOf[i];

			for (var d = 0; d < Coords.NumberOfDimensions; d++)
			{
				var dimensionValueToCheck = coordsToCheck.dimension(d);

				if (dimensionValueToCheck < this.min.dimension(d))
				{
					this.min.dimension_Set(d, dimensionValueToCheck);
				}

				if (dimensionValueToCheck > this.max.dimension(d))
				{
					this.max.dimension_Set(d, dimensionValueToCheck);
				}
				
			}
		}

		return this;
	}
}

function Cloneable()
{
	// static class
}
{
	Cloneable.cloneMany = function(cloneablesToClone)
	{
		var returnValues = [];

		for (var i = 0; i < cloneablesToClone.length; i++)
		{
			var cloneableToClone = cloneablesToClone[i];
			var cloneableCloned = cloneableToClone.clone();
			returnValues.push(cloneableCloned);
		}

		return returnValues;
	}
}

function Collision(pos, distance, edgeCollidedWith, bodyCollidedWith)
{
	this.pos = pos;
	this.distance = distance;
	this.edgeCollidedWith = edgeCollidedWith;
	this.bodyCollidedWith = bodyCollidedWith;
}
{
	// static variables

	Collision.bounds = 
	[
		new Bounds(new Coords(0, 0), new Coords(0, 0)),
		new Bounds(new Coords(0, 0), new Coords(0, 0)),
	];

	// static methods

	Collision.closestInList = function(collisionsToCheck)
	{
		var collisionClosest = collisionsToCheck[0];
		
		for (var i = 1; i < collisionsToCheck.length; i++)
		{
			var collision = collisionsToCheck[i];
			if (collision.distance < collisionClosest.distance)
			{
				collisionClosest = collision;
			}
		}

		return collisionClosest;
	}

	Collision.doBoundsOverlap = function(bounds0, bounds1)
	{
		var returnValue = false;

		var bounds = Collision.bounds;

		bounds[0] = bounds0;
		bounds[1] = bounds1;

		for (var b = 0; b < bounds.length; b++)
		{
			var boundsThis = bounds[b];
			var boundsOther = bounds[1 - b];			

			var doAllDimensionsOverlapSoFar = true;

			for (var d = 0; d < Coords.NumberOfDimensions; d++)
			{
				if 
				(
					boundsThis.max.dimension(d) < boundsOther.min.dimension(d)
					|| boundsThis.min.dimension(d) > boundsOther.max.dimension(d)
				)
				{
					doAllDimensionsOverlapSoFar = false;
					break;
				}
			}

			if (doAllDimensionsOverlapSoFar == true)
			{
				returnValue = true;
				break;
			}
		}

		return returnValue;
	}

	Collision.edgeWithOther = function(edge0, edge1)
	{
		var returnValue;

		var doBoundsOverlap = Collision.doBoundsOverlap
		(
			edge0.bounds,
			edge1.bounds
		);

		if (doBoundsOverlap == true)
		{
			var edge0ProjectedOntoEdge1 = edge0.clone().projectOntoOther
			(
				edge1
			);

			var distanceAlongEdge0ToEdge1 = 
				0 
				- edge0ProjectedOntoEdge1.vertices[0].y 
				/ edge0ProjectedOntoEdge1.direction.y;
	
			// Because the math's not perfect...
			var correctionFactor = 1; 

			if 
			(
				distanceAlongEdge0ToEdge1 >= 0 - correctionFactor
				&& distanceAlongEdge0ToEdge1 <= edge0.length + correctionFactor
			)
			{
				var collisionPos = edge0.vertices[0].clone().add
				(
					edge0.direction.clone().multiplyScalar
					(
						distanceAlongEdge0ToEdge1
					)
				);
				
				returnValue = new Collision
				(
					collisionPos,
					distanceAlongEdge0ToEdge1,
					edge1,
					null // bodyCollidedWith
				);
			}
		}
		else
		{
			returnValue = null;
		}

		return returnValue;
	}

	Collision.moverWithEdge = function(mover, edge)
	{
		var returnValue = null;

		if (mover.vel.dotProduct(edge.right) >= 0)
		{
			var moverEdge = new Edge
			([
				mover.pos,
				mover.pos.clone().add(mover.vel)
			]);	

			returnValue = Collision.edgeWithOther
			(
				moverEdge,
				edge
			);
		}
	
		return returnValue;
	}

	Collision.moverWithOther = function(mover0, mover1)
	{
		var returnValue = null;

		if (mover1.name.indexOf("Edge") == 0)
		{
			var edges = mover1.face.edges;
			for (var i = 0; i < edges.length; i++)
			{
				var edge = edges[i];
				var collision = Collision.moverWithEdge(mover0, edge);
				if (collision != null)
				{
					collision.bodyCollidedWith = mover1;
					returnValue = collision;
				}
			}
		}
		else // if mover
		{
			var doBoundsOverlap = Collision.doBoundsOverlap
			(
				mover0.face.bounds,
				mover1.face.bounds
			);

			if (doBoundsOverlap == true)
			{
				returnValue = new Collision
				(
					mover0.pos,
					0, // distance
					null, // edgeCollidedWith
					mover1 // bodyCollidedWith
				);
			}
		}

		return returnValue;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	// constants

	Coords.NumberOfDimensions = 2;

	// instance methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;

		return this;
	}

	Coords.prototype.ceiling = function()
	{
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);

		return this;
	}

	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;

		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.dimension = function(dimensionIndex)
	{
		return (dimensionIndex == 0 ? this.x : this.y);
	}

	Coords.prototype.dimension_Set = function(dimensionIndex, dimensionValue)
	{
		if (dimensionIndex == 0)
		{
			this.x = dimensionValue;
		}
		else
		{
			this.y = dimensionValue;
		}
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;

		return this;
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;

		return this;
	}
	
	Coords.prototype.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y;
	}

	Coords.prototype.floor = function()
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);

		return this;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;

		return this;
	}

	Coords.prototype.normalize = function(scalar)
	{
		return this.divideScalar(this.magnitude());
	}

	Coords.prototype.right = function()
	{
		var temp = this.y;
		this.y = this.x;
		this.x = 0 - temp;

		return this;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;

		return this;
	}

	Coords.prototype.overwriteWithXY = function(x, y)
	{
		this.x = x;
		this.y = y;

		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;

		return this;
	}

	Coords.prototype.trimToMagnitude = function(magnitudeToTrimTo)
	{
		var magnitude = this.magnitude();

		if (magnitude > magnitudeToTrimTo)
		{
			this.divideScalar
			(
				magnitude
			).multiplyScalar
			(
				magnitudeToTrimTo
			);
		}
	}

	Coords.prototype.trimToRange = function(range)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > range.x)
		{
			this.x = range.x;
		}

		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > range.y)
		{
			this.y = range.y;
		}

		return this;
	}
}

function Debug()
{
	// static class
}
{
	Debug._idNext = 0;
	Debug.idNext = function()
	{
		this._idNext++;
		return this._idNext;
	}
}

function DisplayHelper(viewSize)
{
	this.viewSize = viewSize;
	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	DisplayHelper.prototype.clear = function()
	{
		this.graphics.fillStyle = "White";
		this.graphics.strokeStyle = "LightGray";
		this.graphics.fillRect
		(
			0, 0, 
			this.viewSize.x, this.viewSize.y
		);
		this.graphics.strokeRect
		(
			0, 0, 
			this.viewSize.x, this.viewSize.y
		);
	}

	DisplayHelper.prototype.drawBody = function(body, cameraPos)
	{
		var vertices = body.face.vertices;

		for (var v = 0; v < vertices.length; v++)
		{
			var vertex = vertices[v];
			var vNext = v + 1;
			if (vNext >= vertices.length)
			{
				vNext = 0;
			}
			var vertexNext = vertices[vNext];
	
			this.drawLine
			(
				vertex,
				vertexNext,
				cameraPos
			);
		}
	}

	DisplayHelper.prototype.drawBounds = function(bounds, cameraPos)
	{
		var drawPos = this.drawPos.overwriteWith
		(
			bounds.min
		).subtract
		(
			cameraPos
		);

		var drawSize = this.drawSize.overwriteWith
		(
			bounds.max
		).subtract
		(
			bounds.min
		);

		this.graphics.strokeStyle = "LightGray";
		this.graphics.strokeRect
		(
			drawPos.x, 
			drawPos.y,
			drawSize.x,
			drawSize.y
		);
	}

	DisplayHelper.prototype.drawEdge = function(edge, cameraPos)
	{
		this.drawPos.overwriteWith
		(
			edge.vertices[0]
		);

		this.drawPos2.overwriteWith
		(
			edge.vertices[1]
		)

		this.drawLine
		(
			this.drawPos, 
			this.drawPos2,
			cameraPos
		);
	}

	DisplayHelper.prototype.drawLine = function(startPos, endPos, cameraPos, color)
	{
		if (color == null)
		{
			color = "LightGray";
		}

		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo
		(
			startPos.x - cameraPos.x, 
			startPos.y - cameraPos.y
		);
		this.graphics.lineTo
		(
			endPos.x - cameraPos.x, 
			endPos.y - cameraPos.y
		);
		this.graphics.stroke();
	}

	DisplayHelper.prototype.drawLevelRun = function(levelRun)
	{
		this.clear();

		var bodyForPlayer = levelRun.bodyForPlayer;
		var cameraPos = levelRun.cameraPos;

		cameraPos.overwriteWith
		(
			bodyForPlayer.pos
		).subtract
		(
			this.viewSizeHalf
		).trimToRange
		(
			levelRun.cameraRange
		);

		var cameraViewBounds = new Bounds
		(
			cameraPos,
			cameraPos.clone().add(levelRun.cameraRange)
		);

		//var bodies = levelRun.bodies;
		var bodies = levelRun.collisionMap.bodiesInBoundsAddToList
		(
			cameraViewBounds,
			[]
		);

		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];

			this.drawBody(body, cameraPos);
		}
	}

	DisplayHelper.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.viewSize.x;
		canvas.height = this.viewSize.y;

		this.graphics = canvas.getContext("2d");
		
		document.getElementById("divMain").appendChild(canvas);

		this.drawPos = new Coords(0, 0);
		this.drawPos2 = new Coords(0, 0);
		this.drawSize = new Coords(0, 0);
	}
}

function Edge(vertices)
{
this.id = Debug.idNext();
	this.vertices = vertices;

	this.defn = new BodyDefn
	(
		Edge.BodyDefnName // name
	);

	this.displacement = new Coords(0, 0);
	this.direction = new Coords(0, 0);
	this.right = new Coords(0, 0);
	this.bounds = new Bounds(new Coords(0, 0), new Coords(0, 0));

	this.recalculateDerivedValues();
}
{
	// constants

	Edge.BodyDefnName = "Edge";
	Edge.Thickness = 4;

	// methods

	Edge.prototype.clone = function()
	{
		return new Edge
		(
			[ this.vertices[0].clone(), this.vertices[1].clone() ]
		);
	}

	Edge.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertexThis = this.vertices[i];
			var vertexOther = other.vertices[i];
			vertexThis.overwriteWith(vertexOther);
		}

		this.recalculateDerivedValues();
	}

	Edge.prototype.projectOntoOther = function(other)
	{
		for (var v = 0; v < this.vertices.length; v++)
		{
			var vertexToProject = this.vertices[v];

			vertexToProject.subtract
			(
				other.vertices[0]
			).overwriteWithXY
			(
				vertexToProject.dotProduct(other.direction),
				vertexToProject.dotProduct(other.right)
			);
		}

		this.recalculateDerivedValues();

		return this;
	}

	Edge.prototype.recalculateDerivedValues = function()
	{
		this.displacement.overwriteWith
		(
			this.vertices[1]
		).subtract
		(
			this.vertices[0]
		);

		this.length = this.displacement.magnitude();

		this.direction.overwriteWith
		(
			this.displacement
		).divideScalar
		(
			this.length
		);

		this.right.overwriteWith
		(
			this.direction
		).right();
	
		this.bounds.overwriteWithBoundsOfCoordsMany
		(
			this.vertices
		);
	}
}

function Face(edges)
{
	this.edges = edges;
	this.vertices = [];
	for (var i = 0; i < this.edges.length; i++)
	{
		var edge = this.edges[i];
		var vertex = edge.vertices[0];
		this.vertices.push(vertex);
	}

	if (this.edges.length == 1)
	{
		this.vertices.push(this.edges[0].vertices[1]);
	}

	this.bounds = new Bounds(new Coords(0, 0), new Coords(0, 0));

	this.recalculateDerivedValues();
}
{
	Face.prototype.clone = function()
	{
		return new Face(Cloneable.cloneMany(this.edges));
	}

	Face.prototype.overwriteWith = function(other)
	{
		for (var e = 0; e < this.edges.length; e++)
		{
			var edge = this.edges[e];
			var edgeOther = other.edges[e];
			edge.overwriteWith(edgeOther);
		}

		this.recalculateDerivedValues();

		return this;
	}

	Face.prototype.recalculateDerivedValues = function()
	{
		this.bounds.overwriteWithBoundsOfCoordsMany(this.vertices);
	}

	Face.prototype.transform = function(transformToApply)
	{
		Transform.applyTransformToCoordsMany
		(
			transformToApply,
			this.vertices
		);

		this.recalculateDerivedValues();

		return this;	
	}
}

function Globals()
{}
{
	Globals.Instance = new Globals();

	Globals.prototype.handleEventTimerTick = function()
	{
		this.inputHelper.updateForTimerTick();
		this.levelRun.updateForTimerTick();
	}

	Globals.prototype.initialize = function(displayHelper, levelRun)
	{	
		this.displayHelper = displayHelper;
		this.displayHelper.initialize();

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();

		this.levelRun = levelRun;
		this.levelRun.initialize();

		var millisecondsPerTimerTick = 50;

		this.timer = setInterval
		(
			"Globals.Instance.handleEventTimerTick();", 
			millisecondsPerTimerTick
		);
	}

	Globals.prototype.finalize = function()
	{
		clearInterval(this.timer);
	}
}

function InputHelper()
{}
{
	InputHelper.prototype.initialize = function()
	{
		this.keyCodesPressed = [];
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var keyCode = event.keyCode;
		this.keyCodesPressed[keyCode] = keyCode;
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		var keyCode = event.keyCode;
		delete this.keyCodesPressed[keyCode];
	}

	InputHelper.prototype.updateForTimerTick = function()
	{
		// todo
	}
}

function Intelligence(defn)
{
	this.defn = defn;
	this.defn.initializeIntelligence(this);
}
{
	Intelligence.prototype.decideActionForMover = function(mover)
	{
		this.defn.decideActionForMover(this, mover);
	}
}

function IntelligenceDefnCharger()
{
	// do nothing
}
{
	IntelligenceDefnCharger.prototype.decideActionForMover = function(intelligence, mover)
	{
		mover.vel.x -= mover.defn.accelerationRun;
	}

	IntelligenceDefnCharger.prototype.initializeIntelligence = function(intelligence)
	{
		// do nothing
	}
}

function IntelligenceDefnNone()
{
	// do nothing
}
{
	IntelligenceDefnNone.prototype.decideActionForMover = function(intelligence, mover)
	{
		// do nothing
	}

	IntelligenceDefnNone.prototype.initializeIntelligence = function(intelligence)
	{
		// do nothing
	}
}

function IntelligenceDefnHuman()
{
	// do nothing	
}
{
	IntelligenceDefnHuman.prototype.decideActionForMover = function(intelligence, mover)
	{
		var player = mover;

		var inputHelper = Globals.Instance.inputHelper;

		for (var keyCode in inputHelper.keyCodesPressed)
		{
			if 
			(
				keyCode == "32" // spacebar
				|| keyCode == "87" // w
			)
			{
				if (player.edgeBeingStoodOn != null)
				{
					player.edgeBeingStoodOn = null;
					player.vel.y -= player.defn.accelerationJump;
					delete inputHelper.keyCodesPressed[keyCode];
				}
			}
			else if (keyCode == "65") // a
			{
				var acceleration;

				if (player.edgeBeingStoodOn == null)
				{
					acceleration = player.defn.accelerationFly;
				}
				else
				{
					acceleration = player.defn.accelerationRun;
				}

				player.vel.x -= acceleration;
			}
			else if (keyCode == "68") // d
			{
				var acceleration;

				if (player.edgeBeingStoodOn == null)
				{
					acceleration = player.defn.accelerationFly;
				}
				else
				{
					acceleration = player.defn.accelerationRun;
				}

				player.vel.x += acceleration;
			}
		}	
	}

	IntelligenceDefnHuman.prototype.initializeIntelligence = function(intelligence)
	{
		// do nothing
	}
}

function IntelligenceDefnPatroller()
{
	this.distanceToExtremeMin = 5;
}
{
	IntelligenceDefnPatroller.prototype.decideActionForMover = function(intelligence, mover)
	{
		var edgeBeingStoodOn = mover.edgeBeingStoodOn;

		if (edgeBeingStoodOn  != null)
		{
			var extremeIndex = (intelligence.directionCurrent > 0 ? 1 : 0);
			var edgeExtremeAhead = edgeBeingStoodOn.bounds.minAndMax[extremeIndex];
			var distanceToExtreme = Math.abs
			(
				edgeExtremeAhead.x
				- mover.pos.x
			);

			if (distanceToExtreme < this.distanceToExtremeMin)
			{
				intelligence.directionCurrent *= -1;
			}
	
			mover.vel.x += 
				mover.defn.accelerationRun 
				* intelligence.directionCurrent;
		}
	}

	IntelligenceDefnPatroller.prototype.initializeIntelligence = function(intelligence)
	{
		intelligence.directionCurrent = 1;
	}
}

function Level
(
	name, 
	size, 
	accelerationDueToGravity, 
	velocityMin, 
	friction, 
	edges
)
{
	this.name = name;
	this.size = size;
	this.accelerationDueToGravity = accelerationDueToGravity;
	this.velocityMin = velocityMin;
	this.friction = friction;
	this.edges = edges;
}

function LevelRun(level, movers)
{
	this.level = level;
	this.movers = movers;
	this.bodyForPlayer = this.movers[0];

	this.bodies = [];
	this.bodies = this.bodies.concat(this.movers);

	this.moversToRemove = [];
}
{
	LevelRun.prototype.initialize = function()
	{
		this.cameraPos = new Coords(0, 0);
		this.cameraRange = this.level.size.clone().subtract
		(
			Globals.Instance.displayHelper.viewSize
		);

		this.collisionMap = new Map
		(
			new Coords(32, 1), // sizeInCells,
			this.level.size // sizeInPixels
		);
	
		var edges = this.level.edges;

		for (var i = 0; i < edges.length; i++)
		{
			var edge = edges[i];

			var bodyDefnForEdge = new BodyDefn
			(
				"Edge" + i,
				0, // accelerationRun
				0, // accelerationFly
				0, // velocityMaxRun
				0, // accelerationJump
				0, // velocityMaxFlying
				null, // ?
				new Face
				([
					edge, 
				])
			);

			var bodyForEdge = new Body
			(
				"Edge" + i,
				bodyDefnForEdge,
				new Coords(0, 0) // pos
			)

			this.bodies.push(bodyForEdge);

			var edgeBounds = edge.bounds;
			var cells = this.collisionMap.cellsInBoundsAddToList
			(
				edgeBounds, []
			);

			for (var c = 0; c < cells.length; c++)
			{
				var cell = cells[c];
				cell.bodiesPresent.push(bodyForEdge);
			}
		}

		var movers = this.movers;
		for (var i = 0; i < movers.length; i++)
		{
			var mover = movers[i];

			mover.face.overwriteWith(mover.defn.face).transform
			(
				new TransformTranslate(mover.pos)
			);
		}
	}

	LevelRun.prototype.updateForTimerTick = function()
	{
		this.updateForTimerTick_Intelligence();
		this.updateForTimerTick_Physics();
		this.updateForTimerTick_WinOrLose();
		Globals.Instance.displayHelper.drawLevelRun(this)
	}

	LevelRun.prototype.updateForTimerTick_Intelligence = function()
	{
		for (var m = 0; m < this.movers.length; m++)
		{
			var mover = this.movers[m];
			var intelligence = mover.intelligence;
			intelligence.decideActionForMover(mover);
		}
	}

	LevelRun.prototype.updateForTimerTick_Physics = function()
	{
		for (var m = 0; m < this.movers.length; m++)
		{
			var mover = this.movers[m];

			var collisionMapCells = mover.collisionMapCellsOccupied;
			for (var i = 0; i < collisionMapCells.length; i++)
			{
				var cell = collisionMapCells[i];
				var cellBodies = cell.bodiesPresent;

				cellBodies.splice
				(
					cellBodies.indexOf(mover),
					1
				);
			}

			collisionMapCells.length = 0;
			
			this.collisionMap.cellsInBoundsAddToList
			(
				mover.face.bounds, collisionMapCells
			);

			for (var i = 0; i < collisionMapCells.length; i++)
			{
				var cell = collisionMapCells[i];
				var cellBodies = cell.bodiesPresent;

				cellBodies.push(mover);
			}
		}

		for (var m = 0; m < this.movers.length; m++)
		{
			var mover = this.movers[m];
			var moverDefn = mover.defn;

			mover.vel.add
			(
				this.level.accelerationDueToGravity
			).trimToMagnitude
			(
				mover.defn.velocityMaxFlying
			);

			var bodiesToCollideWith = [];

			this.updateForTimerTick_Physics_CollidableBodiesAddToList
			(
				mover, 
				bodiesToCollideWith
			);

			this.updateForTimerTick_Physics_BodiesCollideWithMover
			(
				mover,
				bodiesToCollideWith
			);

			if (mover.edgeBeingStoodOn != null)
			{
				var edge = mover.edgeBeingStoodOn;
				var edgeTangent = edge.direction;
	
				var accelerationAlongEdge = edgeTangent.clone().multiplyScalar
				(
					this.level.friction 
					* mover.vel.dotProduct(edgeTangent)
				);
	
				mover.vel.subtract
				(
					accelerationAlongEdge
				);
	
				if (mover.vel.magnitude() < this.level.velocityMin)
				{
					mover.vel.clear();
				}
			}

			mover.pos.add
			(
				mover.vel
			);

			mover.face.overwriteWith(mover.defn.face).transform
			(
				new TransformTranslate(mover.pos)
			);

			if (mover.pos.y >= this.level.size.y * 2)
			{
				this.moversToRemove.push(mover);
			}
		}

		for (var i = 0; i < this.moversToRemove.length; i++)
		{
			var mover = this.moversToRemove[i];
			this.movers.splice
			(
				this.movers.indexOf(mover),
				1
			);
		}

		this.moversToRemove.length = 0;
	}

	LevelRun.prototype.updateForTimerTick_Physics_CollidableBodiesAddToList = function
	(
		mover,
		bodiesToCollideWith
	)
	{	
		if (mover.integrity <= 0)
		{
			return;
		}

		var moverBounds = new Bounds
		(
			mover.pos,
			mover.pos.clone().add(mover.vel)
		);

		this.collisionMap.bodiesInBoundsAddToList
		(
			moverBounds, bodiesToCollideWith
		);
	}

	LevelRun.prototype.updateForTimerTick_Physics_BodiesCollideWithMover = function
	(
		mover, 
		bodiesToCollideWith
	)
	{
		mover.edgeBeingStoodOn = null;

		while (true)
		{
			var collisionsOfMoverWithOthers = [];

			for (var i = 0; i < bodiesToCollideWith.length; i++)
			{
				var other = bodiesToCollideWith[i];

				if (mover != other)
				{
					var collisionOfMoverWithOther = Collision.moverWithOther
					(
						mover, 
						other
					);
			
					if (collisionOfMoverWithOther != null)
					{
						collisionsOfMoverWithOthers.push
						(
							collisionOfMoverWithOther
						);
					}
				}
			}

			if (collisionsOfMoverWithOthers.length == 0)
			{
				break;
			}
			else
			{	
				var collisionOfMoverWithOther = Collision.closestInList
				(
					collisionsOfMoverWithOthers
				);
	
				mover.pos.overwriteWith
				(
					collisionOfMoverWithOther.pos
				);

				var bodyCollidedWith = collisionOfMoverWithOther.bodyCollidedWith;

				bodiesToCollideWith.splice
				(
					bodiesToCollideWith.indexOf(bodyCollidedWith),
					1
				);

				var bodyCollidedWithDefnName = bodyCollidedWith.defn.name;


				if (bodyCollidedWithDefnName.indexOf("Enemy") == 0)
				{
					if (mover == this.bodyForPlayer)
					{
						var player = mover;
						var enemy = bodyCollidedWith;

						if (enemy.integrity > 0)
						{
							if (player.pos.y >= enemy.pos.y)
							{
								player.integrity = 0;
								player.vel.x += 3 * enemy.vel.x / Math.abs(enemy.vel.x);
								player.intelligence.defn = new IntelligenceDefnNone();
							}
							else 
							{
								player.vel.y -= 3;
								enemy.integrity = 0;
								enemy.intelligence.defn = new IntelligenceDefnNone();
							}
						}
					}

				}
				else // if (bodyCollidedWithDefnName.indexOf("Edge") == 0)
				{
					var edgeCollidedWith = bodyCollidedWith.face.edges[0];

					if (mover.edgeBeingStoodOn == null)
					{
						mover.edgeBeingStoodOn = edgeCollidedWith;
					}
	
					if (mover.vel.magnitude() > 0)
					{
						var edgeNormal = edgeCollidedWith.right;
			
						var speedAlongEdgeNormal = mover.vel.dotProduct
						(
							edgeNormal
						);
							
						var velocityCancelledByEdge = edgeNormal.clone().multiplyScalar
						(
							speedAlongEdgeNormal
						);
	
						mover.vel.subtract
						(
							velocityCancelledByEdge
						);
					}
				}
			}
		}
	}	


	LevelRun.prototype.updateForTimerTick_WinOrLose = function()
	{
		if (this.bodyForPlayer.pos.y >= this.level.size.y * 2)
		{
			document.write("Game Over");
			Globals.Instance.finalize();
		}
		else if (this.bodyForPlayer.pos.x >= this.level.size.x)
		{
			document.write("You win!");
			Globals.Instance.finalize();
		}
	}
}

function Map(sizeInCells, sizeInPixels)
{
	this.sizeInCells = sizeInCells;
	this.sizeInPixels = sizeInPixels;

	this.sizeInCellsMinusOnes = this.sizeInCells.clone().subtract
	(
		new Coords(1, 1)
	);

	this.cellSizeInPixels = this.sizeInPixels.clone().divide
	(
		this.sizeInCells
	);

	var numberOfCells = this.sizeInCells.x * this.sizeInCells.y;
	this.cells = [];

	for (var i = 0; i < numberOfCells; i++)
	{
		var cell = new MapCell();
		this.cells.push(cell);
	}
}
{
	Map.prototype.cellAtPos = function(cellPos)
	{
		return this.cells[this.indexOfCellAtPos(cellPos)];
	}

	Map.prototype.bodiesInBoundsAddToList = function(boundsInPixels, listToAddTo)
	{
		var boundsMinInCells = boundsInPixels.min.clone().divide
		(
			this.cellSizeInPixels
		).floor().trimToRange
		(
			this.sizeInCellsMinusOnes
		);

		var boundsMaxInCells = boundsInPixels.max.clone().divide
		(
			this.cellSizeInPixels
		).ceiling().trimToRange
		(
			this.sizeInCellsMinusOnes
		);

		var cellPos = new Coords(0, 0);

		for (var y = boundsMinInCells.y; y <= boundsMaxInCells.y; y++)
		{
			cellPos.y = y;

			for (var x = boundsMinInCells.x; x <= boundsMaxInCells.x; x++)
			{
				cellPos.x = x;

				var cell = this.cellAtPos(cellPos);
				var bodiesInCell = cell.bodiesPresent;

				for (var b = 0; b < bodiesInCell.length; b++)
				{
					var body = bodiesInCell[b];
					if (listToAddTo.indexOf(body) == -1)
					{
						listToAddTo.push(body);
					}
				}
			}
		}

		return listToAddTo;
	}

	Map.prototype.cellsInBoundsAddToList = function(boundsInPixels, listToAddTo)
	{
		var boundsMinInCells = boundsInPixels.min.clone().divide
		(
			this.cellSizeInPixels
		).floor().trimToRange
		(
			this.sizeInCellsMinusOnes
		);

		var boundsMaxInCells = boundsInPixels.max.clone().divide
		(
			this.cellSizeInPixels
		).ceiling().trimToRange
		(
			this.sizeInCellsMinusOnes
		);

		var cellPos = new Coords(0, 0);

		for (var y = boundsMinInCells.y; y <= boundsMaxInCells.y; y++)
		{
			cellPos.y = y;

			for (var x = boundsMinInCells.x; x <= boundsMaxInCells.x; x++)
			{
				cellPos.x = x;

				var cell = this.cellAtPos(cellPos);

				listToAddTo.push(cell);
			}
		}

		return listToAddTo;
	}

	Map.prototype.indexOfCellAtPos = function(cellPos)
	{
		return cellPos.y * this.sizeInCells.x + cellPos.x;
	}
}

function MapCell()
{
	this.bodiesPresent = [];
}

function Transform()
{
	// static class
}
{
	Transform.applyTransformToCoordsMany = function(transformToApply, coordsSetToApplyTo)
	{
		for (var i = 0; i < coordsSetToApplyTo.length; i++)
		{
			var coords = coordsSetToApplyTo[i];
			transformToApply.applyToCoords(coords);
		}
	}
}

function TransformTranslate(displacement)
{
	this.displacement = displacement;
}
{
	TransformTranslate.prototype.applyToCoords = function(coordsToApplyTo)
	{
		coordsToApplyTo.add(this.displacement);
	}
}

// run

main();

</script>
</body>
</html>

Advertisement
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

2 Responses to A Simple Side-Scrolling Platformer Game in JavaScript

  1. Arnold says:

    nice

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s