1 
2 #include <algorithm>
3 
4 #include "game/logic/attackjob.h"
5 
6 #include "game/data/units/unit.h"
7 #include "game/data/map/map.h"
8 #include "game/data/units/building.h"
9 #include "game/data/units/vehicle.h"
10 #include "game/data/player/player.h"
11 #include "game/data/report/unit/savedreportdestroyed.h"
12 #include "game/data/report/unit/savedreportattacked.h"
13 #include "netmessage.h"
14 #include "clientevents.h"
15 #include "server.h"
16 #include "client.h"
17 #include "fxeffects.h"
18 #include "utility/log.h"
19 #include "output/sound/sounddevice.h"
20 #include "output/sound/soundchannel.h"
21 
22 
23 //TODO: test alien attack (gound & air)
24 //TODO: load/save attackjobs + isAttacking/isAttacked
25 
26 
27 //--------------------------------------------------------------------------
selectTarget(const cPosition & position,char attackMode,const cMap & map,cPlayer * owner)28 cUnit* cAttackJob::selectTarget (const cPosition& position, char attackMode, const cMap& map, cPlayer* owner)
29 {
30 	cVehicle* targetVehicle = nullptr;
31 	cBuilding* targetBuilding = nullptr;
32 	const cMapField& mapField = map.getField (position);
33 
34 	//planes
35 	//prefere enemy planes. But select own one, if there is no enemy
36 	auto planes = mapField.getPlanes();
37 	for (cVehicle* plane : planes)
38 	{
39 		if (plane->getFlightHeight() >  0 && ! (attackMode & TERRAIN_AIR))    continue;
40 		if (plane->getFlightHeight() == 0 && ! (attackMode & TERRAIN_GROUND)) continue;
41 
42 		if (targetVehicle == nullptr)
43 		{
44 			targetVehicle = plane;
45 		}
46 		else if (targetVehicle->getOwner() == owner)
47 		{
48 			if (plane->getOwner() != owner)
49 			{
50 				targetVehicle = plane;
51 			}
52 		}
53 	}
54 
55 	// vehicles
56 	if (!targetVehicle && (attackMode & TERRAIN_GROUND))
57 	{
58 		targetVehicle = mapField.getVehicle();
59 		if (targetVehicle && (targetVehicle->data.isStealthOn & TERRAIN_SEA) && map.isWater (position) && ! (attackMode & AREA_SUB)) targetVehicle = nullptr;
60 	}
61 
62 	// buildings
63 	if (!targetVehicle && (attackMode & TERRAIN_GROUND))
64 	{
65 		targetBuilding = mapField.getBuilding();
66 		if (targetBuilding && !targetBuilding->getOwner()) targetBuilding = nullptr;
67 	}
68 
69 	if (targetVehicle) return targetVehicle;
70 	return targetBuilding;
71 }
72 
runAttackJobs(std::vector<cAttackJob * > & attackJobs)73 void cAttackJob::runAttackJobs (std::vector<cAttackJob*>& attackJobs)
74 {
75 	auto attackJobsTemp = attackJobs;
76 	for (auto attackJob : attackJobsTemp)
77 	{
78 		attackJob->run(); //this can add new items to 'attackjobs'
79 		if (attackJob->finished())
80 		{
81 			delete attackJob;
82 			attackJobs.erase (std::find (attackJobs.begin(), attackJobs.end(), attackJob));
83 		}
84 	}
85 }
86 
87 //--------------------------------------------------------------------------
cAttackJob(cServer * server_,cUnit * aggressor_,const cPosition & targetPosition_)88 cAttackJob::cAttackJob (cServer* server_, cUnit* aggressor_, const cPosition& targetPosition_) :
89 	aggressorID (aggressor_->iID),
90 	aggressorPlayerNr (aggressor_->getOwner()->getNr()),
91 	aggressorPosition (aggressor_->getPosition()),
92 	attackMode (aggressor_->data.canAttack),
93 	muzzleType (aggressor_->data.muzzleType),
94 	attackPoints (aggressor_->data.getDamage()),
95 	targetPosition (targetPosition_),
96 	server (server_),
97 	client (nullptr),
98 	destroyedTargets(),
99 	fireDir (0),
100 	state (S_ROTATING)
101 {
102 	fireDir = calcFireDir();
103 	counter = calcTimeForRotation() + FIRE_DELAY;
104 
105 	Log.write (" Server: Created AttackJob. Aggressor: " + aggressor_->getDisplayName() + " (ID: " + iToStr (aggressor_->iID) + ") at (" + iToStr (aggressorPosition.x()) + "," + iToStr (aggressorPosition.y()) + "). Target: (" + iToStr (targetPosition.x()) + "," + iToStr (targetPosition.y()) + ").", cLog::eLOG_TYPE_NET_DEBUG);
106 
107 	lockTarget();
108 
109 	server->sendNetMessage (serialize());
110 
111 	//lock agressor
112 	aggressor_->setAttacking (true);
113 
114 	// make the aggressor visible on all clients
115 	// who can see the aggressor offset
116 	for (const auto& player : server->playerList)
117 	{
118 		if (player->canSeeAnyAreaUnder (*aggressor_) == false) continue;
119 		if (aggressor_->getOwner() == player.get()) continue;
120 
121 		aggressor_->setDetectedByPlayer (*server, player.get());
122 	}
123 }
124 
cAttackJob(cClient * client_,cNetMessage & message)125 cAttackJob::cAttackJob (cClient* client_, cNetMessage& message) :
126 	server (nullptr),
127 	client (client_)
128 {
129 	state = static_cast<cAttackJob::eAJStates> (message.popInt16());
130 	counter = message.popInt16();
131 	targetPosition = message.popPosition();
132 	attackPoints = message.popInt16();
133 	muzzleType = message.popInt16();
134 	attackMode = message.popInt16();
135 	aggressorPosition = message.popPosition();
136 	aggressorPlayerNr = message.popInt16();
137 	fireDir = message.popInt16();
138 	aggressorID = message.popInt32();
139 
140 	cUnit* aggressor = client->getUnitFromID (aggressorID);
141 	if (aggressor)
142 		aggressor->setAttacking (true);
143 
144 	if (aggressor)
145 		Log.write (" Client: Received AttackJob. Aggressor: " + aggressor->getDisplayName() + " (ID: " + iToStr (aggressor->iID) + ") at (" + iToStr (aggressorPosition.x()) + "," + iToStr (aggressorPosition.y()) + "). Target: (" + iToStr (targetPosition.x()) + "," + iToStr (targetPosition.y()) + ").", cLog::eLOG_TYPE_NET_DEBUG);
146 	else
147 		Log.write (" Client: Received AttackJob. Aggressor: instance not present on client (ID: " + iToStr (aggressorID) + ") at(" + iToStr (aggressorPosition.x()) + ", " + iToStr (aggressorPosition.y()) + ").Target: (" + iToStr (targetPosition.x()) + ", " + iToStr (targetPosition.y()) + ").", cLog::eLOG_TYPE_NET_DEBUG);
148 
149 	lockTarget();
150 
151 }
152 
~cAttackJob()153 cAttackJob::~cAttackJob()
154 {
155 	// unlock targets in case they were locked at the beginning of the attack, but are not hit by the impact
156 	// for example a plane flies on the target field and takes the shot in place of the original plane
157 	for (auto unitId : lockedTargets)
158 	{
159 		cUnit* unit;
160 		if (server)
161 			unit = server->getUnitFromID (unitId);
162 		else
163 			unit = client->getUnitFromID (unitId);
164 
165 		if (unit)
166 			unit->setIsBeeinAttacked (false);
167 	}
168 }
169 
serialize() const170 std::unique_ptr<cNetMessage> cAttackJob::serialize() const
171 {
172 	auto message = std::make_unique<cNetMessage> (GAME_EV_ATTACKJOB);
173 	message->pushInt32 (aggressorID);
174 	message->pushInt16 (fireDir);
175 	message->pushInt16 (aggressorPlayerNr);
176 	message->pushPosition (aggressorPosition);
177 	message->pushInt16 (attackMode);
178 	message->pushInt16 (muzzleType);
179 	message->pushInt16 (attackPoints);
180 	message->pushPosition (targetPosition);
181 	message->pushInt16 (counter);
182 	message->pushInt16 (state);
183 
184 	return message;
185 }
186 
187 
run()188 void cAttackJob::run()
189 {
190 	if (counter > 0)
191 	{
192 		counter--;
193 	}
194 
195 	switch (state)
196 	{
197 		case S_ROTATING:
198 		{
199 			cUnit* aggressor = getAggressor();
200 			if (aggressor && (counter % ROTATION_SPEED) == 0)
201 				aggressor->rotateTo (fireDir);
202 
203 			if (counter == 0)
204 			{
205 				fire();
206 				state = S_FIRING;
207 			}
208 			break;
209 		}
210 		case S_FIRING:
211 			if (counter == 0)
212 			{
213 				bool destroyed = impact();
214 				if (destroyed)
215 				{
216 					counter = DESTROY_DELAY;
217 					state = S_EXPLODING;
218 				}
219 				else
220 				{
221 					state = S_FINISHED;
222 				}
223 			}
224 			break;
225 		case S_EXPLODING:
226 			if (counter == 0)
227 			{
228 				destroyTarget();
229 				state = S_FINISHED;
230 			}
231 		case S_FINISHED:
232 		default:
233 			break;
234 	}
235 }
236 
finished() const237 bool cAttackJob::finished() const
238 {
239 	return state == S_FINISHED;
240 }
241 
242 //---------------------------------------------
243 // private functions
244 
calcFireDir()245 int cAttackJob::calcFireDir()
246 {
247 	auto dx = (float) (targetPosition.x() - aggressorPosition.x());
248 	auto dy = (float) - (targetPosition.y() - aggressorPosition.y());
249 	auto r = std::sqrt (dx * dx + dy * dy);
250 
251 	int fireDir = getAggressor()->dir;
252 	if (r <= 0.001f)
253 	{
254 		// do not rotate aggressor
255 	}
256 	else
257 	{
258 		// 360 / (2 * PI) = 57.29577951f;
259 		dx /= r;
260 		dy /= r;
261 		r = asinf (dx) * 57.29577951f;
262 		if (dy >= 0)
263 		{
264 			if (r < 0)
265 				r += 360;
266 		}
267 		else
268 			r = 180 - r;
269 
270 		if (r >= 337.5f || r <= 22.5f) fireDir = 0;
271 		else if (r >= 22.5f && r <= 67.5f) fireDir = 1;
272 		else if (r >= 67.5f && r <= 112.5f) fireDir = 2;
273 		else if (r >= 112.5f && r <= 157.5f) fireDir = 3;
274 		else if (r >= 157.5f && r <= 202.5f) fireDir = 4;
275 		else if (r >= 202.5f && r <= 247.5f) fireDir = 5;
276 		else if (r >= 247.5f && r <= 292.5f) fireDir = 6;
277 		else if (r >= 292.5f && r <= 337.5f) fireDir = 7;
278 	}
279 
280 	return fireDir;
281 }
282 
calcTimeForRotation()283 int cAttackJob::calcTimeForRotation()
284 {
285 	int diff = abs (getAggressor()->dir - fireDir);
286 	if (diff > 4) diff = 8 - diff;
287 
288 	return diff * ROTATION_SPEED;
289 }
290 
getAggressor()291 cUnit* cAttackJob::getAggressor()
292 {
293 	if (server)
294 		return server->getUnitFromID (aggressorID);
295 	else
296 		return client->getUnitFromID (aggressorID);
297 }
298 
lockTarget()299 void cAttackJob::lockTarget()
300 {
301 	cPlayer* player = client ? client->getPlayerFromNumber (aggressorPlayerNr) : &server->getPlayerFromNumber (aggressorPlayerNr);
302 	cMap&    map = client ? *client->getMap() : *server->Map;
303 
304 	int range = 0;
305 	if (muzzleType == sUnitData::MUZZLE_TYPE_ROCKET_CLUSTER)
306 		range = 2;
307 
308 	for (int x = -range; x <= range; x++)
309 	{
310 		for (int y = -range; y <= range; y++)
311 		{
312 			if (abs (x) + abs (y) <= range && map.isValidPosition (targetPosition + cPosition (x, y)))
313 			{
314 				cUnit* target = selectTarget (targetPosition + cPosition (x, y), attackMode, map, player);
315 				if (target)
316 				{
317 					target->setIsBeeinAttacked (true);
318 					lockedTargets.push_back (target->iID);
319 					Log.write (" AttackJob locked target " + target->getDisplayName() + " (ID: " + iToStr (target->iID) + ") at (" + iToStr (targetPosition.x() + x) + "," + iToStr (targetPosition.y() + y) + ")", cLog::eLOG_TYPE_NET_DEBUG);
320 				}
321 			}
322 		}
323 	}
324 }
325 
fire()326 void cAttackJob::fire()
327 {
328 	cUnit* aggressor = getAggressor();
329 
330 	//update data
331 	if (aggressor)
332 	{
333 		aggressor->data.setShots (aggressor->data.getShots() - 1);
334 		aggressor->data.setAmmo (aggressor->data.getAmmo() - 1);
335 		if (aggressor->isAVehicle() && aggressor->data.canDriveAndFire == false)
336 			aggressor->data.setSpeed (aggressor->data.getSpeed() - (int) (((float) aggressor->data.getSpeedMax()) / aggressor->data.getShotsMax()));
337 	}
338 
339 	//set timer for next state
340 	auto muzzle = createMuzzleFx (aggressor);
341 	if (muzzle)
342 		counter = muzzle->getLength() + IMPACT_DELAY;
343 
344 	//play muzzle flash / fire rocket
345 	if (client)
346 	{
347 		if (muzzle)
348 			client->addFx (std::move (muzzle), aggressor != nullptr);
349 	}
350 
351 	//make explosive mines explode
352 	if (aggressor && aggressor->data.explodesOnContact && aggressorPosition == targetPosition)
353 	{
354 		if (client)
355 		{
356 			cMap&    map = client ? *client->getMap() : *server->Map;
357 			if (map.isWaterOrCoast (aggressor->getPosition()))
358 			{
359 				client->addFx (std::make_unique<cFxExploWater> (aggressor->getPosition() * 64 + cPosition (32, 32)));
360 			}
361 			else
362 			{
363 				client->addFx (std::make_unique<cFxExploSmall> (aggressor->getPosition() * 64 + cPosition (32, 32)));
364 			}
365 			client->deleteUnit (aggressor);
366 		}
367 		else
368 		{
369 			server->deleteUnit (aggressor, false);
370 		}
371 	}
372 
373 
374 
375 }
376 
createMuzzleFx(cUnit * aggressor)377 std::unique_ptr<cFx> cAttackJob::createMuzzleFx (cUnit* aggressor)
378 {
379 	//TODO: this shouldn't be in the attackjob class. But since
380 	//the attackjobs doesn't always have an instance of the unit,
381 	//it stays here for now
382 
383 	sID id;
384 	if (aggressor)
385 		id = aggressor->data.ID;
386 
387 	cPosition offset (0, 0);
388 	switch (muzzleType)
389 	{
390 		case sUnitData::MUZZLE_TYPE_BIG:
391 			switch (fireDir)
392 			{
393 				case 0:
394 					offset.y() = -40;
395 					break;
396 				case 1:
397 					offset.x() = 32;
398 					offset.y() = -32;
399 					break;
400 				case 2:
401 					offset.x() = 40;
402 					break;
403 				case 3:
404 					offset.x() = 32;
405 					offset.y() = 32;
406 					break;
407 				case 4:
408 					offset.y() = 40;
409 					break;
410 				case 5:
411 					offset.x() = -32;
412 					offset.y() = 32;
413 					break;
414 				case 6:
415 					offset.x() = -40;
416 					break;
417 				case 7:
418 					offset.x() = -32;
419 					offset.y() = -32;
420 					break;
421 			}
422 			return std::make_unique<cFxMuzzleBig> (aggressorPosition * 64 + offset, fireDir, id);
423 
424 		case sUnitData::MUZZLE_TYPE_SMALL:
425 			return std::make_unique<cFxMuzzleSmall> (aggressorPosition * 64, fireDir, id);
426 
427 		case sUnitData::MUZZLE_TYPE_ROCKET:
428 		case sUnitData::MUZZLE_TYPE_ROCKET_CLUSTER:
429 			return std::make_unique<cFxRocket> (aggressorPosition * 64 + cPosition (32, 32), targetPosition * 64 + cPosition (32, 32), fireDir, false, id);
430 
431 		case sUnitData::MUZZLE_TYPE_MED:
432 		case sUnitData::MUZZLE_TYPE_MED_LONG:
433 			switch (fireDir)
434 			{
435 				case 0:
436 					offset.y() = -20;
437 					break;
438 				case 1:
439 					offset.x() = 12;
440 					offset.y() = -12;
441 					break;
442 				case 2:
443 					offset.x() = 20;
444 					break;
445 				case 3:
446 					offset.x() = 12;
447 					offset.y() = 12;
448 					break;
449 				case 4:
450 					offset.y() = 20;
451 					break;
452 				case 5:
453 					offset.x() = -12;
454 					offset.y() = 12;
455 					break;
456 				case 6:
457 					offset.x() = -20;
458 					break;
459 				case 7:
460 					offset.x() = -12;
461 					offset.y() = -12;
462 					break;
463 			}
464 			if (muzzleType == sUnitData::MUZZLE_TYPE_MED)
465 				return std::make_unique<cFxMuzzleMed> (aggressorPosition * 64 + offset, fireDir, id);
466 			else
467 				return std::make_unique<cFxMuzzleMedLong> (aggressorPosition * 64 + offset, fireDir, id);
468 
469 		case sUnitData::MUZZLE_TYPE_TORPEDO:
470 			return std::make_unique<cFxRocket> (aggressorPosition * 64 + cPosition (32, 32), targetPosition * 64 + cPosition (32, 32), fireDir, true, id);
471 		case sUnitData::MUZZLE_TYPE_SNIPER:
472 		//TODO: sniper has no animation?!?
473 		default:
474 			return nullptr;
475 	}
476 }
477 
impact()478 bool cAttackJob::impact()
479 {
480 	bool destroyed = false;
481 	if (muzzleType == sUnitData::MUZZLE_TYPE_ROCKET_CLUSTER)
482 		destroyed = impactCluster();
483 	else
484 		destroyed = impactSingle (targetPosition);
485 
486 	return destroyed;
487 }
488 
impactCluster()489 bool cAttackJob::impactCluster()
490 {
491 	const int clusterDamage = attackPoints;
492 	bool destroyed = false;
493 	std::vector<cUnit*> targets;
494 
495 	//full damage
496 	destroyed = destroyed || impactSingle (targetPosition, &targets);
497 
498 	// 3/4 damage
499 	attackPoints = (clusterDamage * 3) / 4;
500 	destroyed = destroyed || impactSingle (targetPosition + cPosition (-1, 0), &targets);
501 	destroyed = destroyed || impactSingle (targetPosition + cPosition (+1, 0), &targets);
502 	destroyed = destroyed || impactSingle (targetPosition + cPosition (0, -1), &targets);
503 	destroyed = destroyed || impactSingle (targetPosition + cPosition (0, +1), &targets);
504 
505 	// 1/2 damage
506 	attackPoints = clusterDamage / 2;
507 	destroyed = destroyed || impactSingle (targetPosition + cPosition (+1, +1), &targets);
508 	destroyed = destroyed || impactSingle (targetPosition + cPosition (+1, -1), &targets);
509 	destroyed = destroyed || impactSingle (targetPosition + cPosition (-1, +1), &targets);
510 	destroyed = destroyed || impactSingle (targetPosition + cPosition (-1, -1), &targets);
511 
512 	// 1/3 damage
513 	attackPoints = clusterDamage / 3;
514 	destroyed = destroyed || impactSingle (targetPosition + cPosition (-2, 0), &targets);
515 	destroyed = destroyed || impactSingle (targetPosition + cPosition (+2, 0), &targets);
516 	destroyed = destroyed || impactSingle (targetPosition + cPosition (0, -2), &targets);
517 	destroyed = destroyed || impactSingle (targetPosition + cPosition (0, +2), &targets);
518 
519 	return destroyed;
520 }
521 
impactSingle(const cPosition & position,std::vector<cUnit * > * avoidTargets)522 bool cAttackJob::impactSingle (const cPosition& position, std::vector<cUnit*>* avoidTargets)
523 {
524 	//select target
525 	cPlayer* player = client ? client->getPlayerFromNumber (aggressorPlayerNr) : &server->getPlayerFromNumber (aggressorPlayerNr);
526 	cMap&    map    = client ? *client->getMap() : *server->Map;
527 
528 	if (!map.isValidPosition (position))
529 		return false;
530 
531 	cUnit* target = selectTarget (position, attackMode, map, player);
532 
533 	//check list of units that will be ignored as target.
534 	//Used to prevent, that cluster attacks hit the same unit multible times
535 	if (avoidTargets)
536 	{
537 		for (auto unit : *avoidTargets)
538 		{
539 			if (unit == target)
540 				return false;
541 		}
542 		avoidTargets->push_back (target);
543 	}
544 
545 	cPosition offset (0, 0);
546 	if (target && target->isAVehicle())
547 	{
548 		offset = static_cast<cVehicle*> (target)->getMovementOffset();
549 	}
550 
551 	bool destroyed = false;
552 	std::string name;
553 	sID unitID;
554 
555 	// if taget is a stealth unit, make it visible on all clients
556 	if (server && target && target->data.isStealthOn != TERRAIN_NONE)
557 	{
558 		for (const auto& player : server->playerList)
559 		{
560 			if (target->getOwner() == player.get()) continue;
561 			if (!player->canSeeAnyAreaUnder (*target)) continue;
562 
563 			target->setDetectedByPlayer (*server, player.get());
564 		}
565 	}
566 
567 	//make impact on target
568 	if (target)
569 	{
570 		target->data.setHitpoints (target->calcHealth (attackPoints));
571 		target->setHasBeenAttacked (true);
572 		target->setIsBeeinAttacked (false);
573 
574 		name = target->getDisplayName();
575 		unitID = target->data.ID;
576 
577 		if (target->data.getHitpoints() <= 0)
578 		{
579 			target->setIsBeeinAttacked (true);
580 			destroyed = true;
581 			destroyedTargets.push_back (target->iID);
582 			if (client)
583 			{
584 				if (target->isAVehicle())
585 					client->addDestroyFx (*static_cast<cVehicle*> (target));
586 				else
587 					client->addDestroyFx (*static_cast<cBuilding*> (target));
588 			}
589 		}
590 	}
591 
592 	if (!destroyed && client)
593 	{
594 		bool playSound = client->getActivePlayer().canSeeAt (targetPosition);
595 		bool targetHit = target != nullptr;
596 		bool bigTarget = false;
597 		if (target)
598 			bigTarget = target->data.isBig;
599 		client->addFx (std::make_unique<cFxHit> (position * 64 + offset + cPosition (32, 32), targetHit, bigTarget), playSound);
600 	}
601 
602 	auto aggressor = getAggressor();
603 	if (aggressor)
604 		aggressor->setAttacking (false);
605 
606 	//make message
607 	if (target)
608 	{
609 		if (destroyed)
610 		{
611 			target->getOwner()->addSavedReport (std::make_unique<cSavedReportDestroyed> (*target));
612 		}
613 		else
614 		{
615 			target->getOwner()->addSavedReport (std::make_unique<cSavedReportAttacked> (*target));
616 		}
617 	}
618 
619 	if (target)
620 		Log.write (std::string (server ? " Server: " : " Client: ") + "AttackJob Impact. Target: " + target->getDisplayName() + " (ID: " + iToStr (target->iID) + ") at (" + iToStr (targetPosition.x()) + "," + iToStr (targetPosition.y()) + "), Remaining HP: " + iToStr (target->data.getHitpoints()), cLog::eLOG_TYPE_NET_DEBUG);
621 	else
622 		Log.write (std::string (server ? " Server: " : " Client: ") + " AttackJob Impact. Target: none (" + iToStr (targetPosition.x()) + "," + iToStr (targetPosition.y()) + ")", cLog::eLOG_TYPE_NET_DEBUG);
623 
624 	if (server)
625 	{
626 		// check whether a following sentry mode attack is possible
627 		if (target && target->isAVehicle() && !destroyed)
628 			static_cast<cVehicle*> (target)->InSentryRange (*server);
629 
630 		// check whether the aggressor is in sentry range
631 		if (aggressor && aggressor->isAVehicle())
632 			static_cast<cVehicle*> (aggressor)->InSentryRange (*server);
633 	}
634 
635 	return destroyed;
636 }
637 
destroyTarget()638 void cAttackJob::destroyTarget()
639 {
640 	// destroy unit is only called on server, because it sends
641 	// all nessesary net messages to update the client
642 	if (server)
643 	{
644 		for (auto targetId : destroyedTargets)
645 		{
646 			cUnit* unit = server->getUnitFromID (targetId);
647 			if (unit)
648 			{
649 				Log.write (" Server: AttackJob destroyed unit " + unit->getDisplayName() + " (ID: " + iToStr (unit->iID) + ") at (" + iToStr (unit->getPosition().x()) + "," + iToStr (unit->getPosition().y()) + ")", cLog::eLOG_TYPE_NET_DEBUG);
650 				server->destroyUnit (*unit);
651 			}
652 		}
653 	}
654 }
655