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>