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)