A Turn-Based Combat Engine for an RPG in JavaScript

The JavaScript code below implements a simple turn-based combat engine for a Japanese-style RPG in JavaScript. To see it 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/RPGCombatEngine/Source/RPGCombatEngine.html. It’s not really finished yet, but as usual, I figured I’d put it out here before I lose interest for the time being. Use the W, A, S, D, and Enter keys to navigate the menus and choose targets for attacks.

RPGCombatEngine

UPDATE 2017/01/30: I have updated the code to more closely match my current standards. The arrow keys are now used instead of WASD. The new version allows for the drawing of images for player characters, but will fall back to simple shapes if no image files are present at “../Media/Images/Agents.png”. A sample image is included below.

agents

UPDATE 2017/02/05: I have made a few tweaks to the graphics, including representing the combatants with simple images, provided that those images are present in the “../Media/Images/” directory.

agents

ogre

rpgcombatengine


<html>
<body>
<script type="text/javascript">

// main
function main()
{
	var universe = new DemoData().universe();

	Globals.Instance.initialize
	(
		100, // millisecondsPerTimerTick
		new Display
		(
			new Coords(300, 225),
			8, // fontHeightInPixels
			"LightGray", // colorFore
			"Black" // colorBack
		),
		universe
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];
			this[item[keyName]] = item;
		}
		return this;
	}
}

// classes

function Action()
{
	this.status = ActionStatus.Instances.None;
	this.parameters = [];
}
{
	// instance methods

	Action.prototype.defn = function(agent)
	{
		var returnValue = agent.defn().actionDefns[this.defnName];

		if (returnValue == null)
		{
			returnValue = Globals.Instance.universe.actionDefns[this.defnName];
		}

		return returnValue;
	}

	Action.prototype.target = function()
	{
		return this.parameters["Target"];
	}

	Action.prototype.target_Set = function(valueToSet)
	{
		this.parameters["Target"] = valueToSet;
	}

	Action.prototype.updateEncounterAndAgentForTimerTick = function(encounter, agent)
	{
		var display = Globals.Instance.display;

		if (this.status == ActionStatus.Instances.None)
		{
			this.status = ActionStatus.Instances.AwaitingActionDefn;

			var actionDefns = agent.defn().actionDefns;

			var updateEncounter = function()
			{
				var encounter = Globals.Instance.universe.encounter;
				var agent = encounter.agentCurrent;
				var action = agent.action;
				action.defnName = this.text; // hack
				var actionDefn = action.defn(agent);
				if (actionDefn.requiresTarget == true)
				{
					actionStatusNext = ActionStatus.Instances.AwaitingTarget;
				}
				else
				{
					actionStatusNext = ActionStatus.Instances.Complete;
				}
				action.status = actionStatusNext;
			}

			var panes = encounter.defn().panes;

			var menuForActionDefns = new Menu
			(
				"Actions",
				panes["Menu_Player"].pos, // pos
				new Coords(0, 8), // spacing
				null, // updateEncounter
				null, // menuable
				Menu.menuablesToMenus
				(
					actionDefns,
					[ "name" ], // bindingPathsForMenuText
					updateEncounter
				),
				0 // indexOfChildSelected
			);

			encounter.entitiesToSpawn.push(menuForActionDefns);
		}
		else if (this.status == ActionStatus.Instances.AwaitingActionDefn)
		{
			// do nothing	
		}
		else if (this.status == ActionStatus.Instances.AwaitingTarget)
		{
			var intelligence = encounter.partyCurrent().intelligence;
			intelligence.decideAction(this);
		}
		else if (this.status == ActionStatus.Instances.Running)
		{
			var actionDefn = this.defn(agent);
			actionDefn.perform(encounter, agent, this);
		}
		else if (this.status == ActionStatus.Instances.Complete)
		{
			encounter.entitiesToRemove.push(agent.action);
			agent.action = null;
			agent.hasMovedThisTurn = true;
			encounter.agentCurrentAdvance();
		}
	}
}

function ActionDefn
(
	name,
	requiresTarget, 
	perform,
	toMenu
)
{
	this.name = name;
	this.requiresTarget = requiresTarget;
	this.perform = perform;
	this.toMenu = toMenu;
}

function ActionStatus(name)
{
	this.name = name;
}
{
	ActionStatus.Instances = new ActionStatus_Instances();
	
	function ActionStatus_Instances()
	{
		this.None 		= new ActionStatus("None");
		this.AwaitingActionDefn = new ActionStatus("AwaitingActionDefn");
		this.AwaitingTarget 	= new ActionStatus("AwaitingTarget");
		this.Running 		= new ActionStatus("Running");
		this.Complete 		= new ActionStatus("Complete"); 
	}
}

