1 /*
2  *  This file is part of Dune Legacy.
3  *
4  *  Dune Legacy is free software: you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation, either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  Dune Legacy is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with Dune Legacy.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include <units/Harvester.h>
19 
20 #include <globals.h>
21 
22 #include <FileClasses/GFXManager.h>
23 #include <House.h>
24 #include <Game.h>
25 #include <Map.h>
26 #include <Explosion.h>
27 #include <SoundPlayer.h>
28 #include <ScreenBorder.h>
29 
30 #include <players/HumanPlayer.h>
31 #include <players/QuantBot.h>
32 
33 #include <structures/Refinery.h>
34 
35 #include <misc/draw_util.h>
36 
37 #include <algorithm>
38 
39 /* how often is the same sandframe redrawn */
40 #define HARVESTERDELAY 30
41 
42 /* how often to change harvester position while harvesting */
43 #define RANDOMHARVESTMOVE 500
44 
45 /* how much is the harvester movement slowed down when full  */
46 #define MAXIMUMHARVESTERSLOWDOWN FixPt(0,4)
47 
48 /* number spice output frames - 1 */
49 #define LASTSANDFRAME 2
50 
Harvester(House * newOwner)51 Harvester::Harvester(House* newOwner) : TrackedUnit(newOwner)
52 {
53     Harvester::init();
54 
55     setHealth(getMaxHealth());
56 
57     spice = 0;
58     harvestingMode = false;
59     returningToRefinery = false;
60     spiceCheckCounter = 0;
61 
62     attackMode = GUARD;
63 }
64 
Harvester(InputStream & stream)65 Harvester::Harvester(InputStream& stream) : TrackedUnit(stream)
66 {
67     Harvester::init();
68 
69     harvestingMode = stream.readBool();
70     returningToRefinery = stream.readBool();
71     spice = stream.readFixPoint();
72     spiceCheckCounter = stream.readUint32();
73 }
74 
init()75 void Harvester::init()
76 {
77     itemID = Unit_Harvester;
78     owner->incrementUnits(itemID);
79 
80     canAttackStuff = false;
81 
82     graphicID = ObjPic_Harvester;
83     graphic = pGFXManager->getObjPic(graphicID,getOwner()->getHouseID());
84 
85     numImagesX = NUM_ANGLES;
86     numImagesY = 1;
87 }
88 
~Harvester()89 Harvester::~Harvester()
90 {
91 
92 }
93 
save(OutputStream & stream) const94 void Harvester::save(OutputStream& stream) const
95 {
96     TrackedUnit::save(stream);
97     stream.writeBool(harvestingMode);
98     stream.writeBool(returningToRefinery);
99     stream.writeFixPoint(spice);
100     stream.writeUint32(spiceCheckCounter);
101 }
102 
blitToScreen()103 void Harvester::blitToScreen()
104 {
105     int x = screenborder->world2screenX(realX);
106     int y = screenborder->world2screenY(realY);
107 
108     SDL_Texture* pUnitGraphic = graphic[currentZoomlevel];
109     SDL_Rect source = calcSpriteSourceRect(pUnitGraphic, drawnAngle, numImagesX);
110     SDL_Rect dest = calcSpriteDrawingRect( pUnitGraphic, x, y, numImagesX, 1, HAlign::Center, VAlign::Center);
111 
112     SDL_RenderCopy(renderer, pUnitGraphic, &source, &dest);
113 
114     if(isHarvesting() == true) {
115 
116         const Coord harvesterSandOffset[] = {   Coord(-56, 4),
117                                                 Coord(-28, 20),
118                                                 Coord(0, 24),
119                                                 Coord(28, 20),
120                                                 Coord(56, 4),
121                                                 Coord(40, -24),
122                                                 Coord(0, -36),
123                                                 Coord(-36, -24)
124                                             };
125 
126 
127         SDL_Texture** sand = pGFXManager->getObjPic(ObjPic_Harvester_Sand,getOwner()->getHouseID());
128         SDL_Texture* pSandGraphic = sand[currentZoomlevel];
129 
130         int frame = ((currentGame->getGameCycleCount() + (getObjectID() * 10)) / HARVESTERDELAY) % (2*LASTSANDFRAME);
131         if(frame > LASTSANDFRAME) {
132             frame -= LASTSANDFRAME;
133         }
134 
135         SDL_Rect sandSource = calcSpriteSourceRect(pSandGraphic, drawnAngle, NUM_ANGLES, frame, LASTSANDFRAME+1);
136         SDL_Rect sandDest = calcSpriteDrawingRect(  pSandGraphic,
137                                                     screenborder->world2screenX(realX + harvesterSandOffset[drawnAngle].x),
138                                                     screenborder->world2screenY(realY + harvesterSandOffset[drawnAngle].y),
139                                                     NUM_ANGLES, LASTSANDFRAME+1,
140                                                     HAlign::Center, VAlign::Center);
141 
142         SDL_RenderCopy(renderer, pSandGraphic, &sandSource, &sandDest);
143     }
144 
145     if(isBadlyDamaged()) {
146         drawSmoke(x, y);
147     }
148 }
149 
checkPos()150 void Harvester::checkPos()
151 {
152     TrackedUnit::checkPos();
153 
154     if(attackMode == STOP) {
155         harvestingMode = false;
156 
157         if(getOwner()->isAI()){
158             doSetAttackMode(HARVEST);
159         } /*The AI doesn't like STOP*/
160     }
161 
162     if(active)  {
163         if (returningToRefinery) {
164             if (target && (target.getObjPointer() != nullptr) && (target.getObjPointer()->getItemID() == Structure_Refinery)) {
165                 Refinery* pRefinery = static_cast<Refinery*>(target.getObjPointer());
166                 Tile* pTile = currentGameMap->getTile(location);
167                 ObjectBase *pObject = pTile->getGroundObject();
168 
169                 if( justStoppedMoving
170                     && (pObject != nullptr)
171                     && (pObject->getObjectID() == target.getObjectID()) )
172                 {
173                     if(pRefinery->isFree()) {
174                         awaitingPickup = false;
175                         setReturned();
176                     } else {
177                         // the repair yard is already in use by some other unit => move out
178                         Coord newDestination = currentGameMap->findDeploySpot(this, target.getObjPointer()->getLocation(), currentGame->randomGen, getLocation(), pRefinery->getStructureSize());
179                         doMove2Pos(newDestination, true);
180                         requestCarryall();
181                     }
182                 } else if(!awaitingPickup && owner->hasCarryalls() && pRefinery->isFree() && blockDistance(location, pRefinery->getClosestPoint(location)) >= MIN_CARRYALL_LIFT_DISTANCE) {
183                     requestCarryall();
184                 }
185 
186 
187             } else {
188                 int leastNumBookings = std::numeric_limits<int>::max(); //huge amount so refinery couldn't possibly compete with any refinery num bookings
189                 FixPoint closestLeastBookedRefineryDistance = FixPt32_MAX;
190                 Refinery* pBestRefinery = nullptr;
191 
192                 for(StructureBase* pStructure : structureList) {
193                     if((pStructure->getItemID() == Structure_Refinery) && (pStructure->getOwner() == owner)) {
194                         Refinery* pRefinery = static_cast<Refinery*>(pStructure);
195                         Coord closestPoint = pRefinery->getClosestPoint(location);
196                         FixPoint refineryDistance = blockDistance(location, closestPoint);
197 
198                         if (pRefinery->getNumBookings() < leastNumBookings) {
199                             leastNumBookings = pRefinery->getNumBookings();
200                             closestLeastBookedRefineryDistance = refineryDistance;
201                             pBestRefinery = pRefinery;
202                         } else if (pRefinery->getNumBookings() == leastNumBookings) {
203                             if (refineryDistance < closestLeastBookedRefineryDistance) {
204                                 closestLeastBookedRefineryDistance = refineryDistance;
205                                 pBestRefinery = pRefinery;
206                             }
207                         }
208                     }
209                 }
210 
211                 if (pBestRefinery) {
212                     doMove2Object(pBestRefinery);
213                     pBestRefinery->startAnimate();
214                 } else {
215                     setDestination(location);
216                 }
217             }
218         } else if (harvestingMode && !hasBookedCarrier() && destination.isValid() && (blockDistance(location, destination) >= MIN_CARRYALL_LIFT_DISTANCE)) {
219             requestCarryall();
220         } else if(respondable && !harvestingMode && attackMode != STOP) {
221             if(spiceCheckCounter == 0) {
222                 // Find harvest location nearest to our base
223                 Coord newDestination;
224                 if(currentGameMap->findSpice(newDestination, guardPoint)) {
225                     setDestination(newDestination);
226                     setGuardPoint(newDestination);
227                     harvestingMode = true;
228                 } else {
229                     setDestination(location);
230                     setGuardPoint(location);
231                     harvestingMode = false;
232                 }
233                 spiceCheckCounter = 100;
234             } else {
235                 spiceCheckCounter--;
236             }
237         }
238     }
239 }
240 
deploy(const Coord & newLocation)241 void Harvester::deploy(const Coord& newLocation)
242 {
243     if(currentGameMap->tileExists(newLocation)) {
244         TrackedUnit::deploy(newLocation);
245         if(spice == 0) {
246             Coord newDestination;
247             if((attackMode != STOP) && currentGameMap->findSpice(newDestination, guardPoint)) {
248                 harvestingMode = true;
249                 setDestination(newDestination);
250                 setGuardPoint(newDestination);
251 
252             } else {
253                 harvestingMode = false;
254             }
255         }
256     }
257 }
258 
destroy()259 void Harvester::destroy()
260 {
261     if(currentGameMap->tileExists(location) && isVisible()) {
262         int xpos = location.x;
263         int ypos = location.y;
264 
265         if(currentGameMap->tileExists(xpos,ypos)) {
266             FixPoint spiceSpreaded = spice * FixPt(0,75);
267             int availableSandPos = 0;
268 
269             int circleRadius = lround(spice / 210);
270 
271             /* how many regions have sand */
272             for(int i = -circleRadius; i <= circleRadius; i++) {
273                 for(int j = -circleRadius; j <= circleRadius; j++) {
274                     if(currentGameMap->tileExists(xpos + i, ypos + j)
275                         && (distanceFrom(xpos, ypos, xpos + i, ypos + j) + FixPt(0,0005) <= circleRadius))
276                     {
277                         Tile *pTile = currentGameMap->getTile(xpos + i, ypos + j);
278                         if((pTile != nullptr) & ((pTile->isSand()) || (pTile->isSpice()) )) {
279                             availableSandPos++;
280                         }
281                     }
282                 }
283             }
284 
285             /* now we can spread spice */
286             for(int i = -circleRadius; i <= circleRadius; i++) {
287                 for(int j = -circleRadius; j <= circleRadius; j++) {
288                     if(currentGameMap->tileExists(xpos + i, ypos + j)
289                         && (distanceFrom(xpos, ypos, xpos + i, ypos + j) + FixPt(0,0005)  <= circleRadius))
290                     {
291                         Tile *pTile = currentGameMap->getTile(xpos + i, ypos + j);
292                         if((pTile != nullptr) & ((pTile->isSand()) || (pTile->isSpice()) )) {
293                             pTile->setSpice(pTile->getSpice() + spiceSpreaded / availableSandPos);
294                         }
295                     }
296                 }
297             }
298         }
299 
300         setTarget(nullptr);
301 
302         Coord realPos(lround(realX), lround(realY));
303         Uint32 explosionID = currentGame->randomGen.getRandOf(2,Explosion_Medium1, Explosion_Medium2);
304         currentGame->getExplosionList().push_back(new Explosion(explosionID, realPos, owner->getHouseID()));
305 
306         if(isVisible(getOwner()->getTeam())) {
307             screenborder->shakeScreen(18);
308             soundPlayer->playSoundAt(Sound_ExplosionLarge,location);
309         }
310     }
311 
312     TrackedUnit::destroy();
313 }
314 
drawSelectionBox()315 void Harvester::drawSelectionBox()
316 {
317     SDL_Texture* selectionBox = nullptr;
318 
319     switch(currentZoomlevel) {
320         case 0:     selectionBox = pGFXManager->getUIGraphic(UI_SelectionBox_Zoomlevel0);   break;
321         case 1:     selectionBox = pGFXManager->getUIGraphic(UI_SelectionBox_Zoomlevel1);   break;
322         case 2:
323         default:    selectionBox = pGFXManager->getUIGraphic(UI_SelectionBox_Zoomlevel2);   break;
324     }
325 
326     SDL_Rect dest = calcDrawingRect(selectionBox, screenborder->world2screenX(realX), screenborder->world2screenY(realY), HAlign::Center, VAlign::Center);
327     SDL_RenderCopy(renderer, selectionBox, nullptr, &dest);
328 
329     for(int i=1;i<=currentZoomlevel+1;i++) {
330         renderDrawHLine(renderer, dest.x+1, dest.y-i, dest.x+1 + (lround((getHealth()/getMaxHealth())*(getWidth(selectionBox)-3))), getHealthColor());
331     }
332 
333     if((getOwner() == pLocalHouse) && (spice > 0)) {
334         for(int i=1;i<=currentZoomlevel+1;i++) {
335             renderDrawHLine(renderer, dest.x+1, dest.y-i-(currentZoomlevel+1), dest.x+1 + (lround(((spice)/HARVESTERMAXSPICE)*(getWidth(selectionBox)-3))), COLOR_ORANGE);
336         }
337     }
338 }
339 
handleDamage(int damage,Uint32 damagerID,House * damagerOwner)340 void Harvester::handleDamage(int damage, Uint32 damagerID, House* damagerOwner)
341 {
342     TrackedUnit::handleDamage(damage, damagerID, damagerOwner);
343 
344     ObjectBase* damager = currentGame->getObjectManager().getObject(damagerID);
345 
346     if(!target && !forced && damager && canAttack(damager) && (attackMode != STOP)) {
347         setTarget(damager);
348     }
349 }
350 
handleReturnClick()351 void Harvester::handleReturnClick() {
352     currentGame->getCommandManager().addCommand(Command(pLocalPlayer->getPlayerID(), CMD_HARVESTER_RETURN,objectID));
353 }
354 
doReturn()355 void Harvester::doReturn()
356 {
357     if(!returningToRefinery && isActive()) {
358         returningToRefinery = true;
359         harvestingMode = false;
360 
361         if(getAttackMode() == STOP) {
362             setGuardPoint(Coord::Invalid());
363         }
364     }
365 }
366 
setAmountOfSpice(FixPoint newSpice)367 void Harvester::setAmountOfSpice(FixPoint newSpice)
368 {
369     if((newSpice >= 0) && (newSpice <= HARVESTERMAXSPICE)) {
370         spice = newSpice;
371     }
372 }
373 
setDestination(int newX,int newY)374 void Harvester::setDestination(int newX, int newY)
375 {
376     TrackedUnit::setDestination(newX, newY);
377 
378     harvestingMode =  (attackMode != STOP) && (currentGameMap->tileExists(newX, newY) && currentGameMap->getTile(newX,newY)->hasSpice());
379 }
380 
setTarget(const ObjectBase * newTarget)381 void Harvester::setTarget(const ObjectBase* newTarget)
382 {
383     if(returningToRefinery && target && (target.getObjPointer()!= nullptr)
384         && (target.getObjPointer()->getItemID() == Structure_Refinery))
385     {
386         static_cast<Refinery*>(target.getObjPointer())->unBook();
387         returningToRefinery = false;
388     }
389 
390     TrackedUnit::setTarget(newTarget);
391 
392     if(target && (target.getObjPointer() != nullptr)
393         && (target.getObjPointer()->getOwner() == getOwner())
394         && (target.getObjPointer()->getItemID() == Structure_Refinery))
395     {
396         static_cast<Refinery*>(target.getObjPointer())->book();
397         returningToRefinery = true;
398     }
399 
400 }
401 
setReturned()402 void Harvester::setReturned()
403 {
404     if(selected) {
405         removeFromSelectionLists();
406     }
407 
408     currentGameMap->removeObjectFromMap(getObjectID());
409 
410     static_cast<Refinery*>(target.getObjPointer())->assignHarvester(this);
411 
412     returningToRefinery = false;
413     moving = false;
414     respondable = false;
415     setActive(false);
416 
417     setLocation(INVALID_POS, INVALID_POS);
418     setVisible(VIS_ALL, false);
419 }
420 
move()421 void Harvester::move()
422 {
423     TrackedUnit::move();
424 
425     if(active && !moving && !justStoppedMoving) {
426         if(harvestingMode) {
427 
428             if(location == destination) {
429                 if(spice < HARVESTERMAXSPICE) {
430 
431                     Tile* tile = currentGameMap->getTile(location);
432 
433                     if(tile->hasSpice()) {
434 
435                         int beforeTileType = tile->getType();
436                         spice += tile->harvestSpice();
437                         int afterTileType = tile->getType();
438 
439                         if(beforeTileType != afterTileType) {
440                             currentGameMap->spiceRemoved(location);
441                             if(!currentGameMap->findSpice(destination, location)) {
442                                 doReturn();
443                             } else {
444                                 doMove2Pos(destination, false);
445                             }
446                         }
447                     } else if (!currentGameMap->findSpice(destination, location)) {
448                         if(spice > 0) {
449                             doReturn();
450                         }
451                     } else {
452                         doMove2Pos(destination, false);
453                     }
454                 } else {
455                     doReturn();
456                 }
457             }
458         }
459     }
460 }
461 
isHarvesting() const462 bool Harvester::isHarvesting() const {
463     return  harvestingMode
464             && (spice < HARVESTERMAXSPICE)
465             && (blockDistance(location, destination) <= FixPt_SQRT2)
466             && currentGameMap->tileExists(location) && currentGameMap->getTile(location)->hasSpice();
467 }
468 
canAttack(const ObjectBase * object) const469 bool Harvester::canAttack(const ObjectBase* object) const
470 {
471     return((object != nullptr)
472             && object->isInfantry()
473             && (object->getOwner()->getTeam() != owner->getTeam())
474             && object->isVisible(getOwner()->getTeam()));
475 }
476 
extractSpice(FixPoint extractionSpeed)477 FixPoint Harvester::extractSpice(FixPoint extractionSpeed)
478 {
479     FixPoint oldSpice = spice;
480 
481     if((spice - extractionSpeed) >= 0) {
482         spice -= extractionSpeed;
483     } else {
484         spice = 0;
485     }
486 
487     return (oldSpice - spice);
488 }
489 
setSpeeds()490 void Harvester::setSpeeds()
491 {
492     FixPoint speed = getMaxSpeed();
493 
494     if(isBadlyDamaged()) {
495         speed *= HEAVILYDAMAGEDSPEEDMULTIPLIER;
496     }
497 
498     FixPoint percentFull = spice/HARVESTERMAXSPICE;
499     speed = speed * (1 - MAXIMUMHARVESTERSLOWDOWN*percentFull);
500 
501     switch(drawnAngle){
502         case LEFT:      xSpeed = -speed;                    ySpeed = 0;         break;
503         case LEFTUP:    xSpeed = -speed*DIAGONALSPEEDCONST; ySpeed = xSpeed;    break;
504         case UP:        xSpeed = 0;                         ySpeed = -speed;    break;
505         case RIGHTUP:   xSpeed = speed*DIAGONALSPEEDCONST;  ySpeed = -xSpeed;   break;
506         case RIGHT:     xSpeed = speed;                     ySpeed = 0;         break;
507         case RIGHTDOWN: xSpeed = speed*DIAGONALSPEEDCONST;  ySpeed = xSpeed;    break;
508         case DOWN:      xSpeed = 0;                         ySpeed = speed;     break;
509         case LEFTDOWN:  xSpeed = -speed*DIAGONALSPEEDCONST; ySpeed = -xSpeed;   break;
510     }
511 }
512