Index: binaries/data/mods/public/gui/session/utility_functions.js
===================================================================
--- binaries/data/mods/public/gui/session/utility_functions.js	(revision 9207)
+++ binaries/data/mods/public/gui/session/utility_functions.js	(working copy)
@@ -223,29 +223,29 @@
 {
 	switch (formationName)
 	{
-	case "Formation0":
+	case "Loose":
 		return 0;
-	case "Formation1":
+	case "Box":
 		return 1;
-	case "Formation2":
+	case "Column Closed":
 		return 2;
-	case "Formation3":
+	case "Line Closed":
 		return 3;
-	case "Formation4":
+	case "Column Open":
 		return 4;
-	case "Formation5":
+	case "Line Open":
 		return 5;
-	case "Formation6":
+	case "Flank":
 		return 6;
-	case "Formation7":
+	case "Skirmish":
 		return 7;
-	case "Formation8":
+	case "Wedge":
 		return 8;
-	case "Formation9":
+	case "Testudo":
 		return 9;
-	case "Formation10":
-		return 10;	
-	case "Formation11":
+	case "Phalanx":
+		return 10;
+	case "Syntagma":
 		return 11;
 	case "Formation12":
 		return 12;
@@ -275,18 +275,18 @@
 {
 	var formations = [];
 
-	formations.push("Formation0");
-	formations.push("Formation1");
-	formations.push("Formation2");
-	formations.push("Formation3");
-	formations.push("Formation4");
-	formations.push("Formation5");
-	formations.push("Formation6");
-	formations.push("Formation7");
-	formations.push("Formation8");
-	formations.push("Formation9");
-	formations.push("Formation10");
-	formations.push("Formation11");
+	formations.push("Loose");
+	formations.push("Box");
+	formations.push("Column Closed");
+	formations.push("Line Closed");
+	formations.push("Column Open");
+	formations.push("Line Open");
+	formations.push("Flank");
+	formations.push("Skirmish");
+	formations.push("Wedge");
+	formations.push("Testudo");
+	formations.push("Phalanx");
+	formations.push("Syntagma");
 	formations.push("Formation12");
 	return formations;
 }
Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(revision 9207)
+++ binaries/data/mods/public/gui/session/input.js	(working copy)
@@ -1062,11 +1062,17 @@
 // Performs the specified formation
 function performFormation(entity, formationName)
 {
-	submitChatDirectly("FORMATIONS are not implemented yet.");
-
 	if (entity)
 	{
 		console.write(formationName);
+
+		var selection = g_Selection.toList();
+		Engine.PostNetworkCommand({
+			"type": "formation",
+			"entities": selection,
+			"name": formationName
+		});
+
 	}
 }
 
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(revision 9207)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(working copy)
@@ -236,6 +236,17 @@
 		
 		var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
 		cmpGarrisonHolder.UnloadAll();
+		break;		
+
+	case "formation":
+		var cmpUnitAI = GetFormationUnitAI(cmd.entities);
+		if(!cmpUnitAI)
+			break;
+		var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation);
+		if (!cmpFormation)
+			break;
+		cmpFormation.LoadFormation(cmd.name);
+		cmpFormation.MoveMembersIntoFormation(true);
 		break;
 		
 	default:
@@ -339,6 +350,30 @@
 		formationEnt = Engine.AddEntity("special/formation");
 		var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
 		cmpFormation.SetMembers(formation.entities);
+
+		// We retrieve the last common formation name
+		var lastFormationName = undefined;
+		for each (var ent in formation.entities)
+		{
+			var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+			if (cmpUnitAI)
+			{
+				var name = cmpUnitAI.GetLastFormationName();
+				if (lastFormationName == undefined)
+				{
+					lastFormationName = name;
+				}
+				else if (lastFormationName != name)
+				{
+					lastFormationName = undefined;
+					break;
+				}
+			}
+		}
+		if (lastFormationName)
+			cmpFormation.LoadFormation(lastFormationName);
+		else
+			cmpFormation.LoadFormation("default");
 	}
 
 	return Engine.QueryInterface(formationEnt, IID_UnitAI);
Index: binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- binaries/data/mods/public/simulation/components/Identity.js	(revision 9207)
+++ binaries/data/mods/public/simulation/components/Identity.js	(working copy)
@@ -68,6 +68,7 @@
 						"<value>Infantry</value>" +
 						"<value>Cavalry</value>" +
 						"<value>Ranged</value>" +
