1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2
3 #include "System/creg/STL_Map.h"
4 #include "WeaponDefHandler.h"
5 #include "Weapon.h"
6 #include "Game/GameHelper.h"
7 #include "Game/TraceRay.h"
8 #include "Game/Players/Player.h"
9 #include "Map/Ground.h"
10 #include "Sim/Misc/CollisionHandler.h"
11 #include "Sim/Misc/CollisionVolume.h"
12 #include "Sim/Misc/GeometricObjects.h"
13 #include "Sim/Misc/InterceptHandler.h"
14 #include "Sim/Misc/LosHandler.h"
15 #include "Sim/Misc/ModInfo.h"
16 #include "Sim/Misc/TeamHandler.h"
17 #include "Sim/MoveTypes/AAirMoveType.h"
18 #include "Sim/Projectiles/WeaponProjectiles/WeaponProjectile.h"
19 #include "Sim/Units/Scripts/CobInstance.h"
20 #include "Sim/Units/CommandAI/CommandAI.h"
21 #include "Sim/Units/Unit.h"
22 #include "System/EventHandler.h"
23 #include "System/float3.h"
24 #include "System/myMath.h"
25 #include "System/Sync/SyncTracer.h"
26 #include "System/Sound/ISoundChannels.h"
27 #include "System/Log/ILog.h"
28
29 CR_BIND_DERIVED(CWeapon, CObject, (NULL, NULL))
30
31 CR_REG_METADATA(CWeapon, (
32 CR_MEMBER(owner),
33 CR_MEMBER(range),
34 CR_MEMBER(heightMod),
35 CR_MEMBER(reloadTime),
36 CR_MEMBER(reloadStatus),
37 CR_MEMBER(salvoLeft),
38 CR_MEMBER(salvoDelay),
39 CR_MEMBER(salvoSize),
40 CR_MEMBER(projectilesPerShot),
41 CR_MEMBER(nextSalvo),
42 CR_MEMBER(predict),
43 CR_MEMBER(targetUnit),
44 CR_MEMBER(accuracyError),
45 CR_MEMBER(projectileSpeed),
46 CR_MEMBER(predictSpeedMod),
47 CR_MEMBER(metalFireCost),
48 CR_MEMBER(energyFireCost),
49 CR_MEMBER(fireSoundId),
50 CR_MEMBER(fireSoundVolume),
51 CR_MEMBER(hasBlockShot),
52 CR_MEMBER(hasTargetWeight),
53 CR_MEMBER(angleGood),
54 CR_MEMBER(avoidTarget),
55 CR_MEMBER(haveUserTarget),
56 CR_MEMBER(onlyForward),
57 CR_MEMBER(muzzleFlareSize),
58 CR_MEMBER(craterAreaOfEffect),
59 CR_MEMBER(damageAreaOfEffect),
60
61 CR_MEMBER(badTargetCategory),
62 CR_MEMBER(onlyTargetCategory),
63 CR_MEMBER(incomingProjectiles),
64 CR_MEMBER(weaponDef),
65 CR_MEMBER(stockpileTime),
66 CR_MEMBER(buildPercent),
67 CR_MEMBER(numStockpiled),
68 CR_MEMBER(numStockpileQued),
69 CR_MEMBER(interceptTarget),
70 CR_ENUM_MEMBER(targetType),
71 CR_MEMBER(sprayAngle),
72 CR_MEMBER(useWeaponPosForAim),
73
74 CR_MEMBER(lastRequest),
75 CR_MEMBER(lastTargetRetry),
76 CR_MEMBER(lastErrorVectorUpdate),
77
78 CR_MEMBER(slavedTo),
79 CR_MEMBER(maxForwardAngleDif),
80 CR_MEMBER(maxMainDirAngleDif),
81 CR_MEMBER(hasCloseTarget),
82 CR_MEMBER(targetBorder),
83 CR_MEMBER(cylinderTargeting),
84 CR_MEMBER(minIntensity),
85 CR_MEMBER(heightBoostFactor),
86 CR_MEMBER(avoidFlags),
87 CR_MEMBER(collisionFlags),
88 CR_MEMBER(fuelUsage),
89 CR_MEMBER(weaponNum),
90
91 CR_MEMBER(relWeaponPos),
92 CR_MEMBER(weaponPos),
93 CR_MEMBER(relWeaponMuzzlePos),
94 CR_MEMBER(weaponMuzzlePos),
95 CR_MEMBER(weaponDir),
96 CR_MEMBER(mainDir),
97 CR_MEMBER(wantedDir),
98 CR_MEMBER(lastRequestedDir),
99 CR_MEMBER(salvoError),
100 CR_MEMBER(errorVector),
101 CR_MEMBER(errorVectorAdd),
102 CR_MEMBER(targetPos),
103 CR_MEMBER(targetBorderPos)
104 ))
105
106 //////////////////////////////////////////////////////////////////////
107 // Construction/Destruction
108 //////////////////////////////////////////////////////////////////////
109
CWeapon(CUnit * owner,const WeaponDef * def)110 CWeapon::CWeapon(CUnit* owner, const WeaponDef* def):
111 owner(owner),
112 weaponDef(def),
113 weaponNum(-1),
114 haveUserTarget(false),
115 craterAreaOfEffect(1.0f),
116 damageAreaOfEffect(1.0f),
117 muzzleFlareSize(1),
118 useWeaponPosForAim(0),
119 hasCloseTarget(false),
120 reloadTime(1),
121 reloadStatus(0),
122 range(1),
123 heightMod(0),
124 projectileSpeed(1),
125 accuracyError(0),
126 sprayAngle(0),
127 salvoDelay(0),
128 salvoSize(1),
129 projectilesPerShot(1),
130 nextSalvo(0),
131 salvoLeft(0),
132 targetType(Target_None),
133 targetUnit(0),
134 predict(0),
135 predictSpeedMod(1),
136 metalFireCost(0),
137 energyFireCost(0),
138 fireSoundId(0),
139 fireSoundVolume(0),
140 hasBlockShot(false),
141 hasTargetWeight(false),
142 angleGood(false),
143 avoidTarget(false),
144 onlyForward(false),
145 badTargetCategory(0),
146 onlyTargetCategory(0xffffffff),
147
148 interceptTarget(NULL),
149 stockpileTime(1),
150 buildPercent(0),
151 numStockpiled(0),
152 numStockpileQued(0),
153
154 lastRequest(0),
155 lastTargetRetry(-100),
156 lastErrorVectorUpdate(0),
157
158 slavedTo(NULL),
159 maxForwardAngleDif(0.0f),
160 maxMainDirAngleDif(-1.0f),
161 targetBorder(0.f),
162 cylinderTargeting(0.f),
163 minIntensity(0.f),
164 heightBoostFactor(-1.f),
165 avoidFlags(0),
166 collisionFlags(0),
167 fuelUsage(0),
168
169 relWeaponPos(UpVector),
170 weaponPos(ZeroVector),
171 relWeaponMuzzlePos(UpVector),
172 weaponMuzzlePos(ZeroVector),
173 weaponDir(ZeroVector),
174 mainDir(FwdVector),
175 wantedDir(UpVector),
176 lastRequestedDir(-UpVector),
177 salvoError(ZeroVector),
178 errorVector(ZeroVector),
179 errorVectorAdd(ZeroVector),
180 targetPos(OnesVector)
181 {
182 }
183
184
~CWeapon()185 CWeapon::~CWeapon()
186 {
187 if (weaponDef->interceptor)
188 interceptHandler.RemoveInterceptorWeapon(this);
189 }
190
191
SetWeaponNum(int num)192 void CWeapon::SetWeaponNum(int num)
193 {
194 weaponNum = num;
195
196 hasBlockShot = owner->script->HasBlockShot(weaponNum);
197 hasTargetWeight = owner->script->HasTargetWeight(weaponNum);
198 }
199
200
CobBlockShot(const CUnit * targetUnit)201 inline bool CWeapon::CobBlockShot(const CUnit* targetUnit)
202 {
203 if (!hasBlockShot) {
204 return false;
205 }
206
207 return owner->script->BlockShot(weaponNum, targetUnit, haveUserTarget);
208 }
209
210
TargetWeight(const CUnit * targetUnit) const211 float CWeapon::TargetWeight(const CUnit* targetUnit) const
212 {
213 return owner->script->TargetWeight(weaponNum, targetUnit);
214 }
215
216
217
218 // NOTE:
219 // GUIHandler places (some) user ground-attack orders on the
220 // water surface, others on the ocean floor and in both cases
221 // without examining weapon abilities (its logic is "obtuse")
222 //
223 // this inconsistency would be hard(er) to fix on the UI side
224 // so we must adjust all such target positions in synced code
225 //
226 // see also CommandAI::AdjustGroundAttackCommand
AdjustTargetPosToWater(float3 & tgtPos,bool attackGround) const227 void CWeapon::AdjustTargetPosToWater(float3& tgtPos, bool attackGround) const
228 {
229 if (!attackGround)
230 return;
231
232 if (weaponDef->waterweapon) {
233 tgtPos.y = std::max(tgtPos.y, CGround::GetHeightReal(tgtPos.x, tgtPos.z));
234 } else {
235 tgtPos.y = std::max(tgtPos.y, CGround::GetHeightAboveWater(tgtPos.x, tgtPos.z));
236 }
237 }
238
UpdateRelWeaponPos()239 void CWeapon::UpdateRelWeaponPos()
240 {
241 // If we can't get a line of fire from the muzzle, try
242 // the aim piece instead (since the weapon may just be
243 // turned in a wrong way)
244 int weaponPiece = -1;
245 bool weaponAimed = (useWeaponPosForAim == 0);
246
247 if (!weaponAimed) {
248 weaponPiece = owner->script->QueryWeapon(weaponNum);
249 } else {
250 weaponPiece = owner->script->AimFromWeapon(weaponNum);
251 }
252
253 relWeaponMuzzlePos = owner->script->GetPiecePos(weaponPiece);
254
255 if (!weaponAimed) {
256 weaponPiece = owner->script->AimFromWeapon(weaponNum);
257 }
258
259 relWeaponPos = owner->script->GetPiecePos(weaponPiece);
260 }
261
Update()262 void CWeapon::Update()
263 {
264 if (!UpdateStockpile())
265 return;
266
267 UpdateTargeting();
268 UpdateFire();
269 UpdateSalvo();
270
271 #ifdef TRACE_SYNC
272 tracefile << __FUNCTION__;
273 tracefile << weaponPos.x << " " << weaponPos.y << " " << weaponPos.z << " " << targetPos.x << " " << targetPos.y << " " << targetPos.z << "\n";
274 #endif
275 }
276
277
UpdateTargeting()278 void CWeapon::UpdateTargeting()
279 {
280 if (hasCloseTarget) {
281 UpdateRelWeaponPos();
282 }
283
284 if (targetType == Target_Unit) {
285 if (lastErrorVectorUpdate < gs->frameNum - UNIT_SLOWUPDATE_RATE) {
286 errorVectorAdd = (gs->randVector() - errorVector) * (1.0f / UNIT_SLOWUPDATE_RATE);
287 lastErrorVectorUpdate = gs->frameNum;
288 }
289
290 // to prevent runaway prediction (happens sometimes when a missile
291 // is moving *away* from its target), we may need to disable missiles
292 // in case they fly around too long
293 predict = std::min(predict, 50000.0f);
294 errorVector += errorVectorAdd;
295
296 float3 lead = targetUnit->speed * (weaponDef->predictBoost + predictSpeedMod * (1.0f - weaponDef->predictBoost)) * predict;
297
298 if (weaponDef->leadLimit >= 0.0f && lead.SqLength() > Square(weaponDef->leadLimit + weaponDef->leadBonus * owner->experience)) {
299 lead *= (weaponDef->leadLimit + weaponDef->leadBonus*owner->experience) / (lead.Length() + 0.01f);
300 }
301
302 const float3 errorPos = targetUnit->GetErrorPos(owner->allyteam, true);
303 const float errorScale = (MoveErrorExperience() * GAME_SPEED * targetUnit->speed.w);
304
305 float3 tmpTargetPos = errorPos + lead + errorVector * errorScale;
306 float3 tmpTargetVec = tmpTargetPos - weaponMuzzlePos;
307 float3 tmpTargetDir = tmpTargetVec;
308
309 SetTargetBorderPos(targetUnit, tmpTargetPos, tmpTargetVec, tmpTargetDir);
310
311 // never target below terrain
312 // never target below water if not a water-weapon
313 targetPos = (targetBorder == 0.0f)? tmpTargetPos: targetBorderPos;
314 targetPos.y = std::max(targetPos.y, CGround::GetApproximateHeight(targetPos.x, targetPos.z) + 2.0f);
315 targetPos.y = std::max(targetPos.y, targetPos.y * weaponDef->waterweapon);
316 }
317
318 if (weaponDef->interceptor) {
319 // keep track of the closest projectile heading our way (if any)
320 UpdateInterceptTarget();
321 }
322
323 if (targetType != Target_None) {
324 AdjustTargetPosToWater(targetPos, targetType == Target_Pos);
325
326 const float3 worldTargetDir = (targetPos - owner->pos).SafeNormalize();
327 const float3 worldMainDir = owner->GetObjectSpaceVec(mainDir);
328 const bool targetAngleConstraint = CheckTargetAngleConstraint(worldTargetDir, worldMainDir);
329
330 if (angleGood && !targetAngleConstraint) {
331 // weapon finished a previously started AimWeapon thread and wants to
332 // fire, but target is no longer within contraints --> wait for re-aim
333 angleGood = false;
334 }
335 if (onlyForward && targetAngleConstraint) {
336 // NOTE:
337 // this should not need to be here, but many legacy scripts do not
338 // seem to define Aim*Ary in COB for units with onlyForward weapons
339 // (so angleGood is never set to true) -- REMOVE AFTER 90.0
340 angleGood = true;
341 }
342
343 if (gs->frameNum >= (lastRequest + (GAME_SPEED >> 1))) {
344 // periodically re-aim the weapon (by calling the script's AimWeapon
345 // every N=15 frames regardless of current angleGood state)
346 //
347 // NOTE:
348 // let scripts do active aiming even if we are an onlyForward weapon
349 // (reduces how far the entire unit must turn to face worldTargetDir)
350 //
351 // if AimWeapon sets angleGood immediately (ie. before it returns),
352 // the weapon can continuously fire at its maximum rate once every
353 // int(reloadTime / owner->reloadSpeed) frames
354 //
355 // if it does not (eg. because AimWeapon always spawns a thread to
356 // aim the weapon and defers setting angleGood to it) then this can
357 // lead to irregular/stuttering firing behavior, even in scenarios
358 // when the weapon does not have to re-aim --> detecting this case
359 // is the responsibility of the script
360 angleGood = false;
361
362 lastRequestedDir = wantedDir;
363 lastRequest = gs->frameNum;
364
365 const float heading = GetHeadingFromVectorF(wantedDir.x, wantedDir.z);
366 const float pitch = math::asin(Clamp(wantedDir.dot(owner->updir), -1.0f, 1.0f));
367
368 // for COB, this sets <angleGood> to return value of AimWeapon when finished,
369 // for LUS, there exists a callout to set the <angleGood> member directly.
370 // FIXME: convert CSolidObject::heading to radians too.
371 owner->script->AimWeapon(weaponNum, ClampRad(heading - owner->heading * TAANG2RAD), pitch);
372 }
373 }
374 }
375
376
CanFire(bool ignoreAngleGood,bool ignoreTargetType,bool ignoreRequestedDir) const377 bool CWeapon::CanFire(bool ignoreAngleGood, bool ignoreTargetType, bool ignoreRequestedDir) const
378 {
379 // FIXME merge some of the checks with TryTarget/TestRange/TestTarget (!)
380 if (!ignoreAngleGood && !angleGood)
381 return false;
382
383 if (salvoLeft != 0)
384 return false;
385
386 if (!ignoreTargetType && targetType == Target_None)
387 return false;
388
389 if (reloadStatus > gs->frameNum)
390 return false;
391
392 if (weaponDef->stockpile && numStockpiled == 0)
393 return false;
394
395 // muzzle is underwater but we cannot fire underwater
396 if (!weaponDef->fireSubmersed && weaponMuzzlePos.y <= 0.0f)
397 return false;
398
399 // ~20 degree sanity check to force new aim
400 if (!ignoreRequestedDir && wantedDir.dot(lastRequestedDir) <= 0.94f)
401 return false;
402
403 if ((owner->unitDef->maxFuel != 0) && (owner->currentFuel <= 0.0f) && (fuelUsage != 0.0f))
404 return false;
405
406 const CPlayer* fpsPlayer = owner->fpsControlPlayer;
407 const AAirMoveType* airMoveType = dynamic_cast<AAirMoveType*>(owner->moveType);
408
409 // if in FPS mode, player must be pressing at least one button to fire
410 if (fpsPlayer != NULL && !fpsPlayer->fpsController.mouse1 && !fpsPlayer->fpsController.mouse2)
411 return false;
412 // FIXME: there is already CUnit::dontUseWeapons but only used by HoverAirMoveType when landed
413 if (airMoveType != NULL && airMoveType->GetPadStatus() != AAirMoveType::PAD_STATUS_FLYING)
414 return false;
415
416 return true;
417 }
418
UpdateFire()419 void CWeapon::UpdateFire()
420 {
421 if (!CanFire(false, false, false))
422 return;
423
424 CTeam* ownerTeam = teamHandler->Team(owner->team);
425
426 if ((weaponDef->stockpile || (ownerTeam->metal >= metalFireCost && ownerTeam->energy >= energyFireCost))) {
427 owner->script->GetEmitDirPos(owner->script->QueryWeapon(weaponNum), relWeaponMuzzlePos, weaponDir);
428
429 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
430 weaponDir = owner->GetObjectSpaceVec(weaponDir);
431 weaponDir.SafeNormalize();
432 useWeaponPosForAim = (reloadTime / UNIT_SLOWUPDATE_RATE) + 8;
433
434 if (TryTarget(targetPos, haveUserTarget, targetUnit) && !CobBlockShot(targetUnit)) {
435 if (weaponDef->stockpile) {
436 const int oldCount = numStockpiled;
437 numStockpiled--;
438 owner->commandAI->StockpileChanged(this);
439 eventHandler.StockpileChanged(owner, this, oldCount);
440 } else {
441 owner->UseEnergy(energyFireCost);
442 owner->UseMetal(metalFireCost);
443 owner->currentFuel = std::max(0.0f, owner->currentFuel - fuelUsage);
444 }
445
446 reloadStatus = gs->frameNum + int(reloadTime / owner->reloadSpeed);
447
448 salvoLeft = salvoSize;
449 nextSalvo = gs->frameNum;
450 salvoError = gs->randVector() * (owner->IsMoving()? weaponDef->movingAccuracy: accuracyError);
451
452 if (targetType == Target_Pos || (targetType == Target_Unit && !(targetUnit->losStatus[owner->allyteam] & LOS_INLOS))) {
453 // area firing stuff is too effective at radar firing...
454 salvoError *= 1.3f;
455 }
456
457 owner->lastMuzzleFlameSize = muzzleFlareSize;
458 owner->lastMuzzleFlameDir = wantedDir;
459 owner->script->FireWeapon(weaponNum);
460 }
461 } else {
462 if (!weaponDef->stockpile && TryTarget(targetPos, haveUserTarget, targetUnit)) {
463 // update the energy and metal required counts
464 const int minPeriod = std::max(1, (int)(reloadTime / owner->reloadSpeed));
465 const float averageFactor = 1.0f / minPeriod;
466
467 ownerTeam->energyPull += (averageFactor * energyFireCost);
468 ownerTeam->metalPull += (averageFactor * metalFireCost);
469 }
470 }
471 }
472
473
UpdateStockpile()474 bool CWeapon::UpdateStockpile()
475 {
476 if (weaponDef->stockpile) {
477 if (numStockpileQued > 0) {
478 const float p = 1.0f / stockpileTime;
479
480 if (teamHandler->Team(owner->team)->metal >= metalFireCost*p && teamHandler->Team(owner->team)->energy >= energyFireCost*p) {
481 owner->UseEnergy(energyFireCost * p);
482 owner->UseMetal(metalFireCost * p);
483 buildPercent += p;
484 } else {
485 // update the energy and metal required counts
486 teamHandler->Team(owner->team)->energyPull += (energyFireCost * p);
487 teamHandler->Team(owner->team)->metalPull += (metalFireCost * p);
488 }
489 if (buildPercent >= 1) {
490 const int oldCount = numStockpiled;
491 buildPercent = 0;
492 numStockpileQued--;
493 numStockpiled++;
494 owner->commandAI->StockpileChanged(this);
495 eventHandler.StockpileChanged(owner, this, oldCount);
496 }
497 }
498
499 if (numStockpiled <= 0 && salvoLeft <= 0) {
500 return false;
501 }
502 }
503
504 return true;
505 }
506
507
UpdateSalvo()508 void CWeapon::UpdateSalvo()
509 {
510 if (salvoLeft && nextSalvo <= gs->frameNum) {
511 salvoLeft--;
512 nextSalvo = gs->frameNum + salvoDelay;
513 owner->lastFireWeapon = gs->frameNum;
514
515 int projectiles = projectilesPerShot;
516
517 const bool attackingPos = ((targetType == Target_Pos) && (targetPos == owner->attackPos));
518 const bool attackingUnit = ((targetType == Target_Unit) && (targetUnit == owner->attackTarget));
519
520 while (projectiles > 0) {
521 --projectiles;
522
523 owner->script->Shot(weaponNum);
524
525 int piece = owner->script->AimFromWeapon(weaponNum);
526 relWeaponPos = owner->script->GetPiecePos(piece);
527
528 piece = owner->script->QueryWeapon(weaponNum);
529 owner->script->GetEmitDirPos(piece, relWeaponMuzzlePos, weaponDir);
530
531 weaponPos = owner->GetObjectSpacePos(relWeaponPos);
532 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
533 weaponDir = owner->GetObjectSpaceVec(weaponDir);
534 weaponDir.SafeNormalize();
535
536 if (owner->unitDef->decloakOnFire && (owner->scriptCloak <= 2)) {
537 if (owner->isCloaked) {
538 owner->isCloaked = false;
539 eventHandler.UnitDecloaked(owner);
540 }
541 owner->curCloakTimeout = gs->frameNum + owner->cloakTimeout;
542 }
543
544 Fire(false);
545 }
546
547 //Rock the unit in the direction of fire
548 if (owner->script->HasRockUnit()) {
549 float3 rockDir = wantedDir;
550 rockDir.y = 0.0f;
551 rockDir = -rockDir.SafeNormalize();
552 owner->script->RockUnit(rockDir);
553 }
554
555 owner->commandAI->WeaponFired(this, weaponNum == 0, (salvoLeft == 0 && (attackingPos || attackingUnit)));
556
557 if (salvoLeft == 0) {
558 owner->script->EndBurst(weaponNum);
559 }
560 }
561 }
562
AttackGround(float3 newTargetPos,bool isUserTarget)563 bool CWeapon::AttackGround(float3 newTargetPos, bool isUserTarget)
564 {
565 if (!isUserTarget && weaponDef->noAutoTarget)
566 return false;
567 if (weaponDef->interceptor || !weaponDef->canAttackGround)
568 return false;
569
570 // keep target positions on the surface if this weapon hates water
571 AdjustTargetPosToWater(newTargetPos, true);
572
573 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
574
575 if (weaponMuzzlePos.y < CGround::GetHeightReal(weaponMuzzlePos.x, weaponMuzzlePos.z)) {
576 // hope that we are underground because we are a popup weapon and will come above ground later
577 weaponMuzzlePos = owner->pos + UpVector * 10;
578 }
579
580 if (!TryTarget(newTargetPos, isUserTarget, NULL))
581 return false;
582
583 if (targetUnit != NULL) {
584 DeleteDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
585 targetUnit = NULL;
586 }
587
588 haveUserTarget = isUserTarget;
589 targetType = Target_Pos;
590 targetPos = newTargetPos;
591
592 return true;
593 }
594
AttackUnit(CUnit * newTargetUnit,bool isUserTarget)595 bool CWeapon::AttackUnit(CUnit* newTargetUnit, bool isUserTarget)
596 {
597 if ((!isUserTarget && weaponDef->noAutoTarget)) {
598 return false;
599 }
600 if (weaponDef->interceptor)
601 return false;
602
603 weaponPos = owner->GetObjectSpacePos(relWeaponPos);
604 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
605
606 if (weaponMuzzlePos.y < CGround::GetHeightReal(weaponMuzzlePos.x, weaponMuzzlePos.z)) {
607 // hope that we are underground because we are a popup weapon and will come above ground later
608 weaponMuzzlePos = owner->pos + UpVector * 10;
609 }
610
611 if (newTargetUnit == NULL) {
612 if (targetType != Target_Unit) {
613 // make the unit be more likely to keep the current target if user starts to move it
614 targetType = Target_None;
615 }
616
617 // cannot have a user-target without a unit
618 haveUserTarget = false;
619 return false;
620 }
621
622 // check if it is theoretically impossible for us to attack this unit
623 // must be done before we assign <unit> to <targetUnit>, which itself
624 // must precede the TryTarget call (since we do want to assign if it
625 // is eg. just out of range currently --> however, this in turn causes
626 // "lock-on" targeting behavior which is less desirable, eg. we want a
627 // lock on a user-selected target that moved out of range to be broken
628 // after some time so automatic targeting can select new in-range units)
629 //
630 // note that TryTarget is also called from other places and so has to
631 // repeat this check, but the redundancy added is minimal
632 #if 0
633 if (!(onlyTargetCategory & newTargetUnit->category)) {
634 return false;
635 }
636 #endif
637
638 const float3 errorPos = newTargetUnit->GetErrorPos(owner->allyteam, true);
639 const float errorScale = (MoveErrorExperience() * GAME_SPEED * newTargetUnit->speed.w);
640 const float3 newTargetPos = errorPos + errorVector * errorScale;
641
642 if (!TryTarget(newTargetPos, isUserTarget, newTargetUnit))
643 return false;
644
645 if (targetUnit != NULL) {
646 DeleteDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
647 targetUnit = NULL;
648 }
649
650 haveUserTarget = isUserTarget;
651 targetType = Target_Unit;
652 targetUnit = newTargetUnit;
653 targetPos = (targetBorder == 0.0f)? newTargetPos: targetBorderPos;
654 targetPos.y = std::max(targetPos.y, CGround::GetApproximateHeight(targetPos.x, targetPos.z) + 2.0f);
655
656 AddDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
657 avoidTarget = false;
658
659 return true;
660 }
661
662
HoldFire()663 void CWeapon::HoldFire()
664 {
665 if (targetUnit) {
666 DeleteDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
667 targetUnit = NULL;
668 }
669
670 targetType = Target_None;
671
672 if (!weaponDef->noAutoTarget) {
673 // if haveUserTarget is set to false unconditionally, a subsequent
674 // call to AttackUnit from Unit::SlowUpdateWeapons would abort the
675 // attack for noAutoTarget weapons
676 haveUserTarget = false;
677 }
678 }
679
680
681
AllowWeaponTargetCheck()682 bool CWeapon::AllowWeaponTargetCheck()
683 {
684 const int checkAllowed = eventHandler.AllowWeaponTargetCheck(owner->id, weaponNum, weaponDef->id);
685
686 if (checkAllowed >= 0) {
687 return checkAllowed;
688 }
689
690 if (weaponDef->noAutoTarget) { return false; }
691 if (owner->fireState < FIRESTATE_FIREATWILL) { return false; }
692
693 // if CAI has an auto-generated attack order, do not interfere
694 if (!owner->commandAI->CanWeaponAutoTarget())
695 return false;
696
697 if (avoidTarget) { return true; }
698 if (targetType == Target_None) { return true; }
699
700 if (targetType == Target_Unit) {
701 if (targetUnit->category & badTargetCategory) {
702 return true;
703 }
704 if (!TryTarget(targetUnit, haveUserTarget)) {
705 // if we have a user-target (ie. a user attack order)
706 // then only allow generating opportunity targets iff
707 // it is not possible to hit the user's chosen unit
708 // TODO: this makes it easy to add toggle-able locking
709 //
710 // this will switch <targetUnit>, but the CAI will keep
711 // calling AttackUnit while the original order target is
712 // alive to put it back when possible
713 //
714 // note that the CAI itself only auto-picks a target
715 // when a unit has no commands left in its queue, so
716 // it can not interfere
717 return true;
718 }
719 }
720
721 if (gs->frameNum > (lastTargetRetry + 65)) {
722 return true;
723 }
724
725 return false;
726 }
727
AutoTarget()728 void CWeapon::AutoTarget() {
729 lastTargetRetry = gs->frameNum;
730
731 std::multimap<float, CUnit*> targets;
732 std::multimap<float, CUnit*>::const_iterator targetsIt;
733
734 // NOTE:
735 // sorts by INCREASING order of priority, so lower equals better
736 // <targets> can contain duplicates if a unit covers multiple quads
737 // <targets> is normally sorted such that all bad TC units are at the
738 // end, but Lua can mess with the ordering arbitrarily
739 CGameHelper::GenerateWeaponTargets(this, targetUnit, targets);
740
741 CUnit* prevTargetUnit = NULL;
742 CUnit* goodTargetUnit = NULL;
743 CUnit* badTargetUnit = NULL;
744
745 float3 nextTargetPos = ZeroVector;
746
747 for (targetsIt = targets.begin(); targetsIt != targets.end(); ++targetsIt) {
748 CUnit* nextTargetUnit = targetsIt->second;
749
750 if (nextTargetUnit == prevTargetUnit)
751 continue; // filter consecutive duplicates
752 if (nextTargetUnit->IsNeutral() && (owner->fireState <= FIRESTATE_FIREATWILL))
753 continue;
754
755 const float weaponError = MoveErrorExperience() * GAME_SPEED * nextTargetUnit->speed.w;
756
757 prevTargetUnit = nextTargetUnit;
758 nextTargetPos = nextTargetUnit->aimPos + (errorVector * weaponError);
759
760 const float appHeight = CGround::GetApproximateHeight(nextTargetPos.x, nextTargetPos.z) + 2.0f;
761
762 if (nextTargetPos.y < appHeight) {
763 nextTargetPos.y = appHeight;
764 }
765
766 if (!TryTarget(nextTargetPos, false, nextTargetUnit))
767 continue;
768
769 if ((nextTargetUnit->category & badTargetCategory) != 0) {
770 // save the "best" bad target in case we have no other
771 // good targets (of higher priority) left in <targets>
772 if (badTargetUnit != NULL)
773 continue;
774
775 badTargetUnit = nextTargetUnit;
776 } else {
777 goodTargetUnit = nextTargetUnit;
778 break;
779 }
780 }
781
782 if (goodTargetUnit != NULL || badTargetUnit != NULL) {
783 const bool haveOldTarget = (targetUnit != NULL);
784 const bool haveNewTarget =
785 (goodTargetUnit != NULL && goodTargetUnit != targetUnit) ||
786 ( badTargetUnit != NULL && badTargetUnit != targetUnit);
787
788 if (haveOldTarget && haveNewTarget) {
789 // delete our old target dependence if we are switching targets
790 DeleteDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
791 }
792
793 // pick our new target
794 targetType = Target_Unit;
795 targetUnit = (goodTargetUnit != NULL)? goodTargetUnit: badTargetUnit;
796 targetPos = nextTargetPos;
797
798 if (!haveOldTarget || haveNewTarget) {
799 // add new target dependence if we had no target or switched
800 AddDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
801 }
802 }
803 }
804
805
806
SlowUpdate()807 void CWeapon::SlowUpdate()
808 {
809 SlowUpdate(false);
810 }
811
SlowUpdate(bool noAutoTargetOverride)812 void CWeapon::SlowUpdate(bool noAutoTargetOverride)
813 {
814 #ifdef TRACE_SYNC
815 tracefile << "Weapon slow update: ";
816 tracefile << owner->id << " " << weaponNum << "\n";
817 #endif
818
819 UpdateRelWeaponPos();
820 useWeaponPosForAim = std::max(0, useWeaponPosForAim - 1);
821
822 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
823 weaponPos = owner->GetObjectSpacePos(relWeaponPos);
824
825 if (weaponMuzzlePos.y < CGround::GetHeightReal(weaponMuzzlePos.x, weaponMuzzlePos.z)) {
826 // hope that we are underground because we are a popup weapon and will come above ground later
827 weaponMuzzlePos = owner->pos + UpVector * 10;
828 }
829
830 predictSpeedMod = 1.0f + (gs->randFloat() - 0.5f) * 2 * (1.0f - owner->limExperience);
831 hasCloseTarget = ((targetPos - weaponPos).SqLength() < relWeaponPos.SqLength() * 16);
832
833
834 if (targetType != Target_None && !TryTarget(targetPos, haveUserTarget, targetUnit)) {
835 HoldFire();
836 }
837
838 if (targetType == Target_Unit) {
839 // stop firing at cloaked targets
840 if (targetUnit != NULL && targetUnit->isCloaked && !(targetUnit->losStatus[owner->allyteam] & (LOS_INLOS | LOS_INRADAR)))
841 HoldFire();
842
843 if (!haveUserTarget) {
844 // stop firing at neutral targets (unless in FAW mode)
845 // note: HoldFire sets targetUnit to NULL, so recheck
846 if (targetUnit != NULL && targetUnit->IsNeutral() && owner->fireState <= FIRESTATE_FIREATWILL)
847 HoldFire();
848
849 // stop firing at allied targets
850 //
851 // this situation (unit keeps attacking its target if the
852 // target or the unit switches to an allied team) should
853 // be handled by /ally processing now
854 if (targetUnit != NULL && teamHandler->Ally(owner->allyteam, targetUnit->allyteam))
855 HoldFire();
856 }
857 }
858
859 if (slavedTo != NULL) {
860 // use targets from the thing we are slaved to
861 if (targetUnit) {
862 DeleteDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
863 targetUnit = NULL;
864 }
865 targetType = Target_None;
866
867 if (slavedTo->targetType == Target_Unit) {
868 const float errorScale = (MoveErrorExperience() * GAME_SPEED * slavedTo->targetUnit->speed.w);
869
870 const float3 errorPos = slavedTo->targetUnit->GetErrorPos(owner->allyteam, true);
871 const float3 targetErrPos = errorPos + errorVector * errorScale;
872
873 if (TryTarget(targetErrPos, false, slavedTo->targetUnit)) {
874 targetType = Target_Unit;
875 targetUnit = slavedTo->targetUnit;
876 targetPos = targetErrPos;
877
878 AddDeathDependence(targetUnit, DEPENDENCE_TARGETUNIT);
879 }
880 } else if (slavedTo->targetType == Target_Pos) {
881 if (TryTarget(slavedTo->targetPos, false, 0)) {
882 targetType = Target_Pos;
883 targetPos = slavedTo->targetPos;
884 }
885 }
886 return;
887 }
888
889
890 if (!noAutoTargetOverride && AllowWeaponTargetCheck()) {
891 AutoTarget();
892 }
893
894 if (targetType == Target_None) {
895 // if we can't target anything, try switching aim point
896 useWeaponPosForAim = std::max(0, useWeaponPosForAim - 1);
897 }
898 }
899
DependentDied(CObject * o)900 void CWeapon::DependentDied(CObject* o)
901 {
902 if (o == targetUnit) {
903 targetUnit = NULL;
904 if (targetType == Target_Unit) {
905 targetType = Target_None;
906 haveUserTarget = false;
907 }
908 }
909
910 // NOTE: DependentDied is called from ~CObject-->Detach, object is just barely valid
911 if (weaponDef->interceptor || weaponDef->isShield) {
912 incomingProjectiles.erase(static_cast<CWeaponProjectile*>(o)->id);
913 }
914
915 if (o == interceptTarget) {
916 interceptTarget = NULL;
917 }
918 }
919
TargetUnitOrPositionUnderWater(const float3 & targetPos,const CUnit * targetUnit,float offset)920 bool CWeapon::TargetUnitOrPositionUnderWater(const float3& targetPos, const CUnit* targetUnit, float offset)
921 {
922 // test if a target position or unit is strictly underwater
923 if (targetUnit != NULL) {
924 return (targetUnit->IsUnderWater());
925 } else {
926 // consistent with CSolidObject::IsUnderWater (LT)
927 return ((targetPos.y + offset) < 0.0f);
928 }
929 }
930
TargetUnitOrPositionInWater(const float3 & targetPos,const CUnit * targetUnit,float offset)931 bool CWeapon::TargetUnitOrPositionInWater(const float3& targetPos, const CUnit* targetUnit, float offset)
932 {
933 // test if a target position or unit is in water (including underwater)
934 if (targetUnit != NULL) {
935 return (targetUnit->IsInWater());
936 } else {
937 // consistent with CSolidObject::IsInWater (LE)
938 return ((targetPos.y + offset) <= 0.0f);
939 }
940 }
941
CheckTargetAngleConstraint(const float3 & worldTargetDir,const float3 & worldWeaponDir) const942 bool CWeapon::CheckTargetAngleConstraint(const float3& worldTargetDir, const float3& worldWeaponDir) const {
943 if (onlyForward) {
944 if (maxForwardAngleDif > -1.0f) {
945 // if we are not a turret, we care about our owner's direction
946 if (owner->frontdir.dot(worldTargetDir) < maxForwardAngleDif)
947 return false;
948 }
949 } else {
950 if (maxMainDirAngleDif > -1.0f) {
951 if (worldWeaponDir.dot(worldTargetDir) < maxMainDirAngleDif)
952 return false;
953 }
954 }
955
956 return true;
957 }
958
959
SetTargetBorderPos(CUnit * targetUnit,float3 & rawTargetPos,float3 & rawTargetVec,float3 & rawTargetDir)960 bool CWeapon::SetTargetBorderPos(
961 CUnit* targetUnit,
962 float3& rawTargetPos,
963 float3& rawTargetVec,
964 float3& rawTargetDir)
965 {
966 if (targetBorder == 0.0f)
967 return false;
968 if (targetUnit == NULL)
969 return false;
970
971 const float tbScale = math::fabsf(targetBorder);
972
973 CollisionVolume tmpColVol = CollisionVolume(targetUnit->collisionVolume);
974 CollisionQuery tmpColQry;
975
976 // test for "collision" with a temporarily volume
977 // (scaled uniformly by the absolute target-border
978 // factor)
979 tmpColVol.RescaleAxes(float3(tbScale, tbScale, tbScale));
980 tmpColVol.SetBoundingRadius();
981 tmpColVol.SetUseContHitTest(false);
982
983 targetBorderPos = rawTargetPos;
984
985 if (CCollisionHandler::DetectHit(&tmpColVol, targetUnit, weaponMuzzlePos, ZeroVector, NULL)) {
986 // our weapon muzzle is inside the target unit's volume; this
987 // means we do not need to make any adjustments to targetVec
988 // (in this case targetBorderPos remains equal to targetPos)
989 rawTargetVec = ZeroVector;
990 } else {
991 rawTargetDir = rawTargetDir.SafeNormalize();
992
993 // otherwise, perform a raytrace to find the proper length correction
994 // factor for non-spherical coldet volumes based on the ray's ingress
995 // (for positive TB values) or egress (for negative TB values) position;
996 // this either increases or decreases the length of <targetVec> but does
997 // not change its direction
998 tmpColVol.SetUseContHitTest(true);
999
1000 // make the ray-segment long enough so it can reach the far side of the
1001 // scaled collision volume (helps to ensure a ray-intersection is found)
1002 //
1003 // note: ray-intersection is NOT guaranteed if the volume itself has a
1004 // non-zero offset, since here we are "shooting" at the target UNIT's
1005 // aimpoint
1006 const float3 targetOffset = rawTargetDir * (tmpColVol.GetBoundingRadius() * 2.0f);
1007 const float3 targetRayPos = rawTargetPos + targetOffset;
1008
1009 // adjust the length of <targetVec> based on the targetBorder factor
1010 if (CCollisionHandler::DetectHit(&tmpColVol, targetUnit, weaponMuzzlePos, targetRayPos, &tmpColQry)) {
1011 if (targetBorder > 0.0f) { rawTargetVec -= (rawTargetDir * rawTargetPos.distance(tmpColQry.GetIngressPos())); }
1012 if (targetBorder < 0.0f) { rawTargetVec += (rawTargetDir * rawTargetPos.distance(tmpColQry.GetEgressPos())); }
1013
1014 targetBorderPos = weaponMuzzlePos + rawTargetVec;
1015 }
1016 }
1017
1018 // true indicates we took the else-branch and rawTargetDir was normalized
1019 // note: this does *NOT* also imply that targetBorderPos != rawTargetPos
1020 return (rawTargetDir.SqLength() == 1.0f);
1021 }
1022
GetTargetBorderPos(const CUnit * targetUnit,const float3 & rawTargetPos,float3 & rawTargetVec,float3 & rawTargetDir) const1023 bool CWeapon::GetTargetBorderPos(const CUnit* targetUnit, const float3& rawTargetPos, float3& rawTargetVec, float3& rawTargetDir) const
1024 {
1025 if (targetBorder == 0.0f)
1026 return false;
1027 if (targetUnit == NULL)
1028 return false;
1029
1030 const float tbScale = math::fabsf(targetBorder);
1031
1032 CollisionVolume tmpColVol(targetUnit->collisionVolume);
1033 CollisionQuery tmpColQry;
1034
1035 // test for "collision" with a temporarily volume
1036 // (scaled uniformly by the absolute target-border
1037 // factor)
1038 tmpColVol.RescaleAxes(float3(tbScale, tbScale, tbScale));
1039 tmpColVol.SetBoundingRadius();
1040 tmpColVol.SetUseContHitTest(false);
1041
1042 if (CCollisionHandler::DetectHit(&tmpColVol, targetUnit, weaponMuzzlePos, ZeroVector, NULL)) {
1043 // our weapon muzzle is inside the target unit's volume; this
1044 // means we do not need to make any adjustments to targetVec
1045 rawTargetVec = ZeroVector;
1046 } else {
1047 rawTargetDir = rawTargetDir.SafeNormalize();
1048
1049 // otherwise, perform a raytrace to find the proper length correction
1050 // factor for non-spherical coldet volumes based on the ray's ingress
1051 // (for positive TB values) or egress (for negative TB values) position;
1052 // this either increases or decreases the length of <targetVec> but does
1053 // not change its direction
1054 tmpColVol.SetUseContHitTest(true);
1055
1056 // make the ray-segment long enough so it can reach the far side of the
1057 // scaled collision volume (helps to ensure a ray-intersection is found)
1058 //
1059 // note: ray-intersection is NOT guaranteed if the volume itself has a
1060 // non-zero offset, since here we are "shooting" at the target UNIT's
1061 // aimpoint
1062 const float3 targetOffset = rawTargetDir * (tmpColVol.GetBoundingRadius() * 2.0f);
1063 const float3 targetRayPos = rawTargetPos + targetOffset;
1064
1065 // adjust the length of <targetVec> based on the targetBorder factor
1066 if (CCollisionHandler::DetectHit(&tmpColVol, targetUnit, weaponMuzzlePos, targetRayPos, &tmpColQry)) {
1067 if (targetBorder > 0.0f) { rawTargetVec -= (rawTargetDir * rawTargetPos.distance(tmpColQry.GetIngressPos())); }
1068 if (targetBorder < 0.0f) { rawTargetVec += (rawTargetDir * rawTargetPos.distance(tmpColQry.GetEgressPos())); }
1069 }
1070 }
1071
1072 // true indicates we took the else-branch and rawTargetDir was normalized
1073 return (rawTargetDir.SqLength() == 1.0f);
1074 }
1075
TryTarget(const float3 & tgtPos,bool userTarget,const CUnit * targetUnit) const1076 bool CWeapon::TryTarget(const float3& tgtPos, bool userTarget, const CUnit* targetUnit) const
1077 {
1078 if (!TestTarget(tgtPos, userTarget, targetUnit))
1079 return false;
1080
1081 if (!TestRange(tgtPos, userTarget, targetUnit))
1082 return false;
1083
1084 //FIXME add a forcedUserTarget (a forced fire mode enabled with ctrl key or something) and skip the tests below then
1085 return (HaveFreeLineOfFire(tgtPos, userTarget, targetUnit));
1086 }
1087
1088
1089 // if targetUnit != NULL, this checks our onlyTargetCategory against unit->category
1090 // etc. as well as range, otherwise the only concern is range and angular difference
1091 // (terrain is NOT checked here, HaveFreeLineOfFire does that)
TestTarget(const float3 & tgtPos,bool,const CUnit * targetUnit) const1092 bool CWeapon::TestTarget(const float3& tgtPos, bool /*userTarget*/, const CUnit* targetUnit) const
1093 {
1094 if (targetUnit != NULL) {
1095 if (targetUnit == owner)
1096 return false;
1097 if ((targetUnit->category & onlyTargetCategory) == 0)
1098 return false;
1099 if (targetUnit->isDead && modInfo.fireAtKilled == 0)
1100 return false;
1101 if (targetUnit->IsCrashing() && modInfo.fireAtCrashing == 0)
1102 return false;
1103 }
1104
1105 if (!weaponDef->waterweapon) {
1106 // we cannot pick targets underwater, check where target is in relation to us
1107 if (!owner->IsUnderWater() && TargetUnitOrPositionUnderWater(tgtPos, targetUnit))
1108 return false;
1109 // if we are underwater but target is *not* in water, fireSubmersed gets checked
1110 if (owner->IsUnderWater() && TargetUnitOrPositionInWater(tgtPos, targetUnit))
1111 return false;
1112 }
1113
1114 return true;
1115 }
1116
TestRange(const float3 & tgtPos,bool,const CUnit * targetUnit) const1117 bool CWeapon::TestRange(const float3& tgtPos, bool /*userTarget*/, const CUnit* targetUnit) const
1118 {
1119 float3 tmpTargetPos = tgtPos;
1120 float3 tmpTargetVec = tmpTargetPos - weaponMuzzlePos;
1121 float3 tmpTargetDir = tmpTargetVec;
1122
1123 const bool normalized = GetTargetBorderPos(targetUnit, tmpTargetPos, tmpTargetVec, tmpTargetDir);
1124
1125 const float heightDiff = (weaponMuzzlePos.y + tmpTargetVec.y) - owner->pos.y;
1126 float weaponRange = 0.0f; // range modified by heightDiff and cylinderTargeting
1127
1128 if (targetUnit == NULL || cylinderTargeting < 0.01f) {
1129 // check range in a sphere (with extra radius <heightDiff * heightMod>)
1130 weaponRange = GetRange2D(heightDiff * heightMod);
1131 } else {
1132 // check range in a cylinder (with height <cylinderTargeting * range>)
1133 if ((cylinderTargeting * range) > (math::fabsf(heightDiff) * heightMod)) {
1134 weaponRange = GetRange2D(0.0f);
1135 }
1136 }
1137
1138 if (tmpTargetVec.SqLength2D() >= (weaponRange * weaponRange))
1139 return false;
1140
1141 // NOTE: mainDir is in unit-space
1142 const float3 targetNormDir = normalized? tmpTargetDir: tmpTargetDir.SafeNormalize();
1143 const float3 worldMainDir = owner->GetObjectSpaceVec(mainDir);
1144
1145 return (CheckTargetAngleConstraint(targetNormDir, worldMainDir));
1146 }
1147
1148
HaveFreeLineOfFire(const float3 & pos,bool userTarget,const CUnit * unit) const1149 bool CWeapon::HaveFreeLineOfFire(const float3& pos, bool userTarget, const CUnit* unit) const
1150 {
1151 float3 dir = pos - weaponMuzzlePos;
1152
1153 const float length = dir.Length();
1154 const float spread = AccuracyExperience() + SprayAngleExperience();
1155
1156 if (length == 0.0f)
1157 return true;
1158
1159 dir /= length;
1160
1161 // ground check
1162 if ((avoidFlags & Collision::NOGROUND) == 0) {
1163 // NOTE:
1164 // ballistic weapons (Cannon / Missile icw. trajectoryHeight) do not call this,
1165 // they use TrajectoryGroundCol with an external check for the NOGROUND flag
1166 CUnit* unit = NULL;
1167 CFeature* feature = NULL;
1168
1169 const float gdst = TraceRay::TraceRay(weaponMuzzlePos, dir, length, ~Collision::NOGROUND, owner, unit, feature);
1170 const float3 gpos = weaponMuzzlePos + dir * gdst;
1171
1172 // true iff ground does not block the ray of length <length> from <pos> along <dir>
1173 if ((gdst > 0.0f) && (gpos.SqDistance(pos) > Square(damageAreaOfEffect)))
1174 return false;
1175 }
1176
1177 // friendly, neutral & feature check
1178 if (TraceRay::TestCone(weaponMuzzlePos, dir, length, spread, owner->allyteam, avoidFlags, owner)) {
1179 return false;
1180 }
1181
1182 return true;
1183 }
1184
1185
TryTarget(const CUnit * unit,bool userTarget) const1186 bool CWeapon::TryTarget(const CUnit* unit, bool userTarget) const {
1187 return TryTarget(GetUnitPositionWithError(unit), userTarget, unit);
1188 }
1189
GetUnitPositionWithError(const CUnit * unit) const1190 float3 CWeapon::GetUnitPositionWithError(const CUnit* unit) const {
1191 const float3 errorPos = unit->GetErrorPos(owner->allyteam, true);
1192 const float errorScale = (MoveErrorExperience() * GAME_SPEED * unit->speed.w);
1193
1194 float3 tempTargetPos = errorPos + errorVector * errorScale;
1195 tempTargetPos.y = std::max(tempTargetPos.y, CGround::GetApproximateHeight(tempTargetPos.x, tempTargetPos.z) + 2.0f);
1196 return tempTargetPos;
1197 }
1198
TryTargetRotate(CUnit * unit,bool userTarget)1199 bool CWeapon::TryTargetRotate(CUnit* unit, bool userTarget) {
1200 const float3 errorPos = unit->GetErrorPos(owner->allyteam, true);
1201 const float errorScale = (MoveErrorExperience() * GAME_SPEED * unit->speed.w);
1202
1203 float3 tempTargetPos = errorPos + errorVector * errorScale;
1204 tempTargetPos.y = std::max(tempTargetPos.y, CGround::GetApproximateHeight(tempTargetPos.x, tempTargetPos.z) + 2.0f);
1205
1206 const short weaponHeading = GetHeadingFromVector(mainDir.x, mainDir.z);
1207 const short enemyHeading = GetHeadingFromVector(tempTargetPos.x - weaponPos.x, tempTargetPos.z - weaponPos.z);
1208
1209 return TryTargetHeading(enemyHeading - weaponHeading, tempTargetPos, userTarget, unit);
1210 }
1211
TryTargetRotate(float3 pos,bool userTarget)1212 bool CWeapon::TryTargetRotate(float3 pos, bool userTarget) {
1213 if (!userTarget && weaponDef->noAutoTarget)
1214 return false;
1215 if (weaponDef->interceptor || !weaponDef->canAttackGround)
1216 return false;
1217
1218 AdjustTargetPosToWater(pos, true);
1219
1220 const short weaponHeading = GetHeadingFromVector(mainDir.x, mainDir.z);
1221 const short enemyHeading = GetHeadingFromVector(
1222 pos.x - weaponPos.x, pos.z - weaponPos.z);
1223
1224 return TryTargetHeading(enemyHeading - weaponHeading, pos, userTarget, 0);
1225 }
1226
TryTargetHeading(short heading,float3 pos,bool userTarget,CUnit * unit)1227 bool CWeapon::TryTargetHeading(short heading, float3 pos, bool userTarget, CUnit* unit) {
1228 const float3 tempfrontdir(owner->frontdir);
1229 const float3 temprightdir(owner->rightdir);
1230 const short tempHeading = owner->heading;
1231
1232 AdjustTargetPosToWater(pos, unit == NULL);
1233
1234 owner->heading = heading;
1235 owner->frontdir = GetVectorFromHeading(owner->heading);
1236 owner->rightdir = owner->frontdir.cross(owner->updir);
1237
1238 weaponPos = owner->GetObjectSpacePos(relWeaponPos);
1239 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
1240
1241 const bool val = TryTarget(pos, userTarget, unit);
1242
1243 owner->frontdir = tempfrontdir;
1244 owner->rightdir = temprightdir;
1245 owner->heading = tempHeading;
1246
1247 weaponPos = owner->GetObjectSpacePos(relWeaponPos);
1248 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
1249
1250 return val;
1251
1252 }
1253
Init()1254 void CWeapon::Init()
1255 {
1256 relWeaponPos = owner->script->GetPiecePos(owner->script->AimFromWeapon(weaponNum));
1257 weaponPos = owner->GetObjectSpacePos(relWeaponPos);
1258
1259 relWeaponMuzzlePos = owner->script->GetPiecePos(owner->script->QueryWeapon(weaponNum));
1260 weaponMuzzlePos = owner->GetObjectSpacePos(relWeaponMuzzlePos);
1261
1262 muzzleFlareSize = std::min(damageAreaOfEffect * 0.2f, std::min(1500.f, weaponDef->damages[0]) * 0.003f);
1263
1264 if (weaponDef->interceptor)
1265 interceptHandler.AddInterceptorWeapon(this);
1266
1267 if (weaponDef->stockpile) {
1268 owner->stockpileWeapon = this;
1269 owner->commandAI->AddStockpileWeapon(this);
1270 }
1271
1272 if (weaponDef->isShield) {
1273 if ((owner->shieldWeapon == NULL) ||
1274 (owner->shieldWeapon->weaponDef->shieldRadius < weaponDef->shieldRadius)) {
1275 owner->shieldWeapon = this;
1276 }
1277 }
1278 }
1279
Fire(bool scriptCall)1280 void CWeapon::Fire(bool scriptCall)
1281 {
1282 #ifdef TRACE_SYNC
1283 tracefile << weaponDef->name.c_str() << " fire: ";
1284 tracefile << owner->pos.x << " " << owner->frontdir.x << " " << targetPos.x << " " << targetPos.y << " " << targetPos.z;
1285 tracefile << sprayAngle << " " << " " << salvoError.x << " " << salvoError.z << " " << owner->limExperience << " " << projectileSpeed << "\n";
1286 #endif
1287 FireImpl(scriptCall);
1288
1289 if (fireSoundId > 0 && (!weaponDef->soundTrigger || salvoLeft == salvoSize - 1)) {
1290 Channels::Battle->PlaySample(fireSoundId, owner, fireSoundVolume);
1291 }
1292 }
1293
UpdateInterceptTarget()1294 void CWeapon::UpdateInterceptTarget()
1295 {
1296 targetType = Target_None;
1297
1298 float minInterceptTargetDistSq = std::numeric_limits<float>::max();
1299 float curInterceptTargetDistSq = std::numeric_limits<float>::min();
1300
1301 for (std::map<int, CWeaponProjectile*>::iterator pi = incomingProjectiles.begin(); pi != incomingProjectiles.end(); ++pi) {
1302 CWeaponProjectile* p = pi->second;
1303
1304 // set by CWeaponProjectile's ctor when the interceptor fires
1305 if (p->IsBeingIntercepted())
1306 continue;
1307 if ((curInterceptTargetDistSq = (p->pos - weaponPos).SqLength()) >= minInterceptTargetDistSq)
1308 continue;
1309
1310 minInterceptTargetDistSq = curInterceptTargetDistSq;
1311
1312 // NOTE:
1313 // <incomingProjectiles> is sorted by increasing projectile ID
1314 // however projectiles launched later in time (which are still
1315 // likely out of range) can be assigned *lower* ID's than older
1316 // projectiles (which might be almost in range already), so if
1317 // we already have an interception target we should not replace
1318 // it unless another incoming projectile <p> is closer
1319 //
1320 // this is still not optimal (closer projectiles should receive
1321 // higher priority), so just always look for the overall closest
1322 // if ((interceptTarget != NULL) && ((p->pos - weaponPos).SqLength() >= (interceptTarget->pos - weaponPos).SqLength()))
1323 // continue;
1324
1325 // trigger us to auto-fire at this incoming projectile
1326 // we do not really need to set targetPos here since it
1327 // will be read from params.target (GetProjectileParams)
1328 // when our subclass Fire()'s
1329 interceptTarget = p;
1330 targetType = Target_Intercept;
1331 targetPos = p->pos + p->speed;
1332 }
1333 }
1334
1335
GetProjectileParams()1336 ProjectileParams CWeapon::GetProjectileParams()
1337 {
1338 ProjectileParams params;
1339
1340 if (interceptTarget != NULL) {
1341 params.target = interceptTarget;
1342 } else {
1343 params.target = targetUnit;
1344 }
1345
1346 params.weaponID = weaponNum;
1347 params.owner = owner;
1348 params.weaponDef = weaponDef;
1349
1350 return params;
1351 }
1352
1353
GetRange2D(float yDiff) const1354 float CWeapon::GetRange2D(float yDiff) const
1355 {
1356 const float root1 = range * range - yDiff * yDiff;
1357
1358 if (root1 <= 0.0f)
1359 return 0.0f;
1360
1361 return (math::sqrt(root1));
1362 }
1363
1364
StopAttackingAllyTeam(int ally)1365 void CWeapon::StopAttackingAllyTeam(int ally)
1366 {
1367 if (targetUnit && targetUnit->allyteam == ally) {
1368 HoldFire();
1369 }
1370 }
1371
1372
ExperienceErrorScale() const1373 float CWeapon::ExperienceErrorScale() const
1374 {
1375 // accuracy (error) is increased (decreased) with experience
1376 // scale is 1.0f - (limExperience * expAccWeight), such that
1377 // for weight=0 scale is 1 and for weight=1 scale is 1 - exp
1378 // (lower is better)
1379 //
1380 // for accWeight=0.00 and {0.25, 0.50, 0.75, 1.0} exp, scale=(1.0 - {0.25*0.00, 0.5*0.00, 0.75*0.00, 1.0*0.00}) = {1.0000, 1.000, 1.0000, 1.00}
1381 // for accWeight=0.25 and {0.25, 0.50, 0.75, 1.0} exp, scale=(1.0 - {0.25*0.25, 0.5*0.25, 0.75*0.25, 1.0*0.25}) = {0.9375, 0.875, 0.8125, 0.75}
1382 // for accWeight=0.50 and {0.25, 0.50, 0.75, 1.0} exp, scale=(1.0 - {0.25*0.50, 0.5*0.50, 0.75*0.50, 1.0*0.50}) = {0.8750, 0.750, 0.6250, 0.50}
1383 // for accWeight=1.00 and {0.25, 0.50, 0.75, 1.0} exp, scale=(1.0 - {0.25*1.00, 0.5*1.00, 0.75*1.00, 1.0*0.75}) = {0.7500, 0.500, 0.2500, 0.25}
1384 return (CUnit::ExperienceScale(owner->limExperience, weaponDef->ownerExpAccWeight));
1385 }
1386
MoveErrorExperience() const1387 float CWeapon::MoveErrorExperience() const
1388 {
1389 return (ExperienceErrorScale() * weaponDef->targetMoveError);
1390 }
1391
1392