function Agent(name, defnName, pos, itemsEquipped, itemsInInventory)
{
	this.name = name;
	this.defnName = defnName;
	this.pos = pos;
	this.itemsEquipped = itemsEquipped;
	this.itemsInInventory = itemsInInventory;

	this.action = null;
	this.effects = [];

	this.hasMovedThisTurn = false;
}
{
	Agent.prototype.defn = function()
	{
		return Globals.Instance.universe.agentDefns[this.defnName];
	}

	Agent.prototype.initializeForEncounter = function(encounter)
	{
		var defn = this.defn();
		this.integrity = defn.integrityMax;
		this.energy = defn.energyMax;
		this.hasMovedThisTurn = false;

		if (this.itemsEquipped != null)
		{
			for (var i = 0; i < this.itemsEquipped.length; i++)
			{
				var item = this.itemsEquipped[i];
				var itemDefn = item.defn();
				var categoryNames = itemDefn.categoryNames;
				for (var c = 0; c < categoryNames.length; c++)
				{
					var categoryName = categoryNames[c];
					this.itemsEquipped[categoryName] = item;
				}
			}
		}
	}

	Agent.prototype.updateEncounterForTimerTick = function(encounter)
	{
		if (encounter.agentCurrent == this)
		{
			if (this.action == null)
			{
				this.action = new Action();
			}
	
			this.action.updateEncounterAndAgentForTimerTick
			(
				encounter,
				this
			);
		}
	}

	// menuable

	Agent.prototype.toMenu = function()
	{
		var universe = Globals.Instance.universe;
		var encounter = universe.encounter;
		var panes = encounter.defn().panes;
		var agentDefn = this.defn();
		var spellDefns = agentDefn.spellDefns;

		var textForAgent;
		if (this.name == null) // hack
		{
			textForAgent = agentDefn.name;
		}
		else
		{
			textForAgent = this.name;
		}

		textForAgent += 
			" ("
			+ "H:" + this.integrity + "/" + agentDefn.integrityMax;

		if (agentDefn.energyMax > 0)
		{
			textForAgent += 
				" E:" + this.energy + "/" + agentDefn.energyMax
		}

		textForAgent += ")";

		var returnMenu = new Menu
		(
			textForAgent,
			panes["Menu_Player"].pos, // pos
			new Coords(0, 8), // spacing
			this, // menuable
			null, // updateEncounter
			null, // children
			0 // indexOfChildSelected
		);

		return returnMenu;
	}

	// drawable

	Agent.prototype.drawToDisplay = function(display)
	{
		var agent = this;
		var agentDefn = agent.defn();
		agentDefn.visual.drawToDisplayForDrawable(display, this);

		var encounter = Globals.Instance.universe.encounter;
		var agentCurrent = encounter.agentCurrent;

		if (agent == agentCurrent)
		{
			var arrowSizeInPixels = agentDefn.sizeInPixels;

			display.drawArrow
			(
				agent.pos,
				arrowSizeInPixels
			);

			var action = agent.action;
			if (action != null)
			{
				var actionTarget = action.target();

				if (actionTarget != null)
				{
					display.drawArrow
					(
						actionTarget.pos,
						arrowSizeInPixels
					);
				}
			}
		}

	}

}

function AgentDefn
(
	name, 
	visual,
	sizeInPixels, 
	integrityMax,
	energyMax,
	initiativeRange,
	actionDefns,
	spellDefns
)
{
	this.name = name;
	this.visual = visual;
	this.sizeInPixels = sizeInPixels;
	this.integrityMax = integrityMax;
	this.energyMax = energyMax;
	this.initiativeRange = initiativeRange;
	this.actionDefns = actionDefns;
	this.spellDefns = spellDefns;
	
	this.actionDefns.addLookups("name");

	if (this.spellDefns != null)
	{
		this.spellDefns.addLookups("name");
	}
}

function Category(name)
{
	this.name = name;
}

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

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0, 0);
	}

	// methods

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

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

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

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

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

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

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

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

function Display(sizeInPixels, fontHeightInPixels, colorFore, colorBack)
{
	this.sizeInPixels = sizeInPixels;
	this.fontHeightInPixels = fontHeightInPixels;
	this.colorFore = colorFore;
	this.colorBack = colorBack;
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes, this.sizeInPixels,
			this.colorBack, this.colorFore
		);
	}

	Display.prototype.drawArrow = function(pos, size)
	{
		this.graphics.strokeStyle = this.colorFore;
		this.graphics.beginPath();
		this.graphics.moveTo(pos.x, pos.y + size.y / 2);
		this.graphics.lineTo(pos.x - size.x, pos.y);
		this.graphics.lineTo(pos.x - size.x, pos.y + size.y);
		this.graphics.closePath();
		this.graphics.stroke();
	}

	Display.prototype.drawImage = function(systemImage, pos, size)
	{
		if (size == null)
		{
			this.graphics.drawImage
			(
				systemImage,
				pos.x, pos.y
			);
		}
		else
		{
			this.graphics.drawImage
			(
				systemImage,
				pos.x, pos.y,
				size.x, size.y
			);
		}
	}

	Display.prototype.drawImageSlice = function
	(
		systemImage, sliceOffset, sliceSize, drawPos, drawSize
	)
	{
		if (drawSize == null)
		{
			this.graphics.drawImage
			(
				systemImage,
				sliceOffset.x, sliceOffset.y,
				sliceSize.x, sliceSize.y,
				drawPos.x, drawPos.y,
				sliceSize.x, sliceSize.y // drawSize
			);
		}
		else
		{
			this.graphics.drawImage
			(
				systemImage,
				sliceOffset.x, sliceOffset.y,
				sliceSize.x, sliceSize.y,
				drawPos.x, drawPos.y,
				drawSize.x, drawSize.y
			);
		}
	}



	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect
			(
				pos.x, pos.y, size.x, size.y
			);
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect
			(
				pos.x, pos.y, size.x, size.y
			);
		}
	}

	Display.prototype.drawText = function(textToDraw, pos, colorFill, colorBorder)
	{
		var textToDrawAsLines = textToDraw.split("\n");

		if (colorFill == null)
		{
			colorFill = this.colorFore;
		}
		this.graphics.fillStyle = colorFill;

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;	
		}

		for (var i = 0; i < textToDrawAsLines.length; i++)
		{
			var textLine = textToDrawAsLines[i];

			if (colorBorder != null)
			{
				this.graphics.strokeText
				(
					textLine,
					pos.x + 2, // hack
					pos.y + this.fontHeightInPixels * (i + 1)			
				);
			}

			this.graphics.fillText
			(
				textLine,
				pos.x + 2, // hack
				pos.y + this.fontHeightInPixels * (i + 1)
			);
		}
	}

	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.sizeInPixels.x;
		canvas.height = this.sizeInPixels.y;
		this.graphics = canvas.getContext("2d");
		this.graphics.font = "" + this.fontHeightInPixels + "px sans-serif";

		document.body.appendChild(canvas);
	}
}