+						"<value>Melee</value>" +
 						"<value>Mechanical</value>" +
 						"<value>Ship</value>" +
 						"<value>Siege</value>" +
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(revision 9207)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(working copy)
@@ -1161,6 +1161,7 @@
 	this.order = undefined; // always == this.orderQueue[0]
 	this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
 	this.isIdle = false;
+	this.lastFormationName = "default";
 
 	this.SetStance(this.template.DefaultStance);
 };
@@ -1740,6 +1741,16 @@
 	return this.formationController;
 };
 
+UnitAI.prototype.SetLastFormationName = function(name)
+{
+	this.lastFormationName = name;
+};
+
+UnitAI.prototype.GetLastFormationName = function()
+{
+  return this.lastFormationName;
+};
+
 /**
  * Returns the estimated distance that this unit will travel before either
  * finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
Index: binaries/data/mods/public/simulation/components/Formation.js
===================================================================
--- binaries/data/mods/public/simulation/components/Formation.js	(revision 9207)
+++ binaries/data/mods/public/simulation/components/Formation.js	(working copy)
@@ -9,6 +9,7 @@
 {
 	this.members = []; // entity IDs currently belonging to this formation
 	this.columnar = false; // whether we're travelling in column (vs box) formation
+	this.formationName = "default";
 };
 
 Formation.prototype.GetMemberCount = function()
@@ -114,8 +115,6 @@
 	var active = [];
 	var positions = [];
 
-	var types = { "Unknown": 0 }; // TODO: make this work so we put ranged behind infantry etc
-
 	for each (var ent in this.members)
 	{
 		var cmpPosition = Engine.QueryInterface(ent, IID_Position);
@@ -124,8 +123,6 @@
 
 		active.push(ent);
 		positions.push(cmpPosition.GetPosition());
-
-		types["Unknown"] += 1; // TODO
 	}
 
 	// Work out whether this should be a column or box formation
@@ -133,7 +130,7 @@
 	var walkingDistance = cmpUnitAI.ComputeWalkingDistance();
 	this.columnar = (walkingDistance > g_ColumnDistanceThreshold);
 
-	var offsets = this.ComputeFormationOffsets(types, this.columnar);
+	var offsets = this.ComputeFormationOffsets(active, this.columnar);
 
 	var avgpos = this.ComputeAveragePosition(positions);
 	var avgoffset = this.ComputeAveragePosition(offsets);
@@ -143,13 +140,11 @@
 	if (moveCenter || !cmpPosition.IsInWorld())
 		cmpPosition.JumpTo(avgpos.x, avgpos.z);
 
-	// TODO: assign to minimise worst-case distances or whatever
-
-	for (var i = 0; i < active.length; ++i)
+	for (var i = 0; i < offsets.length; ++i)
 	{
 		var offset = offsets[i];
 
-		var cmpUnitAI = Engine.QueryInterface(active[i], IID_UnitAI);
+		var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
 		cmpUnitAI.ReplaceOrder("FormationWalk", {
 			"target": this.entity,
 			"x": offset.x - avgoffset.x,
@@ -177,53 +172,325 @@
 	cmpPosition.JumpTo(avgpos.x, avgpos.z);
 };
 
-Formation.prototype.ComputeFormationOffsets = function(types, columnar)
+Formation.prototype.ComputeFormationOffsets = function(active, columnar)
 {
 	var separation = 4; // TODO: don't hardcode this
 
-	var count = types["Unknown"];
+	var types = {  	"Hero" : [],
+									"Melee" : [],
+									"Ranged" : [],
+									"Support" : [],
+									"Unknown": []
+									}; // TODO: make this work so we put ranged behind infantry etc
 
+	for each (var ent in active)
+	{
+		var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
+		var classes = cmpIdentity.GetClassesList();
+		var done = false;
+		for each (var cla in classes)
+		{
+			if(cla in types)
+			{
+				types[cla].push(ent);
+				done = true;
+				break;
+			}
+		}
+		if (!done)
+		{
+			types["Unknown"].push(ent);
+		}
+	}
+
+	var count = active.length;
+
+	var shape = undefined;
+	var ordering = "default";
+
+	var offsets = [];
+
 	// Choose a sensible width for the basic default formation
 	var cols;
-	if (columnar)
+	if (columnar || this.formationName == "Column Closed")
 	{
 		// Have at most 3 files
 		if (count <= 3)
 			cols = count;
 		else
 			cols = 3;
+		shape = "square";
 	}
-	else
+	else if (this.formationName == "default" || this.formationName == "Phalanx")
 	{
 		// Try to have at least 5 files (so batch training gives a single line),
 		// and at most 8
 		if (count <= 5)
 			cols = count;
-		if (count <= 10)
+		else if (count <= 10)
 			cols = 5;
 		else if (count <= 16)
 			cols = Math.ceil(count / 2);
 		else
 			cols = 8;
+		shape = "square";
 	}
+	else if (this.formationName == "Line Closed")
+	{
+		if (count <= 3)
+			cols = count;
+		else if(count < 30)
+			cols = Math.ceil(count/2);
+		else
+			cols = Math.ceil(count/3);
+		shape = "square";
+	}
+	else if (this.formationName == "Testudo")
+	{
+		if (count < 9 || count > 25)
+		{
+			this.LoadFormation("default");
+			return offsets;
+		}
+		cols = Math.ceil(Math.sqrt(count));
 
-	var ranks = Math.ceil(count / cols);
+		shape = "square";
+	}
+	else if (this.formationName == "Column Open")
+	{
+		cols = 2
+		shape = "opensquare";
+	}
+	else if (this.formationName == "Line Open")
+	{
+		if (count <= 5)
+			cols = 3;
+		else if (count <= 11)
+			cols = 4;
+		else if (count <= 18)
+			cols = 5;
+		else
+			cols = 6;
+		shape = "opensquare";
+	}
+	else if (this.formationName == "Loose") // Circle
+	{
+		var depth;
+		if (count < 5 || count > 36)
+		{
+			this.LoadFormation("default");
+			return offsets;
+		}
+		depth = Math.ceil(count / 12);
 
-	var offsets = [];
+		var left = count;
+		var radius = Math.min(left,12) * separation / (2 * Math.PI);
+		for (var c = 0; c < depth; ++c)
+		{
+			var ctodo = Math.min(left,12);
+			var cradius = radius - c * separation / 2;
+			var delta = 2 * Math.PI / ctodo;
+			for (var alpha = 0; ctodo; alpha+=delta)
+			{
+				var x = Math.cos(alpha) * cradius;
+				var z = Math.sin(alpha) * cradius;
+				offsets.push({"x": x, "z": z});
+				ctodo--;
+				left--;
+			}
+		}
+	}
+	else if (this.formationName == "Box") // Box
+	{
+		var root;
+		if (count > 49)
+		{
+			this.LoadFormation("default");
+			return offsets;
+		}
+		if (count <= 9)
+			root = 3;
+		else if (count > 9 && count <= 25)
+			root = 5;
+		else if (count > 25 && count <= 49)
+			root = 7;
 
-	var left = count;
-	for (var r = 0; r < ranks; ++r)
+		var left = count;
+		var meleeleft = types["Melee"].length;
+		for (var sq = Math.floor(root/2); sq >= 0; --sq)
+		{
+			var width = sq * 2 + 1;
+			var stodo;
+			if (sq == 0)
+			{
+				stodo = left;
+			}
+			else
+			{
+				if (meleeleft >= width*width - (width-2)*(width-2)) // form a complete box
+				{
+					stodo = width*width - (width-2)*(width-2);
+					meleeleft -= stodo;
+				}
+				else	// compact
+					stodo = Math.max(0,left - (width-2)*(width-2));
+			}
+
+			for (var r = -sq; r <= sq && stodo; ++r)
+			{
+				for (var c = -sq; c <= sq && stodo; ++c)
+				{
+					if (Math.abs(r) == sq || Math.abs(c) == sq)
+					{
+						var x = c * separation;
+						var z = -r * separation;
+						offsets.push({"x": x, "z": z});
+						stodo--;
+						left--;
+					}
+				}
+			}
+		}
+	}
+	else if (this.formationName == "Skirmish")
 	{
-		var n = Math.min(left, cols);
-		for (var c = 0; c < n; ++c)
+		if (count < 9 || count > 25)
 		{
-			var x = ((n-1)/2 - c) * separation;
-			var z = -r * separation;
-			offsets.push({"x": x, "z": z});
+			this.LoadFormation("default");
+			return offsets;
 		}
-		left -= n;
+		cols = Math.ceil(count/2);
+		shape = "opensquare";
 	}
+	else if (this.formationName == "Wedge")
+	{
+		var depth;
+		if (count > 49)
+		{
+			this.LoadFormation("default");
+			return offsets;
+		}
+		if (count <= 9)
+			depth = 3;
+		else if (count > 9 && count <= 16)
+			depth = 4;
+		else if (count > 16 && count <= 25)
+			depth = 5;
+		else if (count > 25 && count <= 49)
+			depth = 7;
 
+		var left = count;
+		var width = 2 * depth - 1;
+		for (var p = 0; p < depth && left; ++p)
+		{
+			for (var r = p; r < depth && left; ++r)
+			{
+				var c1 = depth - r + p;
+				var c2 = depth + r - p;
+
+				if (left)
+				{
+						var x = c1 * separation;
+						var z = -r * separation;
+						offsets.push({"x": x, "z": z});
+						left--;
+				}
+				if (left && c1 != c2)
+				{
+						var x = c2 * separation;
+						var z = -r * separation;
+						offsets.push({"x": x, "z": z});
+						left--;
+				}
+			}
+		}
+	}
+	else if (this.formationName == "Flank")
+	{
+		if (count < 8 || count > 24)
+		{
+			this.LoadFormation("default");
+			return offsets;
+		}
+		cols = 3;
+		var leftside = [];
+		leftside[0] = Math.ceil(count/2);
+		leftside[1] = Math.floor(count/2);
+		ranks = Math.ceil(leftside[0] / cols);
+		var off = - separation * 4;
+		for (var side = 0; side < 2; ++side)
+		{
+			var left = leftside[side];
+			off += side * separation * 8;
+			for (var r = 0; r < ranks; ++r)
+			{
+				var n = Math.min(left, cols);
+				for (var c = 0; c < n; ++c)
+				{
+					var x = off + ((n-1)/2 - c) * separation;
+					var z = -r * separation;
+					offsets.push({"x": x, "z": z});
+				}
+				left -= n;
+			}
+		}
+	}
+	else if (this.formationName == "Formation12" || this.formationName == "Syntagma")
+	{
+		return offsets; //@todo
+	}
+
+	if (shape == "square")
+	{
+		var ranks = Math.ceil(count / cols);
+
+		var left = count;
+		for (var r = 0; r < ranks; ++r)
+		{
+			var n = Math.min(left, cols);
+			for (var c = 0; c < n; ++c)
+			{
+				var x = ((n-1)/2 - c) * separation;
+				var z = -r * separation;
+				offsets.push({"x": x, "z": z});
+			}
+			left -= n;
+		}
+	}
+	else if (shape == "opensquare")
+	{
+		var left = count;
+		for (var r = 0; left; ++r)
+		{
+			var n = Math.min(left, cols - (r&1?1:0));
+			for (var c = 0; c < 2*n; c+=2)
+			{
+				var x = (- c - (r&1)) * separation;
+				var z = -r * separation;
+				offsets.push({"x": x, "z": z});
+			}
+			left -= n;
+		}
+	}
+
+	// TODO: assign to minimise worst-case distances or whatever
+	if (ordering == "default")
+	{
+		for each (var offset in offsets)
+		{
+			var ent = undefined;
+			for (var t in types)
+			{
+				if (types[t].length)
+				{
+					ent = types[t].pop();
+					offset.ent = ent
+					break;
+				}
+			}
+		}
+	}
+
 	return offsets;
 };
 
@@ -286,4 +553,14 @@
 		this.RemoveMembers([msg.entity]);
 };
 
+Formation.prototype.LoadFormation = function(formationName)
+{
+	this.formationName = formationName;
+	for each(var ent in this.members)
+	{
+		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+		cmpUnitAI.SetLastFormationName(this.formationName);
+	}
+}
+
 Engine.RegisterComponentType(IID_Formation, "Formation", Formation);
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml	(revision 9207)
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml	(working copy)
@@ -2,6 +2,7 @@
 <Entity parent="template_unit_infantry">
   <Identity>
     <GenericName>Ranged</GenericName>
+    <Classes datatype="tokens">Ranged Infantry</Classes>
   </Identity>
   <Health>
     <Max>90</Max>
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee.xml	(revision 9207)
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee.xml	(working copy)
@@ -2,6 +2,7 @@
 <Entity parent="template_unit_infantry">
   <Identity>
     <GenericName>Melee Infantry</GenericName>
+    <Classes datatype="tokens">Melee Infantry</Classes>
   </Identity>
   <Health>
     <Max>100</Max>

