Another RPG Combat Engine in JavaScript

The code below implements a rudimentary turn-based combat engine for a Japanese-style role-playing game in JavaScript.

I have partially implemented a similar engine in a previous post, but I recently tried to update it and found it very difficult. I suppose I concentrated too much on how it looked and less on how it worked. So this time, I ignored the graphics completely to start with, and just used a simple all-text interface. I hope this will let me focus more on the architecture, which will therefore be cleaner and easier to expand and maintain.


<html>
<body>

<label><b>RPG Combat Engine</b></label>
<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var actionDefns = ActionDefn.Instances()._All;
	
	var actionDefnNames = actionDefns.select("name");

	var party0 = new Party
	(
		"Party0",
		true, // isControlledByHuman
		// agents
		[
			new Agent("Amy", 10, actionDefnNames),
			new Agent("Bob", 10, actionDefnNames),
			new Agent("Cal", 10, actionDefnNames),
			new Agent("Deb", 10, actionDefnNames),
		]
	);
	
	var party1 = new Party
	(
		"Party1",
		false, // isControlledByHuman
		// agents
		[
			new Agent("William", 10, actionDefnNames),
			new Agent("Xavier", 10, actionDefnNames),
			new Agent("Yonni", 10, actionDefnNames),
			new Agent("Zane", 10, actionDefnNames),
		]
	);
	
	var encounter = new Encounter([party0, party1]);
	var encounterAsControl = encounter.toControl();
	var encounterAsDomElement = encounterAsControl.toDomElement();
	var divMain = document.getElementById("divMain");
	divMain.appendChild(encounterAsDomElement);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	};
	
	Array.prototype.get = function()
	{
		return this;
	};
	
	Array.prototype.insertElementAt = function(element, index)
	{
		this.splice(index, 0, element);
		return this;
	}
		
	Array.prototype.remove = function(elementToRemove)
	{
		var indexToRemoveAt = this.indexOf(elementToRemove);
		if (indexToRemoveAt != null)
		{
			this.removeAt(indexToRemoveAt);
		}
		return this;
	}
	
	Array.prototype.removeAt = function(indexToRemoveAt)
	{
		this.splice(indexToRemoveAt, 1);
		return this;
	}
	
	Array.prototype.select = function(fieldName)
	{
		var returnValues = [];
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var fieldValue = this[fieldName];
			returnValues.push(fieldValue);
		}
		return returnValues;
	};
}

function BooleanExtensions()
{
	// extension class
}
{
	Boolean.prototype.get = function()
	{
		return (this == true); // To unwrap it.
	};
}

function StringExtensions()
{
	// extension class
}
{
	String.prototype.get = function()
	{
		return this;
	};
}

// classes

function Action(agentNameActor, defnName, targetName)
{
	this.agentNameActor = agentNameActor;
	this.defnName = defnName;
	this.targetName = targetName;
}
{
	Action.prototype.defn = function()
	{
		return ActionDefn.Instances()._All[this.defnName];
	}

	Action.prototype.isValid = function()
	{
		var returnValue = 
		(
			this.agentNameActor != null
			&& this.defnName != null
			&& this.targetName != null
		);
		return returnValue;
	};
	
	Action.prototype.performForEncounter = function(encounter)
	{
		this.effects = this.defn().performForEncounterAndAction(encounter, this);
	}

	Action.prototype.toString = function()
	{
		var returnValue =
			this.agentNameActor
			+ " used " + this.defnName
			+ " on " + this.targetName + ".  "
			+ (this.effects == null ? "" : this.effects.join("  "));
		return returnValue;
	};
}

function ActionDefn(name, targetTypeName, performForEncounterAndAction)
{
	this.name = name;
	this.targetTypeName = targetTypeName;
	this.performForEncounterAndAction = performForEncounterAndAction;
}
{
	ActionDefn.Instances = function()
	{
		if (ActionDefn._Instances == null)
		{
			ActionDefn._Instances = new ActionDefn_Instances();
		}
		
		return ActionDefn._Instances;
	};
	
	function ActionDefn_Instances()
	{
		var targetTypes = ActionTargetType.Instances()._All;
		var performTodo = function todo() { };
		
		this.Attack = new ActionDefn
		(
			"Attack", 
			targetTypes["Enemy"].name,
			function perform(encounter, action)
			{
				var targetName = action.targetName;			
				var partyTargeted = encounter.partyOther();
				var agentTargeted = partyTargeted.agents[targetName];
				var damage = 3;
				var effectDamage = new Effect("lost", damage, targetName);
				var effects = [ effectDamage ];
				agentTargeted.integrity -= damage;
				if (agentTargeted.integrity <= 0)
				{
					var effectDie = new Effect("died", null, targetName);
					effects.push(effectDie);
					partyTargeted.agents.remove(agentTargeted);
				}
				partyTargeted._control.invalidate();				
				return effects;
			}
		);
		this.Cower = new ActionDefn
		(
			"Cower", targetTypes["Self"].name, performTodo
		);
		this.Flee = new ActionDefn
		(
			"Flee", targetTypes["Self"].name, performTodo
		);
		this.Heal = new ActionDefn
		(
			"Heal", targetTypes["Ally"].name, performTodo
		);		
		this.Protect = new ActionDefn
		(
			"Protect", targetTypes["Ally"].name, performTodo
		);
		this.Wait = new ActionDefn
		(
			"Wait", targetTypes["None"].name, performTodo
		);
		
		this._All = 
		[ 
			this.Attack,
			//this.Cower,
			//this.Flee,
			//this.Heal,
			//this.Protect,
			//this.Wait
		].addLookups("name");
	}
	
	ActionDefn.prototype.targetType = function()
	{
		return ActionTargetType.Instances()._All[this.targetTypeName];
	}
}

