Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(revision 10412)
+++ binaries/data/mods/public/gui/session/input.js	(working copy)
@@ -298,6 +298,10 @@
 			return {"type": "garrison", "cursor": "action-garrison", "target": target};
 		else
 			return 	{"type": "none", "cursor": "action-garrison-disabled", "target": undefined};
+	} 
+	else if (Engine.HotkeyIsPressed("attackmove"))
+	{
+		return {"type": "attack-move"};
 	}
 	else
 	{
@@ -909,6 +913,12 @@
 		Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued});
 		Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
 		return true;
+		
+	case "attack-move":
+		var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
+		Engine.PostNetworkCommand({"type": "attack-move", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
+		Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack_move", "entity": selection[0] });
+		return true;
 
 	case "build": // (same command as repair)
 	case "repair":
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(revision 10412)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(working copy)
@@ -52,6 +52,13 @@
 			});
 		}
 		break;
+		
+	case "attack-move":
+		var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
+		GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
+			cmpUnitAI.AttackMove(cmd.x, cmd.z, cmd.queued);
+		});
+		break;
 
 	case "repair":
 		// This covers both repairing damaged buildings, and constructing unfinished foundations
@@ -413,7 +420,7 @@
 
 	if (formedEnts.length == 0)
 	{
-		// No units support the foundation - return all the others
+		// No units support the formation - return all the others
 		return nonformedUnitAIs;
 	}
 
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(revision 10412)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(working copy)
@@ -164,7 +164,15 @@
 
 		this.SetNextState("FORMATIONMEMBER.WALKING");
 	},
+	
+	// Called when being told to attack-move as part of a formation
+	"Order.FormationAttackMove": function(msg) {
+		var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+		cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
 
+		this.SetNextState("FORMATIONMEMBER.ATTACKMOVING");
+	},
+
 	// Special orders:
 	// (these will be overridden by various states)
 
@@ -394,7 +402,23 @@
 			// Don't bother now, just disband the formation immediately.
 			cmpFormation.Disband();
 		},
+		
+		"Order.AttackMove": function(msg){
+			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+			// The AttackMoveAttack order can mess up the destination position in this.order, so save it.
+			this.attackMovePosition = {x: this.order.data.x, z: this.order.data.z};
+			cmpFormation.CallMemberFunction("SetHeldPosition", [this.attackMovePosition.x, this.attackMovePosition.z]);
 
+			this.MoveToPoint(this.attackMovePosition.x, this.attackMovePosition.z);
+			this.SetNextState("FORMATIONCONTROLLER.ATTACKMOVE.WALK");
+		},
+		
+		"Order.AttackMoveAttack": function(msg){
+			// The enemy that was spotted by a formation member.
+			this.target = msg.data.target;
+			this.SetNextState("FORMATIONCONTROLLER.ATTACKMOVE.ATTACK");
+		},
+
 		"Order.Repair": function(msg) {
 			// TODO: see notes in Order.Attack
 			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
@@ -439,6 +463,64 @@
 				cmpFormation.Disband();
 			},
 		},
+		
+		"ATTACKMOVE": {
+			"WALK": {
+				// Whe we enter/re-enter the walk state then set the formation moving again towards the final destination
+				"enter": function(msg){
+					var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+					if (this.order.data.x) // The AttackMoveAttack order can mess up the destination position in this.order, so save it.
+						this.attackMovePosition = {x: this.order.data.x, z: this.order.data.z};
+					cmpFormation.CallMemberFunction("SetHeldPosition", [this.attackMovePosition.x, this.attackMovePosition.z]);
+
+					this.MoveToPoint(this.attackMovePosition.x, this.attackMovePosition.z);
+				},
+				
+				"MoveStarted": function(msg) {
+					var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+					cmpFormation.MoveMembersIntoFormation(true, true);
+				},
+
+				// For when we have arrived at the commanded destination, break up the formation.
+				"MoveCompleted": function(msg) {
+					if (this.FinishOrder())
+						return;
+
+					var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+					
+					cmpFormation.Disband();
+				},
+			},
+			
+			"ATTACK":{
+				// Send all of the formation to attack the enemy that was spotted
+				"enter": function(msg){
+					// When we enter the attack state stop so the formation controller just stays in one spot until 
+					// the members have finished attacking stuff.
+					this.StopMoving();
+					
+					var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+					cmpFormation.CallMemberFunction("Attack", [this.order.data.target]);
+					cmpFormation.suspendFormation();
+					
+					// A timer to check when we should resume the march
+					this.StartTimer(1000, 1000);
+				},
+				
+				"Timer": function(msg){
+					var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+					// If more than 80% of the formation is idle then resume the walk towards the destination.
+					if (cmpFormation.CountMemberFunction("IsIdle", []) >= 0.8 * cmpFormation.GetMemberCount()){
+						cmpFormation.resumeFormation();
+						this.SetNextState("FORMATIONCONTROLLER.ATTACKMOVE.WALK");
+					}
+				},
+				
+				"leave": function(msg){
+					this.StopTimer();
+				},
+			},
+		},
 	},
 
 
