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