function Effect(defnName)
{
	this.defnName = defnName;
	this.turnApplied = turnApplied;
}
{
	Effect.prototype.defn = function()
	{
		return Globals.Instance.universe.effectDefns[this.defnName];
	}
}

function EffectDefn(name, apply)
{
	this.name = name;
	this.apply = apply;
}

function Empty(pos)
{
	this.pos = pos;
}

function Encounter(defnName, parties)
{
	this.defnName = defnName;
	this.parties = parties;

	this.entities = [];
	this.entitiesToSpawn = [];
	this.entitiesToRemove = [];

	this.entitiesToSpawn = this.entitiesToSpawn.concat(this.parties);

	this.agentCurrent = null;
}
{
	Encounter.prototype.agentCurrentAdvance = function()
	{
		var agentNext = null;
		
		for (var i = 0; i < this.agentsAll.length; i++)
		{
			var agent = this.agentsAll[i];
			if (agent.hasMovedThisTurn == false)
			{
				agentNext = agent;
				haveAllAgentsMovedThisTurn = false;
				break;
			}
		}

		if (haveAllAgentsMovedThisTurn == true)
		{
			for (var i = 0; i < this.agentsAll.length; i++)
			{
				var agent = this.agentsAll[i];
				agent.hasMovedThisTurn = false;
			}
			agentNext = this.agentsAll[0];
		}

		this.agentCurrent = agentNext;
	}

	Encounter.prototype.defn = function()
	{
		return Globals.Instance.universe.encounterDefns[this.defnName];
	}

	Encounter.prototype.initialize = function()
	{
		this.agentsAll = [];

		for (var p = 0; p < this.parties.length; p++)
		{
			var party = this.parties[p];
			var partyAgents = party.agents;

			this.agentsAll = this.agentsAll.concat(partyAgents);
		}

		this.agentCurrentAdvance();
	}

	Encounter.prototype.partyCurrent = function()
	{
		return (this.agentCurrent.party);
	}

	Encounter.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.entitiesToSpawn.length; i++)
		{
			var entity = this.entitiesToSpawn[i];
			if (entity.initializeForEncounter != null)
			{
				entity.initializeForEncounter(this);
			}
			this.entities.push(entity);
		}

		this.entitiesToSpawn.length = 0;

		for (var i = 0; i < this.entities.length; i++)
		{
			var entity = this.entities[i];
			entity.updateEncounterForTimerTick(this);
		}

		for (var i = 0; i < this.entitiesToRemove.length; i++)
		{
			var entity = this.entitiesToRemove[i];
			this.entities.splice
			(
				this.entities.indexOf(entity),
				1
			);
		}

		this.entitiesToRemove.length = 0;

		this.updateForTimerTick_WinOrLose();

		this.drawToDisplay(Globals.Instance.display);
	}

	Encounter.prototype.updateForTimerTick_WinOrLose = function()
	{
		for (var p = 0; p < this.parties.length; p++)
		{
			var party = this.parties[p];
			var partyAgents = party.agents;

			var areAnyAgentsInPartyAlive = false;

			for (var a = 0; a < partyAgents.length; a++)
			{
				var agent = partyAgents[a];

				if (agent.integrity > 0)
				{
					areAnyAgentsInPartyAlive = true;
					break;
				}
			}

			if (areAnyAgentsInPartyAlive == false)
			{
				if (p == 0)
				{
					document.write("You lose!");
				}
				else
				{
					document.write("You win!");
				}
			}
		}
	}

	// drawable

	Encounter.prototype.drawToDisplay = function(display)
	{
		var encounter = this;
		display.clear();

		var encounterDefn = encounter.defn();
		var panes = encounterDefn.panes;

		for (var p = 0; p < panes.length; p++)
		{
			var pane = panes[p];
			pane.drawToDisplay(display);
		}

		var entities = encounter.entities;
		for (var i = 0; i < entities.length; i++)
		{
			var entity = entities[i];

			if (entity.drawToDisplay != null)
			{
				entity.drawToDisplay(display);
			}
		}
	}

}

function EncounterDefn(name, panes)
{
	this.name = name;
	this.panes = panes;

	this.panes.addLookups("name");
}


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

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

	Globals.prototype.initialize = function
	(
		millisecondsPerTimerTick,
		display,
		universe
	)
	{
		this.display = display;
		this.display.initialize();

		this.universe = universe;
		this.universe.initialize();

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

		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		);
	}
}

function Image(name, sourcePath)
{
	this.name = name;
	this.sourcePath = sourcePath;

	this.systemImage = document.createElement("img");
	this.systemImage.src = this.sourcePath;
}

function ImageLibrary(images)
{
	this.images = images;
	this.images.addLookups("name");
}

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

	InputHelper.prototype.updateForTimerTick = function()
	{
		this.keyPressed = null;
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(e)
	{
		this.keyPressed = e.key;
	}

	InputHelper.prototype.handleEventKeyUp = function(e)
	{
		// todo 
	}
}

