Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/binaries/data/mods/public/simulation/components/Formation.js
Show All 11 Lines | "<element name='DisabledTooltip' a:help='Tooltip shown when the formation is disabled.'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='SpeedMultiplier' a:help='The speed of the formation is determined by the minimum speed of all members, multiplied with this number.'>" + | "<element name='SpeedMultiplier' a:help='The speed of the formation is determined by the minimum speed of all members, multiplied with this number.'>" + | ||||
"<ref name='nonNegativeDecimal'/>" + | "<ref name='nonNegativeDecimal'/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" + | "<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='MaxTurningAngle' a:help='The turning angle in radian under which the formation attempts to turn and over which the formation positions are recomputed.'>" + | |||||
"<text/>" + | |||||
"</element>" + | |||||
"<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" + | "<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='SortingClasses' a:help='Classes will be added to the formation in this order. Where the classes will be added first depends on the formation.'>" + | "<element name='SortingClasses' a:help='Classes will be added to the formation in this order. Where the classes will be added first depends on the formation.'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='SortingOrder' a:help='The order of sorting. This defaults to an order where the formation is filled from the first row to the last, and each row from the center to the sides. Other possible sort orders are “fillFromTheSides”, where the most important units are on the sides of each row, and “fillToTheCenter”, where the most vulnerable units are in the center of the formation.'>" + | "<element name='SortingOrder' a:help='The order of sorting. This defaults to an order where the formation is filled from the first row to the last, and each row from the center to the sides. Other possible sort orders are “fillFromTheSides”, where the most important units are on the sides of each row, and “fillToTheCenter”, where the most vulnerable units are in the center of the formation.'>" + | ||||
Show All 34 Lines | Formation.prototype.Schema = | ||||
"</element>" + | "</element>" + | ||||
"<element name='AnimationVariants' a:help='Give a list of animation variants to use for the particular formation members, based on their positions.'>" + | "<element name='AnimationVariants' a:help='Give a list of animation variants to use for the particular formation members, based on their positions.'>" + | ||||
"<text a:help='example text: “1..1,1..-1:animationVariant1;2..2,1..-1;animationVariant2”, this will set animationVariant1 for the first row, and animation2 for the second row. The first part of the numbers (1..1 and 2..2) means the row range. Every row between (and including) those values will switch animationvariants. The second part of the numbers (1..-1) denote the columns inside those rows that will be affected. Note that in both cases, you can use -1 for the last row/column, -2 for the second to last, etc.'/>" + | "<text a:help='example text: “1..1,1..-1:animationVariant1;2..2,1..-1;animationVariant2”, this will set animationVariant1 for the first row, and animation2 for the second row. The first part of the numbers (1..1 and 2..2) means the row range. Every row between (and including) those values will switch animationvariants. The second part of the numbers (1..-1) denote the columns inside those rows that will be affected. Note that in both cases, you can use -1 for the last row/column, -2 for the second to last, etc.'/>" + | ||||
"</element>"; | "</element>"; | ||||
// Distance at which we'll switch between column/box formations. | // Distance at which we'll switch between column/box formations. | ||||
var g_ColumnDistanceThreshold = 128; | var g_ColumnDistanceThreshold = 128; | ||||
// Distance under which the formation will not try to turn towards the target position. | |||||
var g_RotateDistanceThreshold = 1; | |||||
Formation.prototype.variablesToSerialize = [ | Formation.prototype.variablesToSerialize = [ | ||||
"lastOrderVariant", | "lastOrderVariant", | ||||
"members", | "members", | ||||
"memberPositions", | "memberPositions", | ||||
"maxRowsUsed", | "maxRowsUsed", | ||||
"maxColumnsUsed", | "maxColumnsUsed", | ||||
"finishedEntities", | "finishedEntities", | ||||
"idleEntities", | "idleEntities", | ||||
"columnar", | "columnar", | ||||
"rearrange", | "rearrange", | ||||
"formationMembersWithAura", | "formationMembersWithAura", | ||||
"width", | "width", | ||||
"depth", | "depth", | ||||
"twinFormations", | "twinFormations", | ||||
"formationSeparation", | "formationSeparation", | ||||
"offsets" | "offsets" | ||||
]; | ]; | ||||
Formation.prototype.Init = function(deserialized = false) | Formation.prototype.Init = function(deserialized = false) | ||||
{ | { | ||||
this.maxTurningAngle = +this.template.MaxTurningAngle; | |||||
this.sortingClasses = this.template.SortingClasses.split(/\s+/g); | this.sortingClasses = this.template.SortingClasses.split(/\s+/g); | ||||
this.shiftRows = this.template.ShiftRows == "true"; | this.shiftRows = this.template.ShiftRows == "true"; | ||||
this.separationMultiplier = { | this.separationMultiplier = { | ||||
"width": +this.template.UnitSeparationWidthMultiplier, | "width": +this.template.UnitSeparationWidthMultiplier, | ||||
"depth": +this.template.UnitSeparationDepthMultiplier | "depth": +this.template.UnitSeparationDepthMultiplier | ||||
}; | }; | ||||
this.sloppiness = +this.template.Sloppiness; | this.sloppiness = +this.template.Sloppiness; | ||||
this.widthDepthRatio = +this.template.WidthDepthRatio; | this.widthDepthRatio = +this.template.WidthDepthRatio; | ||||
▲ Show 20 Lines • Show All 387 Lines • ▼ Show 20 Lines | |||||
*/ | */ | ||||
Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant) | Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant) | ||||
{ | { | ||||
if (!this.members.length) | if (!this.members.length) | ||||
return; | return; | ||||
let active = []; | let active = []; | ||||
let positions = []; | let positions = []; | ||||
let rotations = 0; | |||||
for (let ent of this.members) | for (let ent of this.members) | ||||
{ | { | ||||
let cmpPosition = Engine.QueryInterface(ent, IID_Position); | let cmpPosition = Engine.QueryInterface(ent, IID_Position); | ||||
if (!cmpPosition || !cmpPosition.IsInWorld()) | if (!cmpPosition || !cmpPosition.IsInWorld()) | ||||
continue; | continue; | ||||
active.push(ent); | active.push(ent); | ||||
// Query the 2D position as the exact height calculation isn't needed, | // Query the 2D position as the exact height calculation isn't needed, | ||||
// but bring the position to the correct coordinates. | // but bring the position to the correct coordinates. | ||||
positions.push(cmpPosition.GetPosition2D()); | positions.push(cmpPosition.GetPosition2D()); | ||||
rotations += cmpPosition.GetRotation().y; | |||||
} | } | ||||
let avgpos = Vector2D.average(positions); | |||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | ||||
// Reposition the formation if we're told to or if we don't already have a position. | // Reposition the formation if we're told to or if we don't already have a position. | ||||
if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld())) | if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld())) | ||||
this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / active.length); | { | ||||
const oldRotation = cmpPosition.GetRotation().y; | |||||
const avgpos = Vector2D.average(positions); | |||||
this.lastOrderVariant = variant; | |||||
// Switch between column and box if necessary. | // Switch between column and box if necessary. | ||||
let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); | const cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); | ||||
let walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance(); | const columnar = cmpFormationUnitAI.ComputeWalkingDistance() > g_ColumnDistanceThreshold; | ||||
let columnar = walkingDistance > g_ColumnDistanceThreshold; | |||||
if (columnar != this.columnar) | if (columnar != this.columnar) | ||||
{ | { | ||||
this.columnar = columnar; | this.columnar = columnar; | ||||
this.offsets = undefined; | this.offsets = undefined; | ||||
} | } | ||||
let newRotation = oldRotation; | |||||
const targetPosition = cmpFormationUnitAI.GetTargetPositions()[0]; | |||||
if (targetPosition !== undefined && avgpos.distanceToSquared(targetPosition) > g_RotateDistanceThreshold) | |||||
newRotation = avgpos.angleTo(targetPosition); | |||||
cmpPosition.TurnTo(newRotation); | |||||
if (!this.areAnglesSimilar(newRotation, oldRotation)) | |||||
this.offsets = undefined; | |||||
} | |||||
this.lastOrderVariant = variant; | |||||
let offsetsChanged = false; | let offsetsChanged = false; | ||||
let newOrientation = this.GetEstimatedOrientation(avgpos); | |||||
if (!this.offsets) | if (!this.offsets) | ||||
{ | { | ||||
this.offsets = this.ComputeFormationOffsets(active, positions); | this.offsets = this.ComputeFormationOffsets(active, positions); | ||||
offsetsChanged = true; | offsetsChanged = true; | ||||
} | } | ||||
let xMax = 0; | let xMax = 0; | ||||
let yMax = 0; | let yMax = 0; | ||||
▲ Show 20 Lines • Show All 260 Lines • ▼ Show 20 Lines | Formation.prototype.ComputeFormationOffsets = function(active, positions) | ||||
if (sortingOrder == "fillFromTheSides") | if (sortingOrder == "fillFromTheSides") | ||||
offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);}); | offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);}); | ||||
else if (sortingOrder == "fillToTheCenter") | else if (sortingOrder == "fillToTheCenter") | ||||
offsets.sort(function(o1, o2) { | offsets.sort(function(o1, o2) { | ||||
return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y)); | return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y)); | ||||
}); | }); | ||||
// Query the 2D position of the formation. | // Query the 2D position of the formation. | ||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | const realPositions = this.GetRealOffsetPositions(offsets); | ||||
let formationPos = cmpPosition.GetPosition2D(); | |||||
// Use realistic place assignment, | // Use realistic place assignment, | ||||
// every soldier searches the closest available place in the formation. | // every soldier searches the closest available place in the formation. | ||||
let newOffsets = []; | let newOffsets = []; | ||||
let realPositions = this.GetRealOffsetPositions(offsets, formationPos); | for (const i of sortingClasses.reverse()) | ||||
for (let i = sortingClasses.length; i; --i) | |||||
{ | { | ||||
let t = types[sortingClasses[i - 1]]; | const t = types[i]; | ||||
if (!t.length) | if (!t.length) | ||||
continue; | continue; | ||||
let usedOffsets = offsets.splice(-t.length); | let usedOffsets = offsets.splice(-t.length); | ||||
let usedRealPositions = realPositions.splice(-t.length); | let usedRealPositions = realPositions.splice(-t.length); | ||||
for (let entPos of t) | for (let entPos of t) | ||||
{ | { | ||||
let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets); | let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets); | ||||
usedRealPositions.splice(closestOffsetId, 1); | usedRealPositions.splice(closestOffsetId, 1); | ||||
Show All 28 Lines | Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets) | ||||
} | } | ||||
this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column }; | this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column }; | ||||
return closestOffsetId; | return closestOffsetId; | ||||
}; | }; | ||||
/** | /** | ||||
* Get the world positions for a list of offsets in this formation. | * Get the world positions for a list of offsets in this formation. | ||||
*/ | */ | ||||
Formation.prototype.GetRealOffsetPositions = function(offsets, pos) | Formation.prototype.GetRealOffsetPositions = function(offsets) | ||||
{ | { | ||||
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | |||||
const pos = cmpPosition.GetPosition2D(); | |||||
const rot = cmpPosition.GetRotation().y; | |||||
const sin = Math.sin(rot); | |||||
const cos = Math.cos(rot); | |||||
let offsetPositions = []; | let offsetPositions = []; | ||||
let { sin, cos } = this.GetEstimatedOrientation(pos); | |||||
// Calculate the world positions. | // Calculate the world positions. | ||||
for (let o of offsets) | for (let o of offsets) | ||||
offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin)); | offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin)); | ||||
return offsetPositions; | return offsetPositions; | ||||
}; | }; | ||||
/** | /** | ||||
* Calculate the estimated rotation of the formation based on the current rotation. | * Returns true if the two given angles (in radians) | ||||
* Return the sine and cosine of the angle. | * are smaller than the maximum turning angle of the formation and therfore allow | ||||
* the formation turn without reassigning positions. | |||||
*/ | */ | ||||
Formation.prototype.GetEstimatedOrientation = function(pos) | |||||
Formation.prototype.areAnglesSimilar = function(a1, a2) | |||||
{ | { | ||||
let r = {}; | const d = Math.abs(a1 - a2) % 2 * Math.PI; | ||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | return d < this.maxTurningAngle || d > 2 * Math.PI - this.maxTurningAngle; | ||||
if (!cmpPosition) | |||||
return r; | |||||
let rot = cmpPosition.GetRotation().y; | |||||
r.sin = Math.sin(rot); | |||||
r.cos = Math.cos(rot); | |||||
return r; | |||||
}; | }; | ||||
/** | /** | ||||
* Set formation controller's speed based on its current members. | * Set formation controller's speed based on its current members. | ||||
*/ | */ | ||||
Formation.prototype.ComputeMotionParameters = function() | Formation.prototype.ComputeMotionParameters = function() | ||||
{ | { | ||||
let maxRadius = 0; | |||||
let minSpeed = Infinity; | let minSpeed = Infinity; | ||||
let minAcceleration = Infinity; | let minAcceleration = Infinity; | ||||
for (let ent of this.members) | for (let ent of this.members) | ||||
{ | { | ||||
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); | let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); | ||||
if (cmpUnitMotion) | if (cmpUnitMotion) | ||||
{ | { | ||||
▲ Show 20 Lines • Show All 138 Lines • Show Last 20 Lines |
Wildfire Games · Phabricator