@@ -496,6 +578,31 @@
 			// and no longer moving, because the formation controller might
 			// move and we'll automatically start chasing after it again)
 		},
+		
+		"ATTACKMOVING": {
+			"enter": function(){
+				this.SelectAnimation("move");
+				// Watch for enemies
+				var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+				var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);
+			},
+			
+			"LosRangeUpdate": function(msg) {
+				var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
+				// TODO: better target selection, e.g. ignore scouts and houses
+				if (msg.data.added[0]){
+					// Tell the formation controller to stop walking and attack this enemy
+					// Using an order here isn't ideal because this is basically a sub-order,
+					// there doesn't seem to be an alternative though.
+					cmpUnitAI.ReplaceOrder("AttackMoveAttack", {target: msg.data.added[0]});
+				}
+			},
+			
+			"leave": function(msg){
+				var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+				rangeMan.DisableActiveQuery(this.losRangeQuery);
+			}
+		},
 	},
 
 
@@ -2173,6 +2280,7 @@
 		var order = this.orderQueue[i];
 		switch (order.type)
 		{
+		case "AttackMove":
 		case "Walk":
 			// Add the distance to the target point
 			var dx = order.data.x - pos.x;
@@ -2185,6 +2293,7 @@
 
 			break; // and continue the loop
 
+		case "AttackMoveAttack":
 		case "WalkToTarget":
 		case "Flee":
 		case "LeaveFoundation":
@@ -2258,6 +2367,11 @@
 	this.AddOrder("Attack", { "target": target, "force": true }, queued);
 };
 