function IntelligenceHuman()
{}
{
	IntelligenceHuman.prototype.decideAction = function(action)
	{
		var encounter = Globals.Instance.universe.encounter;
		var agent = encounter.agentCurrent;

		var target = action.target();
		if (target == null)
		{
			target = encounter.parties[1].agents[0];
			action.target_Set(target);
			action.parameters["EmptyForPosToReturnTo"] = new Empty(agent.pos.clone());
		}

		var inputHelper = Globals.Instance.inputHelper;
		var keyPressed = inputHelper.keyPressed;
	
		if (keyPressed == "Enter")
		{
			action.status = ActionStatus.Instances.Running;
		}
		else
		{
			var partyTargeted = target.party;
			var agentsInPartyTargeted = partyTargeted.agents;

			if (keyPressed == "ArrowLeft")
			{
				partyToTarget = encounter.parties[1];
				if (partyToTarget != partyTargeted)
				{
					target = partyToTarget.agents[0];
				}
			}
			else if (keyPressed == "ArrowRight")
			{
				partyToTarget = encounter.parties[0];
				if (partyToTarget != partyTargeted)
				{
					target = partyToTarget.agents[0];
				}
			}
			else if (keyPressed == "ArrowDown")
			{
				var indexOfAgentToTarget = agentsInPartyTargeted.indexOf(target) + 1;
				if (indexOfAgentToTarget >= agentsInPartyTargeted.length)
				{
					indexOfAgentToTarget = 0;
				}
				target = agentsInPartyTargeted[indexOfAgentToTarget];
			}
			else if (keyPressed == "ArrowUp")
			{		
				var indexOfAgentToTarget = agentsInPartyTargeted.indexOf(target) - 1;
				if (indexOfAgentToTarget < 0)
				{
					indexOfAgentToTarget = agentsInPartyTargeted.length - 1;
				}
				target = agentsInPartyTargeted[indexOfAgentToTarget];
			}

			action.target_Set(target);
		}
	}
}

function IntelligenceMachine()
{}
{
	IntelligenceMachine.prototype.decideAction = function()
	{
		// todo
	}
}

function Item(defnName)
{
	this.defnName = defnName;
}
{
	Item.prototype.apply = function(agent, target)
	{
		this.defn().apply(this, agent, target);
	}

	Item.prototype.defn = function()
	{
		return Globals.Instance.universe.itemDefns[this.defnName];
	}
}

function ItemDefn(name, categoryNames, apply)
{
	this.name = name;
	this.categoryNames = categoryNames;
	this.apply = apply;
}

function Menu
(
	text, 
	pos, 
	spacing, 
	menuable,
	updateEncounter, 
	children, 
	indexOfChildSelected
)
{
	this.text = text;
	this.pos = pos;
	this.spacing = spacing;
	this.menuable = menuable;
	this.updateEncounter = updateEncounter;
	this.children = children;
	this.indexOfChildSelected = indexOfChildSelected;
}
{
	Menu.menuablesToMenus = function(menuables, bindingPathsForMenuText, updateEncounter)
	{
		var returnValues = [];

		for (var i = 0; i < menuables.length; i++)
		{
			var menuable = menuables[i];

			var menuableAsMenu;

			if (menuable.toMenu != null)
			{
				menuableAsMenu = menuable.toMenu();
			}
			else
			{
				var menuText = "";
				for (var f = 0; f < bindingPathsForMenuText.length; f++)
				{
					var bindingPathForMenuText = bindingPathsForMenuText[f];
					var bindingPathElements = bindingPathForMenuText.split(".");
	
					var valueCurrent = menuable;
					for (var g = 0; g < bindingPathElements.length; g++)
					{
						var bindingPathElement = bindingPathElements[g];
	
						if (bindingPathElement.indexOf("()") == -1)
						{
							valueCurrent = valueCurrent[bindingPathElement];
						}
						else
						{
							bindingPathElement = bindingPathElement.substr(0, bindingPathElement.length - "()".length);
							var method = valueCurrent[bindingPathElement];
							valueCurrent = method.call(valueCurrent);
						}
					}
	
					menuText += valueCurrent;
				}
	
				menuableAsMenu = new Menu
				(
					menuText,
					new Coords(0, 0), // pos
					new Coords(0, 8), // spacing
					menuable, // menuable
					updateEncounter,
					null // children
				);
			}
			
			

			returnValues.push(menuableAsMenu);
		}

		return returnValues;
	}

	// instance methods

	Menu.prototype.childSelected = function()
	{
		return (this.indexOfChildSelected == null ? null : this.children[this.indexOfChildSelected]);
	}

	Menu.prototype.indexOfChildSelectedAdd = function(valueToAdd)
	{
		this.indexOfChildSelected += valueToAdd;
		if (this.indexOfChildSelected < 0)
		{
			this.indexOfChildSelected = this.children.length - 1;
		}
		else if (this.indexOfChildSelected >= this.children.length)
		{
			this.indexOfChildSelected = 0;
		}
	}

	Menu.prototype.updateEncounterForTimerTick = function(encounter)
	{
		if (this.isLocked == true)
		{
			return;
		}

		var inputHelper = Globals.Instance.inputHelper;
		var keyPressed = inputHelper.keyPressed;

		if (keyPressed == "ArrowLeft")
		{
			// todo - back
		}
		else if (keyPressed == "Enter" || keyPressed == "ArrowRight") 
		{
			var encounter = Globals.Instance.universe.encounter;
			var childSelected = this.childSelected();

			this.isLocked = true;

			if (childSelected.children == null)
			{
				childSelected.updateEncounter(encounter);
			}
			else
			{
				encounter.entitiesToRemove.push(this);
				encounter.entitiesToSpawn.push(childSelected);
			}
		}
		else if (keyPressed == "ArrowDown")
		{
			this.indexOfChildSelectedAdd(1);
		}
		else if (keyPressed == "ArrowUp")
		{
			this.indexOfChildSelectedAdd(-1);
		}
	}

	// drawable

	Menu.prototype.drawToDisplay = function(display)
	{
		var menu = this;
		var pos = menu.pos;
		var spacing = menu.spacing;

		var arrowSize = new Coords(8, 8);
		var drawPos = pos.clone().add
		(
			new Coords(arrowSize.x + 2, spacing.y / 2)
		);

		var children = menu.children;

		if (children != null)
		{
			for (var i = 0; i < children.length; i++)
			{
				var child = children[i];
				var displayText = child.text;
				display.drawText
				(
					displayText,
					drawPos
				);
		
				drawPos.y += arrowSize.y / 3; // hack
	
				if (i == menu.indexOfChildSelected)
				{
					display.drawArrow
					(
						drawPos, // pos - hack
						arrowSize // size
					);
				}

				drawPos.y -= arrowSize.y / 3; // hack
	
				drawPos.add(spacing);

			} // end for
		}
	}

}