function ActionTargetType(name, targetsGetForEncounter)
{
	this.name = name;
	this.targetsGetForEncounter = targetsGetForEncounter;
}
{
	ActionTargetType.Instances = function()
	{
		if (ActionTargetType._Instances == null)
		{
			ActionTargetType._Instances = new ActionTargetType_Instances();
		}
		
		return ActionTargetType._Instances;
	};
	
	function ActionTargetType_Instances()
	{
		this.Ally = new ActionTargetType
		(
			"Ally",
			function targetsGetForEncounter(encounter)
			{
				return encounter.partyCurrent().agents;
			}
		);
		this.Enemy = new ActionTargetType
		(
			"Enemy",
			function targetsGetForEncounter(encounter)
			{
				return encounter.partyOther().agents;
			}
		);
		this.None = new ActionTargetType
		(
			"None",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}
		);
		this.Other = new ActionTargetType
		(
			"Other",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.Party = new ActionTargetType
		(
			"Party",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.PartyAllies = new ActionTargetType
		(
			"AlliesAll",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.PartyEnemies = new ActionTargetType
		(
			"EnemiesAll",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);		
		this.Self = new ActionTargetType
		(
			"Self",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		
		this._All = 
		[
			this.Ally,
			this.Enemy,
			this.None,
			this.Other,
			this.Party,
			this.PartyAllies,
			this.PartyEnemies,
			this.Self
		].addLookups("name");
	}
}

function Agent(name, integrityMax, actionDefnNames)
{
	this.name = name;
	this.integrityMax = integrityMax;
	this.actionDefnNames = actionDefnNames;
	
	this.integrity = integrityMax;
	this.hasMovedThisTurn = false;
}
{
	Agent.prototype.toString = function()
	{
		return this.name + " (" + this.integrity + "/" + this.integrityMax + ")";
	}
}

function ControlBinding(context, get, set)
{
	this.context = context;
	this.get = (get == null ? this.getDefault : get).bind(this);
	this.set = (set == null ? this.setDefault : set).bind(this);
}
{
	ControlBinding.prototype.getDefault = function(binding)
	{
		return binding.context;
	};
	
	ControlBinding.prototype.setDefault = function(binding, value)
	{
		// Do nothing.
	};
}

function ControlBreak()
{}
{
	ControlBreak.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("br");
		}
		
		return this._domElement;
	};
}

function ControlButton(text, click, isEnabled)
{
	this.text = text;
	this.click = click;
	this.isEnabled = (isEnabled == null ? true : isEnabled);
}
{
	ControlButton.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("button");
			this._domElement.innerHTML = this.text;
			this._domElement.onclick = this.click;
		}
		
		var isEnabled = this.isEnabled.get();
		this._domElement.disabled = (isEnabled == false);
		
		return this._domElement;
	};
}

function ControlContainer(children)
{
	this.children = children;
	
	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	};
}
{
	ControlContainer.prototype.invalidate = function()
	{
		if (this.parent == null)
		{
			this.toDomElement();
		}
		else
		{
			this.parent.invalidate();
		}
	};

	ControlContainer.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("div");
			this._domElement.style.border = "1px solid";
			
			for (var i = 0; i < this.children.length; i++)
			{
				var child = this.children[i];
				var childAsDomElement = child.toDomElement();
				this._domElement.appendChild(childAsDomElement);
			}
		}
		
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.toDomElement();
		}
		
		return this._domElement;
	};
}

function ControlLabel(text)
{
	this.text = text;
}
{
	ControlLabel.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("label");
		}
		
		this._domElement.innerHTML = this.text.get();
		
		return this._domElement;
	};
}

