Index: binaries/data/mods/public/simulation/components/GroupWalkManager.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/GroupWalkManager.js
@@ -0,0 +1,266 @@
+function GroupWalkManager() {}
+
+GroupWalkManager.prototype.Schema =
+ "";
+
+// This is a simple system component that keeps track of existing "groups"
+// when a "walk together" order is issued as well as necessary information.
+
+GroupWalkManager.prototype.Init = function()
+{
+ this.groups = new Map();
+ this.nextGroupID = 0;
+};
+
+GroupWalkManager.prototype.Serialize = function()
+{
+};
+
+GroupWalkManager.prototype.CreateGroup = function(formableEntsID, x, z, range, formationTemplate)
+{
+ let speed = 50.0; // TODO: put formation max speed?
+ for (let ent of formableEntsID)
+ {
+ let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
+ let sp = cmpUnitMotion.GetBaseSpeed();
+ if (sp < speed)
+ speed = sp;
+ }
+ let group = {
+ "entities" : formableEntsID,
+ "x" : x,
+ "z" : z,
+ "range": range,
+ "formationTemplate":formationTemplate,
+ "maxSpeed": speed,
+ "state": "waiting",
+ "readyForNextStep" : [],
+ "entHasObstructedPath" : [],
+ "step":0,
+ "waypoints": [],
+ "offsets": {}
+ };
+ this.groups.set(this.nextGroupID++, group);
+
+ return this.nextGroupID - 1;
+};
+
+GroupWalkManager.prototype.GetMaxSpeed = function(ID)
+{
+ // undefined if undefined
+ return !!this.groups.get(ID) ? this.groups.get(ID).maxSpeed : 0;
+}
+
+GroupWalkManager.prototype.GetGroup = function(ID)
+{
+ // undefined if undefined
+ return this.groups.get(ID);
+}
+
+// An entity is telling us it won't be able to reach the intended position
+// if enough entities have told us this, skip the waypoint unless it's the last
+GroupWalkManager.prototype.SetBlockedPath = function(ID, ent)
+{
+ if (!this.groups.get(ID))
+ {
+ error("Entity " + ent + " blocked for unkown group " + ID);
+ return;
+ }
+ let group = this.groups.get(ID);
+ if (group.entities.indexOf(ent) === -1)
+ {
+ error("Entity " + ent + " blocked for group " + ID + " it is not a part of.");
+ return;
+ }
+ if (group.entHasObstructedPath.indexOf(ent) === -1)
+ group.entHasObstructedPath.push(ent);
+
+ if (group.entHasObstructedPath.length < group.entities.length * 0.1)
+ return;
+
+ if (group.waypoints.length <= 1)
+ return;
+
+ this.AdvanceGroupState(ID);
+}
+
+// An entity is telling us it's ready for the next waypoint
+// if enough entities have told us, shift the whole group.
+GroupWalkManager.prototype.SetReady = function(ID, ent)
+{
+ if (!this.groups.get(ID))
+ {
+ error("Entity " + ent + " ready for unkown group " + ID);
+ return;
+ }
+ let group = this.groups.get(ID);
+ if (group.entities.indexOf(ent) === -1)
+ {
+ error("Entity " + ent + " ready for group " + ID + " it is not a part of.");
+ return;
+ }
+ if (group.readyForNextStep.indexOf(ent) === -1)
+ group.readyForNextStep.push(ent);
+
+ if (group.state == "waiting" && group.readyForNextStep.length !== group.entities.length
+ || group.readyForNextStep.length < group.entities.length * 0.85)
+ return;
+
+ this.AdvanceGroupState(ID);
+}
+
+GroupWalkManager.prototype.AdvanceGroupState = function(ID)
+{
+ let group = this.groups.get(ID);
+ if (!this.groups.get(ID))
+ {
+ error("Trying to advance unknown group " + ID);
+ return;
+ }
+
+ group.readyForNextStep = [];
+ if (group.state == "waiting")
+ {
+ group.rallyPoint = { "x":0, "z": 0 };
+
+ // TODO: find the algo to find the initial grouping position.
+
+ let x = 0;
+ let z = 0;
+ let bestDist = Number.POSITIVE_INFINITY;
+ for (let ent of group.entities)
+ {
+ let cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ let ex = cmpPosition.GetPosition2D().x;
+ let ez = cmpPosition.GetPosition2D().y;
+
+ let entPos = new Vector2D(ex, ez);
+ let goal = new Vector2D(group.x, group.z);
+
+ let dist = entPos.distanceToSquared(goal);
+ if (dist < bestDist)
+ {
+ x = ex;
+ z = ez;
+ bestDist = dist;
+ }
+ }
+ group.rallyPoint.x = x;
+ group.rallyPoint.z = z;
+
+ let p1 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z);
+
+ let cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
+ let path = cmpPathfinder.ComputePath(group.rallyPoint.x, group.rallyPoint.z, group.x, group.z, "large");
+ group.waypoints = path;
+ group.step = group.waypoints.length;
+ if (group.waypoints.length > 1)
+ {
+ group.rallyPoint = { "x":group.waypoints[group.step-1].x, "z":group.waypoints[group.step-1].y };
+ group.step--;
+ }
+
+ // compute offsets.
+ let p2 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z);
+ p1.sub(p2).mult(-1);
+
+ let angle = Math.atan2(p1.x, p1.y);
+ group.offsets = this.ComputeOffsetsForWaypoint(angle, p1, group.entities);
+
+ group.state = "walking";
+ }
+ else if (group.state == "walking")
+ {
+ if (group.step === 0)
+ {
+ group.state = "arrived";
+ return;
+ }
+ let p1 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z);
+
+ group.rallyPoint = { "x":group.waypoints[group.step-1].x, "z":group.waypoints[group.step-1].y };
+ group.step--;
+
+ // compute offsets.
+ let p2 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z);
+ p1.sub(p2).mult(-1);
+
+ let angle = Math.atan2(p1.x, p1.y);
+ group.offsets = this.ComputeOffsetsForWaypoint(angle, p1, group.entities);
+
+ group.entHasObstructedPath = [];
+
+ group.state = "walking";
+
+ for (let ent of group.entities)
+ {
+ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ cmpUnitAI.SetNextStateAlwaysEntering("WALKING");
+ }
+ }
+}
+
+GroupWalkManager.prototype.ComputeOffsetsForWaypoint = function(angle, center, entities)
+{
+ // for now we'll do a simple rectangular formations
+ // TODO: support more stuff.
+ let ret = {};
+
+ let xW = Math.round(Math.min(6, Math.max(2, entities.length/4)));
+ let y = -1;
+ let maxYOffset = 0;
+ let largeEntities = [];
+ for (let i = 0; i < entities.length; i++)
+ {
+ let ent = entities[i];
+ let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
+ if (cmpUnitMotion.GetUnitClearance() > 1)
+ {
+ largeEntities.push(ent);
+ continue;
+ }
+ let x = i % xW;
+ if (x == 0)
+ y++;
+ let offsetX = 3 * (x-xW/2.0);
+ let offsetY = -4 * y;
+ maxYOffset = -offsetY;
+ let vector = new Vector2D(offsetX, offsetY);
+ ret[ent] = vector.rotate(angle);
+ }
+ let baseY = y * -4;
+ y = 0;
+ xW = Math.round(Math.min(3, Math.max(1, largeEntities.length/2)));
+ for (let i = 0; i < largeEntities.length; i++)
+ {
+ let ent = largeEntities[i];
+ let x = i % xW;
+ if (x == 0)
+ y++;
+ let offsetX = 9 * (x-xW/2.0);
+ let offsetY = baseY -10 * y;
+ maxYOffset = -offsetY;
+ let vector = new Vector2D(offsetX, offsetY);
+ ret[ent] = vector.rotate(angle);
+ }
+
+ for (let ent in ret)
+ ret[ent].y += maxYOffset / 2.0;
+
+ return ret;
+}
+
+GroupWalkManager.prototype.ResignFromGroup = function(ID, ent)
+{
+ let group = this.groups.get(ID);
+ if (!group)
+ return;
+ group.entities.splice(group.entities.indexOf(ent), 1);
+ if (group.readyForNextStep.indexOf(ent) !== -1)
+ group.readyForNextStep.splice(group.readyForNextStep.indexOf(ent), 1);
+
+ if (!group.entities.length)
+ this.groups.delete(ID);
+}
+
+Engine.RegisterSystemComponentType(IID_GroupWalkManager, "GroupWalkManager", GroupWalkManager);
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -378,6 +378,7 @@
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
+ "formation": cmpUnitAI.GetFormationTemplate(),
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
@@ -803,7 +804,7 @@
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
- return [];
+ return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
@@ -833,12 +834,10 @@
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
- for (let ent of data.ents)
+ for (let ent of data.entities)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
- // GetLastFormationName is named in a strange way as it (also) is
- // the value of the current formation (see Formation.js LoadFormation)
- if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
+ if (cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate)
return true;
}
return false;
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js
+++ binaries/data/mods/public/simulation/components/UnitAI.js
@@ -222,6 +222,34 @@
},
+ "Order.GroupWalk": function(msg) {
+ if (this.IsAnimal() || this.IsTurret())
+ {
+ this.FinishOrder();
+ return;
+ }
+
+ // For packable units:
+ // 1. If packed, we can move.
+ // 2. If unpacked, we first need to pack, then follow case 1.
+ if (this.CanPack())
+ {
+ // Case 2: pack
+ this.PushOrderFront("Pack", { "force": true });
+ return;
+ }
+
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID);
+ if (!group || group.state != "waiting")
+ {
+ this.FinishOrder();
+ return;
+ }
+
+ this.SetNextStateAlwaysEntering("INDIVIDUAL.GROUPWALKING");
+ },
+
"Order.Walk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
@@ -763,6 +791,7 @@
"Attacked": function(msg) {
// Respond to attack if we always target attackers, or if we target attackers
// during passive orders (e.g. gathering/repairing are never forced)
+ // TODO: handle group-walking order.
if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
@@ -915,6 +944,157 @@
},
},
+ "GROUPWALKING": {
+ "enter": function() {
+ this.group = this.order.data.groupID;
+ this.SetNextState("WAITING");
+ },
+
+ "leave": function() {
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.group);
+ if (group)
+ cmpGroupWalkManager.ResignFromGroup(this.group, this.entity);
+ this.group = undefined;
+ },
+
+ "WAITING" : {
+ "enter": function() {
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID);
+ if (!group || group.state != "waiting")
+ {
+ this.FinishOrder();
+ return true;
+ }
+ cmpGroupWalkManager.SetReady(this.order.data.groupID, this.entity);
+ this.StartTimer(200, 400);
+ },
+
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID);
+ if (!group || group.state == "arrived")
+ {
+ this.FinishOrder();
+ return true;
+ }
+ if (group.state == "walking")
+ this.SetNextState("WALKING");
+ },
+ },
+ "WALKING" : {
+ "enter": function() {
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID);
+ if (!group || group.state != "walking")
+ {
+ this.FinishOrder();
+ return true;
+ }
+ let offset = group.offsets[this.entity];
+ this.MoveToPointRange(group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, true);
+
+ // TODO: for the first "grouping" order it'd be nice to skip this
+ let maxSpeed = cmpGroupWalkManager.GetMaxSpeed(this.order.data.groupID);
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ let ratio = maxSpeed / cmpUnitMotion.GetBaseSpeed();
+ cmpUnitMotion.SetSpeed(ratio);
+ this.StartTimer(200, 500);
+ this.step = group.step; // temporary, deleted in leave
+ },
+
+ "leave": function() {
+ this.StopTimer();
+ this.ready = undefined;
+ this.step = undefined;
+ this.validatedOffset = undefined;
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ cmpUnitMotion.SetSpeed(this.GetWalkSpeed());
+ },
+
+ "Timer": function() {
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID);
+ if (!group)
+ {
+ this.FinishOrder();
+ return true;
+ }
+ if (group.state == "arrived")
+ {
+ // TODO: should probably handle stances and do different things depending on the order here.
+
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ let offset = group.offsets[this.entity];
+ if (!cmpUnitMotion.IsActuallyMoving()
+ && cmpObstructionManager.IsInPointRange(this.entity, group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, 0))
+ this.FinishOrder();
+ return;
+ }
+ if (group.step < this.step)
+ {
+ // jump straight to the next rallypoint.
+ this.SetNextStateAlwaysEntering("WALKING");
+ return;
+ }
+ if (this.ready)
+ return;
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let range = group.step !== 0 ? 10 : group.range;
+ let offset = group.offsets[this.entity];
+ if (cmpObstructionManager.IsInPointRange(this.entity, group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, range))
+ {
+ this.ready = true;
+ cmpGroupWalkManager.SetReady(this.order.data.groupID, this.entity);
+ }
+ else if (!this.validatedOffset)
+ {
+ this.validatedOffset = true;
+ // check whether our reachable goal is close enough to our intended offset.
+ // TODO: ideally we'd also check the actual-path-distance to the rallypoint, and figure if we've been placed somewhere wrong.
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ let goal = cmpUnitMotion.GetReachableGoalPosition();
+ let distance = (goal.x - group.rallyPoint.x - offset.x) * (goal.x - group.rallyPoint.x - offset.x)
+ + (goal.y - group.rallyPoint.z - offset.y) * (goal.y - group.rallyPoint.z - offset.y)
+ if (distance > 3)
+ cmpGroupWalkManager.SetBlockedPath(this.order.data.groupID, this.entity);
+ }
+ },
+
+ "MoveCompleted": function(msg) {
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID);
+ if (!group)
+ {
+ this.FinishOrder();
+ return;
+ }
+
+ if (!msg.data.error)
+ return;
+
+ // UnitMotion has told us we were unlikely to reach our destination.
+ // if we're way out of position we should exit the group
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let offset = group.offsets[this.entity];
+ if (!cmpObstructionManager.IsInPointRange(this.entity, group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, 60))
+ {
+ this.FinishOrder();
+ return;
+ }
+ // tell our group we're ready, it's probably just that our waypoint is impassable right now
+ this.ready = true;
+ cmpGroupWalkManager.SetReady(this.order.data.groupID, this.entity);
+ }
+ }
+ },
+
"WALKING": {
"enter": function () {
},
@@ -2668,6 +2848,8 @@
this.lastAttacked = undefined;
this.lastHealed = undefined;
+ this.formationTemplate = "formations/null";
+
this.SetStance(this.template.DefaultStance);
};
@@ -4220,6 +4402,28 @@
};
/**
+ * Set the preferred formation for this entity.
+ */
+UnitAI.prototype.SetFormationTemplate = function(template)
+{
+ // TODO: validate this entity accepts this?
+ this.formationTemplate = template;
+};
+
+UnitAI.prototype.GetFormationTemplate = function()
+{
+ return this.formationTemplate;
+};
+
+/**
+ * Adds group-walk order to the queue, necessarily in front.
+ */
+UnitAI.prototype.GroupWalk = function(groupID)
+{
+ this.AddOrder("GroupWalk", { "groupID": groupID }, false);
+};
+
+/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
Index: binaries/data/mods/public/simulation/components/interfaces/GroupWalkManager.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/interfaces/GroupWalkManager.js
@@ -0,0 +1 @@
+Engine.RegisterInterface("GroupWalkManager");
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js
+++ binaries/data/mods/public/simulation/helpers/Commands.js
@@ -141,7 +141,9 @@
"walk": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued);
+ // the grouped entities will group-walk to the target, the non-grouped will just walk there.
+ unitAIs[0].forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
},
@@ -159,9 +161,11 @@
"attack-walk": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued);
- });
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued || +i);
+ });
},
"attack": function(player, cmd, data)
@@ -171,16 +175,23 @@
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Attack(cmd.target, cmd.queued || +i, allowCapture);
+ });
},
"patrol": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI =>
- cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued)
- );
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued || +i)
+ });
},
"heal": function(player, cmd, data)
@@ -188,9 +199,14 @@
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Heal(cmd.target, cmd.queued);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Heal(cmd.target, cmd.queued || +i);
+ });
},
"repair": function(player, cmd, data)
@@ -199,9 +215,14 @@
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued || +i);
+ });
},
"gather": function(player, cmd, data)
@@ -209,16 +230,23 @@
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Gather(cmd.target, cmd.queued);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Gather(cmd.target, cmd.queued || +i);
+ });
},
"gather-near-position": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
- });
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued || +i);
+ });
},
"returnresource": function(player, cmd, data)
@@ -226,9 +254,14 @@
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.ReturnResource(cmd.target, cmd.queued || +i);
+ });
},
"back-to-work": function(player, cmd, data)
@@ -441,9 +474,14 @@
return;
}
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Garrison(cmd.target, cmd.queued);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Garrison(cmd.target, cmd.queued || +i);
+ });
},
"guard": function(player, cmd, data)
@@ -456,15 +494,22 @@
return;
}
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Guard(cmd.target, cmd.queued);
- });
+ let pos = Engine.QueryInterface(cmd.target, IID_Position);
+ if (!pos)
+ return;
+ let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued);
+ for (let i in unitAIs)
+ unitAIs[i].forEach(cmpUnitAI => {
+ cmpUnitAI.Guard(cmd.target, cmd.queued || +i);
+ });
},
"stop": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.Stop(cmd.queued);
+ data.entities.forEach(ent => {
+ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (cmpUnitAI)
+ cmpUnitAI.Stop(cmd.queued);
});
},
@@ -556,8 +601,10 @@
"formation": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player, cmd.name).forEach(cmpUnitAI => {
- cmpUnitAI.MoveIntoFormation(cmd);
+ data.entities.forEach(ent => {
+ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (cmpUnitAI)
+ cmpUnitAI.SetFormationTemplate(cmd.name);
});
},
@@ -606,8 +653,10 @@
"setup-trade-route": function(player, cmd, data)
{
- GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
- cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
+ data.entities.forEach(ent => {
+ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (cmpUnitAI)
+ cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
@@ -835,32 +884,6 @@
}
/**
- * Get some information about the formations used by entities.
- * The entities must have a UnitAI component.
- */
-function ExtractFormations(ents)
-{
- var entities = []; // subset of ents that have UnitAI
- var members = {}; // { formationentity: [ent, ent, ...], ... }
- for (let ent of ents)
- {
- var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
- var fid = cmpUnitAI.GetFormationController();
- if (fid != INVALID_ENTITY)
- {
- if (!members[fid])
- members[fid] = [];
- members[fid].push(ent);
- }
- entities.push(ent);
- }
-
- var ids = [ id for (id in members) ];
-
- return { "entities": entities, "members": members, "ids": ids };
-}
-
-/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
@@ -1360,106 +1383,67 @@
}
}
-/**
- * Remove the given list of entities from their current formations.
- */
-function RemoveFromFormation(ents)
-{
- var formation = ExtractFormations(ents);
- for (var fid in formation.members)
- {
- var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
- if (cmpFormation)
- cmpFormation.RemoveMembers(formation.members[fid]);
- }
-}
-
-/**
- * Returns a list of UnitAI components, each belonging either to a
- * selected unit or to a formation entity for groups of the selected units.
- */
-function GetFormationUnitAIs(ents, player, formationTemplate)
+function CreateGroupWalkOrderIfNecessary(ents, x, z, range, queued)
{
- let unitAIs = [];
+ // First, loop through units and check if all of them share a single non-null formation.
+ let formationTemplate = null;
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
- continue;
- unitAIs.push(cmpUnitAI);
- }
- return unitAIs;
-}
-
-/**
- * Group a list of entities in clusters via single-links
- */
-function ClusterEntities(ents, separationDistance)
-{
- var clusters = [];
- if (!ents.length)
- return clusters;
-
- var distSq = separationDistance * separationDistance;
- var positions = [];
- // triangular matrix with the (squared) distances between the different clusters
- // the other half is not initialised
- var matrix = [];
- for (let i = 0; i < ents.length; ++i)
- {
- matrix[i] = [];
- clusters.push([ents[i]]);
- var cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
- positions.push(cmpPosition.GetPosition2D());
- for (let j = 0; j < i; ++j)
- matrix[i][j] = positions[i].distanceToSquared(positions[j]);
+ {
+ formationTemplate = "formations/null";
+ break;
+ }
+ if (!formationTemplate)
+ formationTemplate = cmpUnitAI.GetFormationTemplate();
+ if (formationTemplate == "formations/null")
+ break;
+ else if (cmpUnitAI.GetFormationTemplate() !== formationTemplate)
+ {
+ formationTemplate = "formations/null";
+ break;
+ }
}
- while (clusters.length > 1)
- {
- // search two clusters that are closer than the required distance
- var closeClusters = undefined;
- for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i)
- for (var j = i - 1; j >= 0 && !closeClusters; --j)
- if (matrix[i][j] < distSq)
- closeClusters = [i,j];
+ let nonFormableUnitAIs = [];
+ let formableEntsID = [];
+ let formableEntsAI = [];
- // if no more close clusters found, just return all found clusters so far
- if (!closeClusters)
- return clusters;
+ // don't create a walk together order if this is a queued order because that's just going to be weird
+ let createGroupOrder = queued === false && formationTemplate !== "formations/null";
- // make a new cluster with the entities from the two found clusters
- var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
+ for (let ent of ents)
+ {
+ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ let cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
+ continue;
- // calculate the minimum distance between the new cluster and all other remaining
- // clusters by taking the minimum of the two distances.
- var distances = [];
- for (let i = 0; i < clusters.length; ++i)
- {
- if (i == closeClusters[1] || i == closeClusters[0])
- continue;
- var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]];
- var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]];
- distances.push(Math.min(dist1, dist2));
- }
- // remove the rows and columns in the matrix for the merged clusters,
- // and the clusters themselves from the cluster list
- clusters.splice(closeClusters[0],1);
- clusters.splice(closeClusters[1],1);
- matrix.splice(closeClusters[0],1);
- matrix.splice(closeClusters[1],1);
- for (let i = 0; i < matrix.length; ++i)
+ let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
+ if (createGroupOrder && cmpIdentity)// && cmpIdentity.CanUseFormation(formationTemplate))
{
- if (matrix[i].length > closeClusters[0])
- matrix[i].splice(closeClusters[0],1);
- if (matrix[i].length > closeClusters[1])
- matrix[i].splice(closeClusters[1],1);
+ formableEntsID.push(ent);
+ formableEntsAI.push(cmpUnitAI);
}
- // add a new row of distances to the matrix and the new cluster
- clusters.push(newCluster);
- matrix.push(distances);
+ else
+ nonFormableUnitAIs.push(cmpUnitAI);
+ }
+
+ // TODO: validate formation
+ if (createGroupOrder && formableEntsAI.length > 1)
+ {
+ // TODO: get position, get obstruction, that kind of stuff.
+ let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager);
+ let groupID = cmpGroupWalkManager.CreateGroup(formableEntsID, x, z, range, formationTemplate);
+ formableEntsAI.forEach(cmpUnitAI => { cmpUnitAI.GroupWalk(groupID); });
}
- return clusters;
+ else
+ {
+ nonFormableUnitAIs = nonFormableUnitAIs.concat(formableEntsAI);
+ formableEntsAI = [];
+ }
+ return [nonFormableUnitAIs, formableEntsAI];
}
function GetFormationRequirements(formationTemplate)
@@ -1471,7 +1455,6 @@
return { "minCount": +template.Formation.RequiredMemberCount };
}
-
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
Index: binaries/data/mods/public/simulation/templates/template_formation.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_formation.xml
+++ binaries/data/mods/public/simulation/templates/template_formation.xml
@@ -32,7 +32,6 @@
0.75
- true
2
aggressive
12.0
@@ -40,7 +39,6 @@
true
- true
1.0
large
Index: source/simulation2/components/CCmpPathfinder_Common.h
===================================================================
--- source/simulation2/components/CCmpPathfinder_Common.h
+++ source/simulation2/components/CCmpPathfinder_Common.h
@@ -249,6 +249,23 @@
m_LongPathfinder.ComputePath(x0, z0, goal, passClass, ret);
}
+ virtual std::vector ComputePath_Script(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, const std::string& passClass)
+ {
+ WaypointPath ret;
+ PathGoal goal;
+ goal.type = PathGoal::POINT;
+ goal.x = x1;
+ goal.z = z1;
+ goal.maxdist = fixed::FromInt(50); // TODO: determine good value for this.
+ m_LongPathfinder.ComputePath(x0, z0, goal, GetPassabilityClass(passClass), ret);
+ std::vector output;
+ output.reserve(ret.m_Waypoints.size());
+ for (Waypoint& wpt : ret.m_Waypoints)
+ output.emplace_back(CFixedVector2D(wpt.x, wpt.z));
+ return output;
+ }
+
+
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, entity_id_t notify);
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t clearance, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret);
Index: source/simulation2/components/ICmpPathfinder.h
===================================================================
--- source/simulation2/components/ICmpPathfinder.h
+++ source/simulation2/components/ICmpPathfinder.h
@@ -114,6 +114,11 @@
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret) = 0;
/**
+ * Version for JS components
+ */
+ virtual std::vector ComputePath_Script(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, const std::string& passClass) = 0;
+
+ /**
* Asynchronous version of ComputePath.
* The result will be sent as CMessagePathResult to 'notify'.
* Returns a unique non-zero number, which will match the 'ticket' in the result,
Index: source/simulation2/components/ICmpPathfinder.cpp
===================================================================
--- source/simulation2/components/ICmpPathfinder.cpp
+++ source/simulation2/components/ICmpPathfinder.cpp
@@ -25,4 +25,5 @@
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpPathfinder, SetDebugOverlay, bool)
DEFINE_INTERFACE_METHOD_1("SetHierDebugOverlay", void, ICmpPathfinder, SetHierDebugOverlay, bool)
DEFINE_INTERFACE_METHOD_CONST_1("GetPassabilityClass", pass_class_t, ICmpPathfinder, GetPassabilityClass, std::string)
+DEFINE_INTERFACE_METHOD_5("ComputePath", std::vector, ICmpPathfinder, ComputePath_Script, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, std::string)
END_INTERFACE_WRAPPER(Pathfinder)