Line data Source code
1 : // (A serious implementation of this might want to use C++ instead of JS
2 : // for performance; this is just for fun.)
3 1 : const SHORT_FINAL = 2.5;
4 : function UnitMotionFlying() {}
5 :
6 1 : UnitMotionFlying.prototype.Schema =
7 : "<element name='MaxSpeed'>" +
8 : "<ref name='nonNegativeDecimal'/>" +
9 : "</element>" +
10 : "<element name='TakeoffSpeed'>" +
11 : "<ref name='nonNegativeDecimal'/>" +
12 : "</element>" +
13 : "<optional>" +
14 : "<element name='StationaryDistance' a:help='Allows the object to be stationary when reaching a target. Value defines the maximum distance at which a target is considered reached.'>" +
15 : "<ref name='positiveDecimal'/>" +
16 : "</element>" +
17 : "</optional>" +
18 : "<element name='LandingSpeed'>" +
19 : "<ref name='nonNegativeDecimal'/>" +
20 : "</element>" +
21 : "<element name='AccelRate'>" +
22 : "<ref name='nonNegativeDecimal'/>" +
23 : "</element>" +
24 : "<element name='SlowingRate'>" +
25 : "<ref name='nonNegativeDecimal'/>" +
26 : "</element>" +
27 : "<element name='BrakingRate'>" +
28 : "<ref name='nonNegativeDecimal'/>" +
29 : "</element>" +
30 : "<element name='TurnRate'>" +
31 : "<ref name='nonNegativeDecimal'/>" +
32 : "</element>" +
33 : "<element name='OvershootTime'>" +
34 : "<ref name='nonNegativeDecimal'/>" +
35 : "</element>" +
36 : "<element name='FlyingHeight'>" +
37 : "<data type='decimal'/>" +
38 : "</element>" +
39 : "<element name='ClimbRate'>" +
40 : "<ref name='nonNegativeDecimal'/>" +
41 : "</element>" +
42 : "<element name='DiesInWater'>" +
43 : "<data type='boolean'/>" +
44 : "</element>" +
45 : "<element name='PassabilityClass'>" +
46 : "<text/>" +
47 : "</element>";
48 :
49 1 : UnitMotionFlying.prototype.Init = function()
50 : {
51 1 : this.hasTarget = false;
52 1 : this.reachedTarget = false;
53 1 : this.targetX = 0;
54 1 : this.targetZ = 0;
55 1 : this.targetMinRange = 0;
56 1 : this.targetMaxRange = 0;
57 1 : this.speed = 0;
58 1 : this.landing = false;
59 1 : this.onGround = true;
60 1 : this.pitch = 0;
61 1 : this.roll = 0;
62 1 : this.waterDeath = false;
63 1 : this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass);
64 : };
65 :
66 1 : UnitMotionFlying.prototype.OnUpdate = function(msg)
67 : {
68 17 : let turnLength = msg.turnLength;
69 17 : if (!this.hasTarget)
70 3 : return;
71 14 : let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
72 14 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
73 14 : let pos = cmpPosition.GetPosition();
74 14 : let angle = cmpPosition.GetRotation().y;
75 14 : let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
76 14 : let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
77 14 : let ground = Math.max(cmpTerrain.GetGroundLevel(pos.x, pos.z), cmpWaterManager.GetWaterLevel(pos.x, pos.z));
78 14 : let newangle = angle;
79 14 : let canTurn = true;
80 14 : let distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
81 14 : if (this.landing)
82 : {
83 6 : if (this.speed > 0 && this.onGround)
84 : {
85 3 : if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater == "true")
86 0 : this.waterDeath = true;
87 3 : this.pitch = 0;
88 : // Deaccelerate forwards...at a very reduced pace.
89 3 : if (this.waterDeath)
90 0 : this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate * 10);
91 : else
92 3 : this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate);
93 3 : canTurn = false;
94 : // Clamp to ground if below it, or descend if above.
95 3 : if (pos.y < ground)
96 0 : pos.y = ground;
97 3 : else if (pos.y > ground)
98 0 : pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
99 : }
100 3 : else if (this.speed == 0 && this.onGround)
101 : {
102 1 : let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
103 1 : if (this.waterDeath && cmpHealth)
104 0 : cmpHealth.Kill();
105 : else
106 : {
107 1 : this.pitch = 0;
108 : // We've stopped.
109 1 : if (cmpGarrisonHolder)
110 1 : cmpGarrisonHolder.AllowGarrisoning(true, "UnitMotionFlying");
111 1 : canTurn = false;
112 1 : this.hasTarget = false;
113 1 : this.landing = false;
114 : // Summon planes back from the edge of the map.
115 1 : let terrainSize = cmpTerrain.GetMapSize();
116 1 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
117 1 : if (cmpRangeManager.GetLosCircular())
118 : {
119 1 : let mapRadius = terrainSize/2;
120 1 : let x = pos.x - mapRadius;
121 1 : let z = pos.z - mapRadius;
122 1 : let div = (mapRadius - 12) / Math.sqrt(x*x + z*z);
123 1 : if (div < 1)
124 : {
125 1 : pos.x = mapRadius + x*div;
126 1 : pos.z = mapRadius + z*div;
127 1 : newangle += Math.PI;
128 1 : distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
129 : }
130 : }
131 : else
132 : {
133 0 : pos.x = Math.max(Math.min(pos.x, terrainSize - 12), 12);
134 0 : pos.z = Math.max(Math.min(pos.z, terrainSize - 12), 12);
135 0 : newangle += Math.PI;
136 0 : distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
137 : }
138 : }
139 : }
140 : else
141 : {
142 : // Final Approach.
143 : // We need to slow down to land!
144 2 : this.speed = Math.max(this.template.LandingSpeed, this.speed - turnLength * this.template.SlowingRate);
145 2 : canTurn = false;
146 2 : let targetHeight = ground;
147 : // Steep, then gradual descent.
148 2 : if ((pos.y - targetHeight) / this.template.FlyingHeight > 1 / SHORT_FINAL)
149 2 : this.pitch = -Math.PI / 18;
150 : else
151 0 : this.pitch = Math.PI / 18;
152 2 : let descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + SHORT_FINAL) * SHORT_FINAL;
153 2 : if (pos.y < targetHeight)
154 0 : pos.y = Math.max(targetHeight, pos.y + turnLength * descentRate);
155 2 : else if (pos.y > targetHeight)
156 2 : pos.y = Math.max(targetHeight, pos.y - turnLength * descentRate);
157 2 : if (targetHeight == pos.y)
158 : {
159 1 : this.onGround = true;
160 1 : if (targetHeight == cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater)
161 0 : this.waterDeath = true;
162 : }
163 : }
164 : }
165 : else
166 : {
167 8 : if (this.template.StationaryDistance && distanceToTargetSquared <= +this.template.StationaryDistance * +this.template.StationaryDistance)
168 : {
169 0 : cmpPosition.SetXZRotation(0, 0);
170 0 : this.pitch = 0;
171 0 : this.roll = 0;
172 0 : this.reachedTarget = true;
173 0 : cmpPosition.TurnTo(Math.atan2(this.targetX - pos.x, this.targetZ - pos.z));
174 0 : Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" });
175 0 : return;
176 : }
177 : // If we haven't reached max speed yet then we're still on the ground;
178 : // otherwise we're taking off or flying.
179 : // this.onGround in case of a go-around after landing (but not fully stopped).
180 :
181 8 : if (this.speed < this.template.TakeoffSpeed && this.onGround)
182 : {
183 2 : if (cmpGarrisonHolder)
184 2 : cmpGarrisonHolder.AllowGarrisoning(false, "UnitMotionFlying");
185 2 : this.pitch = 0;
186 : // Accelerate forwards.
187 2 : this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
188 2 : canTurn = false;
189 : // Clamp to ground if below it, or descend if above.
190 2 : if (pos.y < ground)
191 0 : pos.y = ground;
192 2 : else if (pos.y > ground)
193 0 : pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
194 : }
195 : else
196 : {
197 6 : this.onGround = false;
198 : // Climb/sink to max height above ground.
199 6 : this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
200 6 : let targetHeight = ground + (+this.template.FlyingHeight);
201 6 : if (Math.abs(pos.y-targetHeight) > this.template.FlyingHeight/5)
202 : {
203 3 : this.pitch = Math.PI / 9;
204 3 : canTurn = false;
205 : }
206 : else
207 3 : this.pitch = 0;
208 6 : if (pos.y < targetHeight)
209 3 : pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate);
210 3 : else if (pos.y > targetHeight)
211 : {
212 0 : pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate);
213 0 : this.pitch = -1 * this.pitch;
214 : }
215 : }
216 : }
217 :
218 : // If we're in range of the target then tell people that we've reached it.
219 : // (TODO: quantisation breaks this)
220 14 : if (!this.reachedTarget &&
221 : this.targetMinRange * this.targetMinRange <= distanceToTargetSquared &&
222 : distanceToTargetSquared <= this.targetMaxRange * this.targetMaxRange)
223 : {
224 0 : this.reachedTarget = true;
225 0 : Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" });
226 : }
227 :
228 : // If we're facing away from the target, and are still fairly close to it,
229 : // then carry on going straight so we overshoot in a straight line.
230 14 : let isBehindTarget = ((this.targetX - pos.x) * Math.sin(angle) + (this.targetZ - pos.z) * Math.cos(angle) < 0);
231 : // Overshoot the target: carry on straight.
232 14 : if (isBehindTarget && distanceToTargetSquared < this.template.MaxSpeed * this.template.MaxSpeed * this.template.OvershootTime * this.template.OvershootTime)
233 0 : canTurn = false;
234 :
235 14 : if (canTurn)
236 : {
237 : // Turn towards the target.
238 3 : let targetAngle = Math.atan2(this.targetX - pos.x, this.targetZ - pos.z);
239 3 : let delta = targetAngle - angle;
240 : // Wrap delta to -pi..pi.
241 3 : delta = (delta + Math.PI) % (2*Math.PI);
242 3 : if (delta < 0)
243 0 : delta += 2 * Math.PI;
244 3 : delta -= Math.PI;
245 : // Clamp to max rate.
246 3 : let deltaClamped = Math.min(Math.max(delta, -this.template.TurnRate * turnLength), this.template.TurnRate * turnLength);
247 : // Calculate new orientation, in a peculiar way in order to make sure the
248 : // result gets close to targetAngle (rather than being n*2*pi out).
249 3 : newangle = targetAngle + deltaClamped - delta;
250 3 : if (newangle - angle > Math.PI / 18)
251 0 : this.roll = Math.PI / 9;
252 3 : else if (newangle - angle < -Math.PI / 18)
253 2 : this.roll = -Math.PI / 9;
254 : else
255 1 : this.roll = newangle - angle;
256 : }
257 : else
258 11 : this.roll = 0;
259 :
260 14 : pos.x += this.speed * turnLength * Math.sin(angle);
261 14 : pos.z += this.speed * turnLength * Math.cos(angle);
262 14 : cmpPosition.SetHeightFixed(pos.y);
263 14 : cmpPosition.TurnTo(newangle);
264 14 : cmpPosition.SetXZRotation(this.pitch, this.roll);
265 14 : cmpPosition.MoveTo(pos.x, pos.z);
266 : };
267 :
268 1 : UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
269 : {
270 1 : this.hasTarget = true;
271 1 : this.landing = false;
272 1 : this.reachedTarget = false;
273 1 : this.targetX = x;
274 1 : this.targetZ = z;
275 1 : this.targetMinRange = minRange;
276 1 : this.targetMaxRange = maxRange;
277 :
278 1 : return true;
279 : };
280 :
281 1 : UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
282 : {
283 1 : let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
284 1 : if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
285 0 : return false;
286 :
287 1 : let targetPos = cmpTargetPosition.GetPosition2D();
288 :
289 1 : this.hasTarget = true;
290 1 : this.reachedTarget = false;
291 1 : this.targetX = targetPos.x;
292 1 : this.targetZ = targetPos.y;
293 1 : this.targetMinRange = minRange;
294 1 : this.targetMaxRange = maxRange;
295 :
296 1 : return true;
297 : };
298 :
299 1 : UnitMotionFlying.prototype.SetMemberOfFormation = function()
300 : {
301 : // Ignored.
302 : };
303 :
304 1 : UnitMotionFlying.prototype.GetWalkSpeed = function()
305 : {
306 0 : return +this.template.MaxSpeed;
307 : };
308 :
309 1 : UnitMotionFlying.prototype.SetSpeedMultiplier = function(multiplier)
310 : {
311 : // Ignore this, the speed is always the walk speed.
312 : };
313 :
314 1 : UnitMotionFlying.prototype.GetRunMultiplier = function()
315 : {
316 2 : return 1;
317 : };
318 :
319 : /**
320 : * Estimate the next position of the unit. Just linearly extrapolate.
321 : * TODO: Reuse the movement code for a better estimate.
322 : */
323 1 : UnitMotionFlying.prototype.EstimateFuturePosition = function(dt)
324 : {
325 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
326 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
327 0 : return Vector2D();
328 0 : let position = cmpPosition.GetPosition2D();
329 :
330 0 : return Vector2D.add(position, Vector2D.sub(position, cmpPosition.GetPreviousPosition2D()).mult(dt/Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetLatestTurnLength()));
331 : };
332 :
333 1 : UnitMotionFlying.prototype.IsMoveRequested = function()
334 : {
335 0 : return this.hasTarget;
336 : };
337 :
338 1 : UnitMotionFlying.prototype.GetCurrentSpeed = function()
339 : {
340 20 : return this.speed;
341 : };
342 :
343 1 : UnitMotionFlying.prototype.GetSpeedMultiplier = function()
344 : {
345 4 : return this.speed / +this.template.MaxSpeed;
346 : };
347 :
348 1 : UnitMotionFlying.prototype.GetAcceleration = function()
349 : {
350 0 : return +this.template.AccelRate;
351 : };
352 :
353 1 : UnitMotionFlying.prototype.SetAcceleration = function()
354 : {
355 : // Acceleration is set by the template. Ignore.
356 : };
357 :
358 1 : UnitMotionFlying.prototype.GetPassabilityClassName = function()
359 : {
360 3 : return this.passabilityClassName ? this.passabilityClassName : this.template.PassabilityClass;
361 : };
362 :
363 1 : UnitMotionFlying.prototype.SetPassabilityClassName = function(passClassName)
364 : {
365 1 : this.passabilityClassName = passClassName;
366 1 : const cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
367 1 : if (cmpPathfinder)
368 1 : this.passabilityClass = cmpPathfinder.GetPassabilityClass(passClassName);
369 : };
370 :
371 1 : UnitMotionFlying.prototype.GetPassabilityClass = function()
372 : {
373 1 : return this.passabilityClass;
374 : };
375 :
376 1 : UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z)
377 : {
378 : // Ignore this - angle is controlled by the target-seeking code instead.
379 : };
380 :
381 1 : UnitMotionFlying.prototype.SetFacePointAfterMove = function()
382 : {
383 : // Ignore this - angle is controlled by the target-seeking code instead.
384 : };
385 :
386 1 : UnitMotionFlying.prototype.StopMoving = function()
387 : {
388 : // Invert.
389 1 : if (!this.waterDeath)
390 1 : this.landing = !this.landing;
391 :
392 : };
393 :
394 1 : UnitMotionFlying.prototype.SetDebugOverlay = function(enabled)
395 : {
396 : };
397 :
398 1 : Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying);
|