function Message(text, pos)
{
	this.text = text;
	this.pos = pos;

	this.ticksToLive = 100;
}

function NumberHelper()
{
	// static class
}
{
	NumberHelper.trimValueToMinAndMax = function(value, min, max)
	{
		if (value < min)
		{
			value = min;
		}
		else if (value > max)
		{
			value = max;
		}

		return value;
	}
}

function Pane(name, pos, size)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
}
{
	// drawable

	Pane.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle
		(
			this.pos, this.size, 
			display.colorBack, 
			display.colorFore
		);
	}
}

function Party(name, intelligence, agents)
{
	this.name = name;
	this.intelligence = intelligence;
	this.agents = agents;

	this.agentIndexCurrent = null;

	for (var i = 0; i < this.agents.length; i++)
	{
		var agent = this.agents[i];
		agent.party = this;
	}
}
{
	Party.prototype.agentCurrent = function()
 	{
		return (this.agentIndexCurrent == null ? null : this.agents[this.agentIndexCurrent]);
	}

	Party.prototype.agentCurrentAdvance = function()
	{
		if (this.agentIndexCurrent == null)
		{
			this.agentIndexCurrent = 0;
		}
		else
		{
			this.agentIndexCurrent++;
			if (this.agentIndexCurrent >= this.agents.length)
			{
				this.agentIndexCurrent = 0;
			}
		}
	}

	Party.prototype.initializeForEncounter = function(encounter)
	{
		for (var i = 0; i < this.agents.length; i++)
		{
			var agent = this.agents[i];
			encounter.entitiesToSpawn.push(agent);
		}
	}

	Party.prototype.updateEncounterForTimerTick = function(encounter)
	{
		// do nothing
	}

	// drawable

	Party.prototype.drawToDisplay = function(display)
	{
		var panes = Globals.Instance.universe.encounter.defn().panes;

		var paneForStatus = panes["Status_" + this.name];

		var menuForParty = new Menu
		(
			"Party",
			paneForStatus.pos,
			new Coords(0, 8), // spacing
			null, // updateEncounter
			this, // menuable
			Menu.menuablesToMenus
			(
				this.agents,
				[ "name", "integrity", "defn().integrityMax" ], // bindingPathsForDisplayText
				null // updateEncounter
			)
		)

		menuForParty.drawToDisplay(display);
	}
}

function Range(min, max)
{
	this.min = min;
	this.max = max;
	this.size = this.max - this.min;
}
{
	Range.prototype.randomNumberInRange = function()
	{
		return
			this.min 
			+ Math.floor
			(
				Math.random()
				* this.size
			);
	}
}

function SpellDefn(name, apply)
{
	this.name = name;
	this.apply = apply;
}

function Universe
(
	name, 
	imageLibrary,
	actionDefns,
	agentDefns, 
	effectDefns, 
	encounterDefns, 
	itemDefns,
	encounter
)
{
	this.name = name;
	this.imageLibrary = imageLibrary;
	this.actionDefns = actionDefns;
	this.agentDefns = agentDefns;
	this.effectDefns = effectDefns;
	this.encounterDefns = encounterDefns;
	this.itemDefns = itemDefns;
	this.encounter = encounter;

	this.agentDefns.addLookups("name");
	this.effectDefns.addLookups("name");
	this.encounterDefns.addLookups("name");
	this.itemDefns.addLookups("name");
}
{
	Universe.prototype.initialize = function()
	{
		this.encounter.initialize();
	}

	Universe.prototype.updateForTimerTick = function()
	{
		this.encounter.updateForTimerTick();
	}
}

function VisualDynamic(drawToDisplayForDrawable)
{
	this.drawToDisplayForDrawable = drawToDisplayForDrawable;
}
{
	VisualDynamic.prototype.drawToDisplayForDrawable = function
	(
		display, drawable
	)
	{
		this.drawToDisplayForDrawable.call(this, display, drawable);
	}
}

function VisualFallthrough(children)
{
	this.children = children;	
}
{
	VisualFallthrough.prototype.drawToDisplayForDrawable = function
	(
		display, drawable
	)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			try
			{
				child.drawToDisplayForDrawable(display, drawable);
				break;
			}
			catch (err)
			{
				// do nothing
				var todo = 1;
			}
		}
	}

}

function VisualGroup(children)
{
	this.children = children;
}
{
	VisualGroup.prototype.drawToDisplayForDrawable = function
	(
		display, drawable
	)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.drawToDisplayForDrawable(display, drawable);
		}
	}
}

