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