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