function VisualImage(imageName)
{
	this.imageName = imageName;
}
{
	VisualImage.prototype.drawToDisplayForDrawable = function
	(
		display, drawable
	)
	{
		var imageLibrary = Globals.Instance.universe.imageLibrary;
		var image = imageLibrary.images[this.imageName];
		display.drawImage(image.systemImage, drawable.pos);
	}
}

function VisualImageSlice(imageName, offset, size)
{
	this.imageName = imageName;
	this.offset = offset;
	this.size = size;
}
{
	VisualImageSlice.prototype.drawToDisplayForDrawable = function
	(
		display, drawable
	)
	{
		var imageLibrary = Globals.Instance.universe.imageLibrary;
		var image = imageLibrary.images[this.imageName];
		display.drawImageSlice
		(
			image.systemImage, this.offset, this.size, drawable.pos
		);
	}
}


function VisualRectangle(size, colorFill, colorBorder)
{
	this.size = size;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;
}
{
	VisualRectangle.prototype.drawToDisplayForDrawable = function
	(
		display, drawable
	)
	{
		display.drawRectangle
		(
			drawable.pos, this.size, 
			this.colorFill, this.colorBorder
		);
	}
}

function VisualText(text, colorFill)
{
	this.text = text;
	this.colorFill = colorFill;
}
{
	VisualText.prototype.drawToDisplayForDrawable = function(display, drawable)
	{
		display.drawText
		(
			this.text,
			pos,
			this.colorFill
		);
	}
}

// demo