function ControlList
(
	numberOfItemsVisible,
	bindingForItemText,
	bindingForItemValue,	
	bindingForSelectedValue,
	items
)
{
	this.numberOfItemsVisible = numberOfItemsVisible;
	this.bindingForItemText = bindingForItemText;
	this.bindingForItemValue = bindingForItemValue;
	this.bindingForSelectedValue = bindingForSelectedValue;
	this.items = items;
}
{
	ControlList.prototype.invalidate = function()
	{
		if (this.parent != null)
		{
			this.parent.invalidate();
		}
	};

	ControlList.prototype.toDomElement = function()
	{
		var control = this;
		
		if (this._domElement == null)
		{
			this._domElement = document.createElement("select");
			this._domElement.size = this.numberOfItemsVisible;
			this._domElement.onchange = function (event)
			{
				var selectedValue = event.target.value;
				control.bindingForSelectedValue.set(selectedValue);
				control.invalidate();
			};
		}
	
		this._domElement.innerHTML = "";
		var items = this.items.get();
		for (var i = 0; i < items.length; i++)
		{
			var item = items[i];
			var itemAsOption = document.createElement("option");
			this.bindingForItemText.context = item;
			var text = this.bindingForItemText.get();
			this.bindingForItemValue.context = item;
			var value = this.bindingForItemValue.get();
			itemAsOption.innerHTML = text;
			this._domElement.appendChild(itemAsOption);
		}
		
		this._domElement.value = this.bindingForSelectedValue.get();		
		
		return this._domElement;
	};
}

function ControlSpan(children, isVisible)
{
	this.children = children;
	this.isVisible = (isVisible == null ? true : isVisible);
	
	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	}
}
{
	ControlSpan.prototype.invalidate = function()
	{
		if (this.parent == null)
		{
			this.toDomElement();
		}
		else
		{
			this.parent.invalidate();
		}
	};

	ControlSpan.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("span");
			
			for (var i = 0; i < this.children.length; i++)
			{
				var child = this.children[i];
				var childAsDomElement = child.toDomElement();
				this._domElement.appendChild(childAsDomElement);
			}
		}
		
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.toDomElement();
		}
		
		var isVisible = this.isVisible.get();
		this._domElement.style.display = (isVisible ? "inline" : "none");
				
		return this._domElement;
	};
}

function Effect(defnName, magnitude, targetName)
{
	this.defnName = defnName;
	this.magnitude = magnitude;
	this.targetName = targetName;
}
{
	Effect.prototype.toString = function()
	{
		var returnValue = this.targetName + " " + this.defnName;
		if (this.magnitude != null)
		{
			returnValue += " " + this.magnitude;
		}
		returnValue += ".";
		return returnValue;
	};
}