+UnitAI.prototype.AttackMove = function(x, z, queued)
+{
+	this.AddOrder("AttackMove", { "x": x, "z": z }, queued);
+};
+
 UnitAI.prototype.Garrison = function(target, queued)
 {
 	if (!this.CanGarrison(target))
Index: binaries/data/mods/public/simulation/components/Formation.js
===================================================================
--- binaries/data/mods/public/simulation/components/Formation.js	(revision 10412)
+++ binaries/data/mods/public/simulation/components/Formation.js	(working copy)
@@ -8,13 +8,15 @@
 Formation.prototype.Init = function()
 {
 	this.members = []; // entity IDs currently belonging to this formation
+	this.suspendedMembers = []; // entity ID's which currently belong to the formation but are being left to their own devices.
 	this.columnar = false; // whether we're travelling in column (vs box) formation
 	this.formationName = "Line Closed";
 };
 
 Formation.prototype.GetMemberCount = function()
 {
-	return this.members.length;
+	var allMembers = this.members.concat(this.suspendedMembers);
+	return allMembers.length;
 };
 
 /**
@@ -26,7 +28,7 @@
  */
 Formation.prototype.GetPrimaryMember = function()
 {
-	return this.members[0];
+	return this.members[0] || this.suspendedMembers[0];
 };
 
 /**
@@ -36,6 +38,7 @@
  */
 Formation.prototype.SetMembers = function(ents)
 {
+	this.suspendedMembers = [];
 	this.members = ents;
 
 	for each (var ent in this.members)
@@ -57,6 +60,7 @@
 Formation.prototype.RemoveMembers = function(ents)
 {
 	this.members = this.members.filter(function(e) { return ents.indexOf(e) == -1; });
+	this.suspendedMembers = this.suspendedMembers.filter(function(e) { return ents.indexOf(e) == -1; });
 
 	for each (var ent in ents)
 	{
@@ -65,7 +69,7 @@
 	}
 
 	// If there's nobody left, destroy the formation
-	if (this.members.length == 0)
+	if (this.members.length == 0 && this.suspendedMembers.length == 0)
 	{
 		Engine.DestroyEntity(this.entity);
 		return;
@@ -82,23 +86,38 @@
  */
 Formation.prototype.Disband = function()
 {
-	for each (var ent in this.members)
+	var allMembers = this.members.concat(this.suspendedMembers);
+	for each (var ent in allMembers)
 	{
 		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
 		cmpUnitAI.SetFormationController(INVALID_ENTITY);
 	}
 
 	this.members = [];
+	this.suspendedMembers = [];
 
 	Engine.DestroyEntity(this.entity);
 };
 
+// Allow the units in the formation to move freely but keep them registered to this formation
+Formation.prototype.suspendFormation = function(){
+	this.suspendedMembers = this.members;
+	this.members = [];
+};
+
+// Make the units resume their positions in the formation
+Formation.prototype.resumeFormation = function(){
+	this.SetMembers(this.suspendedMembers);
+	this.suspendedMembers = [];
+};
+
 /**
  * Call obj.funcname(args) on UnitAI components of all members.
  */
 Formation.prototype.CallMemberFunction = function(funcname, args)
 {
-	for each (var ent in this.members)
+	var allMembers = this.members.concat(this.suspendedMembers);
+	for each (var ent in allMembers)
 	{
 		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
 		cmpUnitAI[funcname].apply(cmpUnitAI, args);
@@ -106,11 +125,28 @@
 };
 
 /**
+ * Call obj.funcname(args) on UnitAI components of all members and count true responses
+ */
+Formation.prototype.CountMemberFunction = function(funcname, args)
+{
+	var count = 0;
+	var allMembers = this.members.concat(this.suspendedMembers);
+	for each (var ent in allMembers)
+	{
+		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+		if (cmpUnitAI[funcname].apply(cmpUnitAI, args))
+			count ++;
+	}
+	return count;
+};
+
+/**
  * Set all members to form up into the formation shape.
  * If moveCenter is true, the formation center will be reinitialised
  * to the center of the units.
+ * If attackMove is true then the units will be given an AttackMove order
  */
-Formation.prototype.MoveMembersIntoFormation = function(moveCenter)
+Formation.prototype.MoveMembersIntoFormation = function(moveCenter, attackMove)
 {
 	var active = [];
 	var positions = [];
@@ -145,11 +181,19 @@
 		var offset = offsets[i];
 
 		var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
-		cmpUnitAI.ReplaceOrder("FormationWalk", {
-			"target": this.entity,
-			"x": offset.x - avgoffset.x,
-			"z": offset.z - avgoffset.z
-		});
+		if (attackMove){
+			cmpUnitAI.ReplaceOrder("FormationAttackMove", {
+				"target": this.entity,
+				"x": offset.x - avgoffset.x,
+				"z": offset.z - avgoffset.z
+			});
+		}else{
+			cmpUnitAI.ReplaceOrder("FormationWalk", {
+				"target": this.entity,
+				"x": offset.x - avgoffset.x,
+				"z": offset.z - avgoffset.z
+			});
+		}
 	}
 };
 
@@ -594,8 +638,10 @@
 {
 	// When an entity is captured or destroyed, it should no longer be
 	// controlled by this formation
+	
+	var allMembers = this.members.concat(this.suspendedMembers);
 
-	if (this.members.indexOf(msg.entity) != -1)
+	if (allMembers.indexOf(msg.entity) != -1)
 		this.RemoveMembers([msg.entity]);
 };
 
@@ -611,12 +657,23 @@
 		var cmpNewUnitAI = Engine.QueryInterface(msg.newentity, IID_UnitAI);
 		cmpNewUnitAI.SetFormationController(this.entity);
 	}
+	if (this.suspendedMembers.indexOf(msg.entity) != -1)
+	{
+		this.suspendedMembers[this.suspendedMembers.indexOf(msg.entity)] = msg.newentity;
+
+		var cmpOldUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI);
+		cmpOldUnitAI.SetFormationController(INVALID_ENTITY);
+
+		var cmpNewUnitAI = Engine.QueryInterface(msg.newentity, IID_UnitAI);
+		cmpNewUnitAI.SetFormationController(this.entity);
+	}
 }
 
 Formation.prototype.LoadFormation = function(formationName)
 {
+	var allMembers = this.members.concat(this.suspendedMembers);
 	this.formationName = formationName;
-	for each (var ent in this.members)
+	for each (var ent in allMembers)
 	{
 		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
 		cmpUnitAI.SetLastFormationName(this.formationName);
Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg	(revision 10412)
+++ binaries/data/config/default.cfg	(working copy)
@@ -202,6 +202,8 @@
 hotkey.profile.toggle = "F11"               ; Enable/disable real-time profiler
 hotkey.profile.save = "Shift+F11"           ; Save current profiler data to logs/profile.txt
 
+hotkey.attackmove = Super
+
 ; EXPERIMENTAL: joystick/gamepad settings
 joystick.enable = false
 joystick.deadzone = 8192