function DemoData()
{
	// do nothing
}
{
	DemoData.prototype.universe = function()
	{
		var imageDirectory = "../Media/Images/";

		var imageLibrary = new ImageLibrary
		([
			new Image("Agents", imageDirectory + "Agents.png"),
			new Image("Ogre", imageDirectory + "Ogre.png"),
		]);

		var effectDefns = [];

		var categories = 
		[
			new Category("Consumable"),
			new Category("Weapon"),
		].addLookups("name");

		var itemDefns = this.universe_1_ItemDefns(categories);

		var actionDefnsCommon = this.universe_2_ActionDefns();

		var agentDefns = this.universe_3_AgentDefns(actionDefnsCommon);

		var encounterDefns = 
		[
			new EncounterDefn
			(
				"Default",
				// panes
				[
					new Pane("Field_Other", new Coords(0, 0), new Coords(225, 150)),
					new Pane("Field_Player", new Coords(225, 0), new Coords(75, 150)),
					new Pane("Status_Other", new Coords(0, 150), new Coords(100, 75)),
					new Pane("Status_Player", new Coords(100, 150), new Coords(125, 75)),
					new Pane("Menu_Player", new Coords(225, 150), new Coords(75, 75)),				
				]
			),
		].addLookups("name");

		var encounter = this.universe_4_Encounter(encounterDefns, agentDefns, itemDefns);

		var universe = new Universe
		(
			"Universe0",
			imageLibrary,
			actionDefnsCommon,
			agentDefns, 
			effectDefns,
			encounterDefns, 
			itemDefns,
			encounter
		);

		return universe;

	} // end method

	DemoData.prototype.universe_1_ItemDefns = function(categories)
	{
		var itemDefns = 
		[
			// consumables
	
			new ItemDefn
			(
				"Heal Potion", 
				[ categories["Consumable"].name ], 
				// apply
				function(item, agent, target) 
				{ 
					var items = agent.itemsInInventory;
					items.splice
					(
						items.indexOf(item),
						1
					);
					target.integrity += 20; 
				}
			),

			new ItemDefn
			(
				"Energy Potion", 
				[ categories["Consumable"].name ], 
				// apply
				function(item, agent, target) 
				{ 
					var items = agent.itemsInInventory;
					items.splice
					(
						items.indexOf(item),
						1
					);
					target.energy += 20; 
				}
			),

			// weapons

			new ItemDefn
			(
				"Dagger", 
				[ categories["Weapon"].name ],
				// apply 
				function(item, agent, target) 
				{ 
					target.integrity -= 2;
				}
			),

			new ItemDefn
			(
				"Mace", 
				[ categories["Weapon"].name ], 
				// apply
				function(item, agent, target) 
				{ 
					target.integrity -= 6;
				}
			),

			new ItemDefn
			(
				"Sword", 
				[ categories["Weapon"].name ], 
				// apply
				function(item, agent, target) 
				{ 
					target.integrity -= 10;
				}
			),
		];

		itemDefns.addLookups("name");

		return itemDefns;

	}

	DemoData.prototype.universe_2_ActionDefns = function()
	{
		var actionDefnsCommon = 
		[
			new ActionDefn
			(
				"Attack",
				true, // requiresTarget
				function(encounter, agent, action)
				{
					var target = action.target();
					var displacementFromAgentToTarget = target.pos.clone().subtract
					(
						agent.pos
					);

					var distanceToTarget = displacementFromAgentToTarget.magnitude();

					var distanceToMovePerTick = 16;
				
					if (distanceToTarget > distanceToMovePerTick)
					{
						var displacementToMove = displacementFromAgentToTarget.divideScalar
						(
							distanceToTarget
						).multiplyScalar
						(
							distanceToMovePerTick
						);
	
						agent.pos.add(displacementToMove);
					}
					else
					{	
						var emptyToReturnTo = action.parameters["EmptyForPosToReturnTo"];

						if (target.integrity == null)
						{
							agent.pos.overwriteWith(emptyToReturnTo.pos);
							action.target_Set(null);
							action.status = ActionStatus.Instances.Complete;
						}
						else
						{
							var weaponEquipped = agent.itemsEquipped["Weapon"];
							if (weaponEquipped != null)
							{
								weaponEquipped.apply
								(
									agent,
									target
								);
							}
							action.target_Set(emptyToReturnTo);
						}
					}
				},
				null // toMenu
			),

			new ActionDefn
			(
				"Defend",
				false, // requiresTarget
				function(encounter, agent, action)
				{
					action.status = ActionStatus.Instances.Complete;
				},
				null // toMenu
			),

			new ActionDefn
			(
				"Magic",
				true, // requiresTarget
				// updateEncounter
				function(encounter, agent, action)
				{
					action.status = ActionStatus.Instances.Complete;
				},
				// toMenu
				function()
				{
					var universe = Globals.Instance.universe;
					var encounter = universe.encounter;
					var panes = encounter.defn().panes;
					var agent = encounter.agentCurrent;
					var spellDefns = agent.defn().spellDefns;

					var returnMenu = new Menu
					(
						"Magic",
						panes["Menu_Player"].pos, // pos
						new Coords(0, 8), // spacing
						null, // menuable
						null, // updateEncounter
						Menu.menuablesToMenus
						(
							spellDefns,
							[ "name" ], // bindingPathsForMenuText
							function (encounter) 
							{ 
								var spellDefn = this.menuable;
								var agent = encounter.agentCurrent;
								var action = agent.action;
								action.parameters["SpellDefn"] = spellDefn;
								action.defnName = "Spell";
								action.status = ActionStatus.Instances.AwaitingTarget;
							}
						),
						0 // indexOfChildSelected
					);

					return returnMenu;
				}
			),

			new ActionDefn
			(
				"Spell",
				true, // requiresTarget
				function(encounter, agent, action)
				{
					var target = action.target();
					var displacementFromAgentToTarget = target.pos.clone().subtract
					(
						agent.pos
					);

					var distanceToTarget = displacementFromAgentToTarget.magnitude();

					var distanceToMovePerTick = 16;
					
					if (distanceToTarget > distanceToMovePerTick)
					{
						var displacementToMove = displacementFromAgentToTarget.divideScalar
						(
							distanceToTarget
						).multiplyScalar
						(
							distanceToMovePerTick
						);
	
						agent.pos.add(displacementToMove);
					}
					else
					{	
						var emptyToReturnTo = action.parameters["EmptyForPosToReturnTo"];					
	
						if (target.integrity == null)
						{
							agent.pos.overwriteWith(emptyToReturnTo.pos);
							action.target_Set(null);
							action.status = ActionStatus.Instances.Complete;
						}
						else
						{
							var spellDefn = action.parameters["SpellDefn"];
							spellDefn.apply
							(
								agent,
								target
							);
							action.target_Set(emptyToReturnTo);
						}
					}
				},
				null // toMenu
			),		

			new ActionDefn
			(
				"Item",
				false, // requiresTarget
				function(encounter, agent, action)
				{
					action.status = ActionStatus.Instances.Complete;
				},
				// toMenu
				function()
				{
					var universe = Globals.Instance.universe;
					var encounter = universe.encounter;
					var panes = encounter.defn().panes;
					var agent = encounter.agentCurrent;
					var items = agent.itemsInInventory;	

					var returnMenu = new Menu
					(
						"Items",
						panes["Menu_Player"].pos, // pos
						new Coords(0, 8), // spacing
						null, // menuable
						null, // updateEncounter
						Menu.menuablesToMenus
						(
							items,
							[ "defn().name" ], // bindingPathsForMenuText
							function (encounter) 
							{ 
								var item = this.menuable;
								var agentCurrent = encounter.agentCurrent;
								var target = agentCurrent.action.target();
								item.apply(agentCurrent, target);
							}
						),
						0 // indexOfChildSelected
					);

					return returnMenu;
				}
			),

			new ActionDefn
			(
				"Wait",
				false, // requiresTarget
				function(encounter, agent, action)
				{
					encounter.agentCurrentAdvance();
				},
				null // toMenu
			),

			new ActionDefn
			(
				"Run",
				false, // requiresTarget
				function(encounter, agent, action)
				{
					// todo
					action.status = ActionStatus.Instances.Complete;
				},
				null // toMenu  
			),
		];	

		actionDefnsCommon.addLookups("name");

		return actionDefnsCommon;
	}

	DemoData.prototype.universe_3_AgentDefns = function(actionDefnsCommon)
	{
		var agentSizeInPixelsStandard = new Coords(24, 24);

		var visualAgentLabel = new VisualDynamic
		(
			function drawToDisplayForDrawable(display, drawable)
			{
				var agent = drawable;
				var agentDefn = agent.defn();
				var agentText = "\n";
				if (agent.name != null)
				{
					agentText += agent.name + "\n";
				}
				agentText += agentDefn.name;
				display.drawText
				(
					agentText, agent.pos, 
					display.colorFore, display.colorBack
				);
			}
		);

		var agentDefns = 
		[
			new AgentDefn
			(
				"Mage", 
				new VisualGroup
				([
					new VisualFallthrough
					([
						new VisualImageSlice
						(
							"Agents",
							new Coords(0, 0).multiply
							(
								agentSizeInPixelsStandard
							), 
							agentSizeInPixelsStandard
						),
						new VisualRectangle
						(
							agentSizeInPixelsStandard, 
							null, // colorFill
							"LightGray" 
						),
					]),
					visualAgentLabel,
				]),
				agentSizeInPixelsStandard,
				10, //integrityMax,
				20, //energyMax,
				new Range(1, 10), // initiativeRange
				//actionDefns
				[
					actionDefnsCommon["Attack"],
					actionDefnsCommon["Magic"],
					actionDefnsCommon["Item"],
					actionDefnsCommon["Wait"],
				],
				// spellDefns
				[
					new SpellDefn
					(
						"Fire",
						// apply 
						function(agent, target) 
						{ 
							agent.energy -= 5;
							target.integrity -= 20;
						} 
					),
				]
			),		
	
			new AgentDefn
			(
				"Priest", 
				new VisualGroup
				([
					new VisualFallthrough
					([
						new VisualImageSlice
						(
							"Agents",
							new Coords(1, 0).multiply
							(
								agentSizeInPixelsStandard
							), 
							agentSizeInPixelsStandard
						),
						new VisualRectangle
						(
							agentSizeInPixelsStandard, 
							null, // colorFill
							"LightGray" 
						),
					]),
					visualAgentLabel,
				]),
				agentSizeInPixelsStandard,
				20, //integrityMax,
				20, //energyMax,
				new Range(1, 10), // initiativeRange
				//actionDefns
				[
					actionDefnsCommon["Attack"],
					actionDefnsCommon["Magic"],
					actionDefnsCommon["Item"],
					actionDefnsCommon["Wait"],
				],
				// spellDefns
				[
					new SpellDefn
					(
						"Heal",
						// apply 
						function(agent, target) 
						{ 
							agent.energy += 5;
							var integrityToHeal = 20;
							target.integrity = NumberHelper.trimValueToMinAndMax
							(
								target.integrity + integrityToHeal,
								0, // min
								target.defn().integrityMax // max
							);
						} 
					),
				]
			),

			new AgentDefn
			(
				"Warrior", 
				new VisualGroup
				([
					new VisualFallthrough
					([
						new VisualImageSlice
						(
							"Agents",
							new Coords(2, 0).multiply
							(
								agentSizeInPixelsStandard
							), 
							agentSizeInPixelsStandard
						),
						new VisualRectangle
						(
							agentSizeInPixelsStandard, 
							null, // colorFill
							"LightGray" 
						),
					]),
					visualAgentLabel,
				]),
				agentSizeInPixelsStandard,
				30, //integrityMax,
				0, //energyMax,
				new Range(1, 10), // initiativeRange
				//actionDefns
				[
					actionDefnsCommon["Attack"],
					actionDefnsCommon["Item"],
					actionDefnsCommon["Wait"],
				],
				null // spellDefns
			),

			new AgentDefn
			(
				"Ogre", 
				new VisualGroup
				([
					new VisualFallthrough
					([
						new VisualImage("Ogre"),
						new VisualRectangle
						(
							new Coords(40, 40), 
							null, // colorFill
							"LightGray" 
						),
					]),
					visualAgentLabel,
				]),
				new Coords(40, 40), // size
				50, //integrityMax,
				0, //energyMax,
				new Range(1, 10), // initiativeRange
				//actionDefns
				[
					actionDefnsCommon["Attack"],
				],
				null // spellDefns
			),	
		];

		agentDefns.addLookups("name");

		return agentDefns;
	}

	DemoData.prototype.universe_4_Encounter = function(encounterDefns, agentDefns, itemDefns)
	{
		var encounter = new Encounter
		(
			encounterDefns["Default"].name,
			// parties
			[
				new Party
				(
					"Player",
					new IntelligenceHuman(),
					[
						new Agent
						(
							"One", 
							agentDefns["Mage"].name,
							new Coords(256, 20), 
							// itemsEquipped
							[
								new Item(itemDefns["Dagger"].name)
							],
							// itemsInInventory
							[
								new Item(itemDefns["Energy Potion"].name),
								new Item(itemDefns["Energy Potion"].name),
								new Item(itemDefns["Energy Potion"].name),
							] 
						),
						new Agent
						(
							"Two",
							agentDefns["Priest"].name,
							new Coords(256, 50),
							// itemsEquipped
							[
								new Item(itemDefns["Mace"].name)
							],
							[
								new Item(itemDefns["Heal Potion"].name),
								new Item(itemDefns["Heal Potion"].name),
								new Item(itemDefns["Heal Potion"].name),
							] // itemsInInventory
						),
						new Agent
						(
							"Three",
							agentDefns["Warrior"].name,
							new Coords(256, 80),
							// itemsEquipped
							[
								new Item(itemDefns["Sword"].name)
							],
							[] // itemsInInventory
						),
						new Agent
						(
							"Four",
							agentDefns["Mage"].name,
							new Coords(256, 110),
							// itemsEquipped
							[
							new Item(itemDefns["Dagger"].name)
							],
							[] // itemsInInventory
						),
					]
				),
				new Party
				(
					"Other",
					new IntelligenceHuman(),
					[
						new Agent
						(
							null, // name
							agentDefns["Ogre"].name,
							new Coords(90, 20),
							null, // itemsEquipped
							null // itemsInInventory
						),
						new Agent
						(
							null, // name
							agentDefns["Ogre"].name,
							new Coords(30, 50),
							null, // itemsEquipped
							null // itemsInInventory
						),
						new Agent
						(
							null, // name
							agentDefns["Ogre"].name,
							new Coords(90, 80),
							null, // itemsEquipped
							null // itemsInInventory
						),
						new Agent
						(
							null, // name
							agentDefns["Ogre"].name,
							new Coords(150, 50),
							null, // itemsEquipped
							null // itemsInInventory
						),
					]
				),
			]	
		);

		return encounter;
	}

} // end class DemoData

// run

main();

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

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

One Response to A Turn-Based Combat Engine for an RPG in JavaScript

  1. code4liberty says:

    Reminds me of Final Fantasy.

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 )

Google+ photo

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

Connecting to %s