function Encounter(parties)
{
	this.parties = parties;
	
	this.partyIndexCurrent = 0;
	this.agentIndexCurrent = 0;
	this.actionsSoFar = [];
	this.actionCurrent = new Action(this.agentCurrent().name);
}
{
	Encounter.prototype.actionDefnSelected = function()
	{
		var returnValue = ActionDefn.Instances()._All[this.actionCurrent.defnName];
		return returnValue;
	};
	
	Encounter.prototype.actionCurrentPerform = function()
	{
		var action = this.actionCurrent;
		if (action.isValid())
		{
			action.performForEncounter(this);
			
			this.actionsSoFar.insertElementAt(action, 0);
			
			this.agentCurrentAdvance();			
		}
	};
	
	Encounter.prototype.agentCurrent = function()
	{
		var returnValue = this.partyCurrent().agents[this.agentIndexCurrent];
		return returnValue;
	};
	
	Encounter.prototype.agentCurrentAdvance = function()
	{
		var partyCurrent = this.partyCurrent();
		var agents = partyCurrent.agents;
		this.agentIndexCurrent++;
		if (this.agentIndexCurrent >= agents.length)
		{
			this.partyIndexCurrent = 1 - this.partyIndexCurrent; 
			this.agentIndexCurrent = 0;
		}
		this.actionCurrent = new Action(this.agentCurrent().name);
		this._control.invalidate();
	};
			
	Encounter.prototype.agentsAll = function()
	{
		if (this._agentsAll == null)
		{
			this._agentsAll = this.parties[0].agents.concat(this.parties[1].agents);
		}
		
		return this._agentsAll;
	};
		
	Encounter.prototype.partyCurrent = function()
	{
		return this.parties[this.partyIndexCurrent];
	};
	
	Encounter.prototype.partyOther = function()
	{
		return this.parties[1 - this.partyIndexCurrent];
	};
		
	// controls

	Encounter.prototype.toControl = function()
	{
		if (this._control == null)
		{
			var controlsForParties = [];
			for (var i = 0; i < this.parties.length; i++)
			{
				var party = this.parties[i];
				var partyAsControl = party.toControl();
				controlsForParties.push(partyAsControl);
			}
			
			var containerActions = new ControlContainer
			([
				new ControlLabel("Next Action:"),
				new ControlBreak(),
				
				new ControlLabel
				(
					new ControlBinding
					(
						this, // context
						function get() { return this.context.agentCurrent().name; }
					)
				),
				new ControlLabel(" uses "),
				new ControlList
				(
					1, // numberOfItemsVisible
					// bindingGetForItemText
					new ControlBinding
					(
						null, function get() { return this.context.name; }
					),
					// bindingGetForItemValue
					new ControlBinding
					(
						null, function get() { return this.context.name; }
					),
					// bindingForSelectedValue
					new ControlBinding
					(
						this, // context 
						function get() { return this.context.actionCurrent.defnName; }, 
						function set(value) { return this.context.actionCurrent.defnName = value; } 						
					),				
					ActionDefn.Instances()._All
				),
				new ControlSpan
				( 
					[ 
						new ControlLabel(" on "),
						new ControlList
						(
							1, // numberOfItemsVisible,
							// bindingForItemText
							new ControlBinding
							(
								null, function get() { return this.context.name; }
							),
							// bindingForItemValue
							new ControlBinding
							(
								null, function get() { return this.context.name; }
							),
							// bindingForSelectedValue
							new ControlBinding
							(
								this, // context 
								function get() { return this.context.actionCurrent.targetName; }, 
								function set(value) { this.context.actionCurrent.targetName = value; }
							),
							// items
							new ControlBinding
							(
								this, // context
								function get()
								{
									var returnValues;
									var actionDefn = this.context.actionCurrent.defn();
									if (actionDefn == null)
									{
										returnValues = [];
									}
									else
									{
										var actionTargetType = actionDefn.targetType();
										returnValues = actionTargetType.targetsGetForEncounter(this.context);
									}
									return returnValues;
								}
							)
						),
					],
					// isVisible
					new ControlBinding
					(
						this, // context
						function get() 
						{ 
							var actionDefn = this.context.actionCurrent.defn();
							if (actionDefn == null)
							{
								return false;
							}
							var actionTargetType = actionDefn.targetType();
							var targets = actionTargetType.targetsGetForEncounter(this.context);
							var returnValue = (targets.length > 0);
							return returnValue;
						}
					) 
				),
				new ControlLabel(" "),
				
				new ControlButton
				(
					"Go",
					this.actionCurrentPerform.bind(this),
					// isEnabled
					new ControlBinding
					(
						this, // context
						function get() { return this.context.actionCurrent.isValid(); } 
					)
				),
				new ControlBreak(),
				
				new ControlLabel("Actions So Far:"),
				new ControlBreak(),
				new ControlList
				(
					3, // numberOfItemsVisible				
					// bindingForItemText
					new ControlBinding
					(
						null, function get() { return this.context.toString(); }
					),
					// bindingForItemValue
					new ControlBinding
					(
						null, // context 
						function get(binding) { return "todo"; }, 
					),									
					// bindingForSelectedValue
					new ControlBinding
					(
						null, // context 
						function get(binding) { return "todo"; }, 
					),				
					this.actionsSoFar
				)
			]);
		
			this._control = new ControlContainer
			(
				[
					controlsForParties[0],
					controlsForParties[1],
					containerActions
				]
			);
		}
		
		for (var i = 0; i < this.parties.length; i++)
		{
			var party = this.parties[i];
			party.toControl().toDomElement();
		}
	
		return this._control;
	};
}

function Party(name, isControlledByHuman, agents)
{
	this.name = name;
	this.isControlledByHuman = isControlledByHuman;
	this.agents = agents.addLookups("name");
}
{
	// controls
	
	Party.prototype.toControl = function()
	{
		if (this._control == null)
		{
			this._control = new ControlContainer
			([
				new ControlLabel(this.name),
				new ControlBreak(),
				new ControlList
				(
					this.agents.length, // numberOfItemsVisible
					// bindingForItemText
					new ControlBinding
					(
						null, // context 
						function get() { return this.context.toString(); }, 
					),
					// bindingForItemValue
					new ControlBinding
					(
						null, // context 
						function get() { return this.context.name; }, 
					),
					// bindingForSelectedValue
					new ControlBinding
					(
						this, // context 
						function get() { return this.context.agentNameSelected; }, 
						function set(value) { return this.context.agentNameSelected = value; }, 						
					),				
					this.agents // items
				),
				
				new ControlBreak(),
				new ControlLabel("Details:"),
				new ControlLabel
				(
					new ControlBinding
					(
						this, // context 
						function get() 
						{ 
							var returnValue = this.context.agentNameSelected;
							returnValue = (returnValue == null ? "[none]" : returnValue);
							return returnValue;
						}, 
						function set(value) { return this.context.agentNameSelected = value; }, 						
					)
				)
			]);
		}
		
		return this._control;
	};
}

// run
main();

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

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

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 )

Google photo

You are commenting using your Google 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