1 /*
2 * Copyright 2010-2014 OpenXcom Developers.
3 *
4 * This file is part of OpenXcom.
5 *
6 * OpenXcom is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * OpenXcom is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with OpenXcom. If not, see <http://www.gnu.org/licenses/>.
18 */
19 #define _USE_MATH_DEFINES
20 #include <assert.h>
21 #include <cmath>
22 #include <climits>
23 #include <set>
24 #include <functional>
25 #include "TileEngine.h"
26 #include <SDL.h>
27 #include "BattleAIState.h"
28 #include "AlienBAIState.h"
29 #include "UnitTurnBState.h"
30 #include "Map.h"
31 #include "Camera.h"
32 #include "../Savegame/SavedGame.h"
33 #include "../Savegame/SavedBattleGame.h"
34 #include "ExplosionBState.h"
35 #include "../Savegame/Tile.h"
36 #include "../Savegame/BattleItem.h"
37 #include "../Savegame/BattleUnit.h"
38 #include "../Savegame/Soldier.h"
39 #include "../Engine/RNG.h"
40 #include "BattlescapeState.h"
41 #include "../Ruleset/MapDataSet.h"
42 #include "../Ruleset/MapData.h"
43 #include "../Ruleset/Unit.h"
44 #include "../Ruleset/RuleSoldier.h"
45 #include "../Ruleset/Armor.h"
46 #include "../Ruleset/Ruleset.h"
47 #include "../Resource/ResourcePack.h"
48 #include "Pathfinding.h"
49 #include "../Engine/Options.h"
50 #include "ProjectileFlyBState.h"
51 #include "../Engine/Logger.h"
52 #include "../fmath.h"
53
54 namespace OpenXcom
55 {
56
57 const int TileEngine::heightFromCenter[11] = {0,-2,+2,-4,+4,-6,+6,-8,+8,-12,+12};
58
59 /**
60 * Sets up a TileEngine.
61 * @param save Pointer to SavedBattleGame object.
62 * @param voxelData List of voxel data.
63 */
TileEngine(SavedBattleGame * save,std::vector<Uint16> * voxelData)64 TileEngine::TileEngine(SavedBattleGame *save, std::vector<Uint16> *voxelData) : _save(save), _voxelData(voxelData), _personalLighting(true)
65 {
66 }
67
68 /**
69 * Deletes the TileEngine.
70 */
~TileEngine()71 TileEngine::~TileEngine()
72 {
73
74 }
75
76 /**
77 * Calculates sun shading for the whole terrain.
78 */
calculateSunShading()79 void TileEngine::calculateSunShading()
80 {
81 const int layer = 0; // Ambient lighting layer.
82
83 for (int i = 0; i < _save->getMapSizeXYZ(); ++i)
84 {
85 _save->getTiles()[i]->resetLight(layer);
86 calculateSunShading(_save->getTiles()[i]);
87 }
88 }
89
90 /**
91 * Calculates sun shading for 1 tile. Sun comes from above and is blocked by floors or objects.
92 * @param tile The tile to calculate sun shading for.
93 */
calculateSunShading(Tile * tile)94 void TileEngine::calculateSunShading(Tile *tile)
95 {
96 const int layer = 0; // Ambient lighting layer.
97
98 int power = 15 - _save->getGlobalShade();
99
100 // At night/dusk sun isn't dropping shades blocked by roofs
101 if (_save->getGlobalShade() <= 4)
102 {
103 if (verticalBlockage(_save->getTile(Position(tile->getPosition().x, tile->getPosition().y, _save->getMapSizeZ() - 1)), tile, DT_NONE))
104 {
105 power -= 2;
106 }
107 }
108
109 tile->addLight(power, layer);
110 }
111
112 /**
113 * Recalculates lighting for the terrain: objects,items,fire.
114 */
calculateTerrainLighting()115 void TileEngine::calculateTerrainLighting()
116 {
117 const int layer = 1; // Static lighting layer.
118 const int fireLightPower = 15; // amount of light a fire generates
119
120 // reset all light to 0 first
121 for (int i = 0; i < _save->getMapSizeXYZ(); ++i)
122 {
123 _save->getTiles()[i]->resetLight(layer);
124 }
125
126 // add lighting of terrain
127 for (int i = 0; i < _save->getMapSizeXYZ(); ++i)
128 {
129 // only floors and objects can light up
130 if (_save->getTiles()[i]->getMapData(MapData::O_FLOOR)
131 && _save->getTiles()[i]->getMapData(MapData::O_FLOOR)->getLightSource())
132 {
133 addLight(_save->getTiles()[i]->getPosition(), _save->getTiles()[i]->getMapData(MapData::O_FLOOR)->getLightSource(), layer);
134 }
135 if (_save->getTiles()[i]->getMapData(MapData::O_OBJECT)
136 && _save->getTiles()[i]->getMapData(MapData::O_OBJECT)->getLightSource())
137 {
138 addLight(_save->getTiles()[i]->getPosition(), _save->getTiles()[i]->getMapData(MapData::O_OBJECT)->getLightSource(), layer);
139 }
140
141 // fires
142 if (_save->getTiles()[i]->getFire())
143 {
144 addLight(_save->getTiles()[i]->getPosition(), fireLightPower, layer);
145 }
146
147 for (std::vector<BattleItem*>::iterator it = _save->getTiles()[i]->getInventory()->begin(); it != _save->getTiles()[i]->getInventory()->end(); ++it)
148 {
149 if ((*it)->getRules()->getBattleType() == BT_FLARE)
150 {
151 addLight(_save->getTiles()[i]->getPosition(), (*it)->getRules()->getPower(), layer);
152 }
153 }
154
155 }
156
157 }
158
159 /**
160 * Recalculates lighting for the units.
161 */
calculateUnitLighting()162 void TileEngine::calculateUnitLighting()
163 {
164 const int layer = 2; // Dynamic lighting layer.
165 const int personalLightPower = 15; // amount of light a unit generates
166 const int fireLightPower = 15; // amount of light a fire generates
167
168 // reset all light to 0 first
169 for (int i = 0; i < _save->getMapSizeXYZ(); ++i)
170 {
171 _save->getTiles()[i]->resetLight(layer);
172 }
173
174 for (std::vector<BattleUnit*>::iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
175 {
176 // add lighting of soldiers
177 if (_personalLighting && (*i)->getFaction() == FACTION_PLAYER && !(*i)->isOut())
178 {
179 addLight((*i)->getPosition(), personalLightPower, layer);
180 }
181 // add lighting of units on fire
182 if ((*i)->getFire())
183 {
184 addLight((*i)->getPosition(), fireLightPower, layer);
185 }
186 }
187 }
188
189 /**
190 * Adds circular light pattern starting from center and losing power with distance travelled.
191 * @param center Center.
192 * @param power Power.
193 * @param layer Light is separated in 3 layers: Ambient, Static and Dynamic.
194 */
addLight(const Position & center,int power,int layer)195 void TileEngine::addLight(const Position ¢er, int power, int layer)
196 {
197 // only loop through the positive quadrant.
198 for (int x = 0; x <= power; ++x)
199 {
200 for (int y = 0; y <= power; ++y)
201 {
202 for (int z = 0; z < _save->getMapSizeZ(); z++)
203 {
204 int distance = (int)Round(sqrt(float(x*x + y*y)));
205
206 if (_save->getTile(Position(center.x + x,center.y + y, z)))
207 _save->getTile(Position(center.x + x,center.y + y, z))->addLight(power - distance, layer);
208
209 if (_save->getTile(Position(center.x - x,center.y - y, z)))
210 _save->getTile(Position(center.x - x,center.y - y, z))->addLight(power - distance, layer);
211
212 if (_save->getTile(Position(center.x - x,center.y + y, z)))
213 _save->getTile(Position(center.x - x,center.y + y, z))->addLight(power - distance, layer);
214
215 if (_save->getTile(Position(center.x + x,center.y - y, z)))
216 _save->getTile(Position(center.x + x,center.y - y, z))->addLight(power - distance, layer);
217 }
218 }
219 }
220 }
221
222 /**
223 * Calculates line of sight of a soldier.
224 * @param unit Unit to check line of sight of.
225 * @return True when new aliens are spotted.
226 */
calculateFOV(BattleUnit * unit)227 bool TileEngine::calculateFOV(BattleUnit *unit)
228 {
229 size_t oldNumVisibleUnits = unit->getUnitsSpottedThisTurn().size();
230 Position center = unit->getPosition();
231 Position test;
232 int direction;
233 bool swap;
234 std::vector<Position> _trajectory;
235 if (Options::strafe && (unit->getTurretType() > -1)) {
236 direction = unit->getTurretDirection();
237 }
238 else
239 {
240 direction = unit->getDirection();
241 }
242 swap = (direction==0 || direction==4);
243 int signX[8] = { +1, +1, +1, +1, -1, -1, -1, -1 };
244 int signY[8] = { -1, -1, -1, +1, +1, +1, -1, -1 };
245 int y1, y2;
246
247 unit->clearVisibleUnits();
248 unit->clearVisibleTiles();
249
250 if (unit->isOut())
251 return false;
252 Position pos = unit->getPosition();
253
254 if ((unit->getHeight() + unit->getFloatHeight() + -_save->getTile(unit->getPosition())->getTerrainLevel()) >= 24 + 4)
255 {
256 ++pos.z;
257 }
258 for (int x = 0; x <= MAX_VIEW_DISTANCE; ++x)
259 {
260 if (direction%2)
261 {
262 y1 = 0;
263 y2 = MAX_VIEW_DISTANCE;
264 }
265 else
266 {
267 y1 = -x;
268 y2 = x;
269 }
270 for (int y = y1; y <= y2; ++y)
271 {
272 for (int z = 0; z < _save->getMapSizeZ(); z++)
273 {
274 const int distanceSqr = x*x + y*y;
275 test.z = z;
276 if (distanceSqr <= MAX_VIEW_DISTANCE*MAX_VIEW_DISTANCE)
277 {
278 test.x = center.x + signX[direction]*(swap?y:x);
279 test.y = center.y + signY[direction]*(swap?x:y);
280 if (_save->getTile(test))
281 {
282 BattleUnit *visibleUnit = _save->getTile(test)->getUnit();
283 if (visibleUnit && !visibleUnit->isOut() && visible(unit, _save->getTile(test)))
284 {
285 if (unit->getFaction() == FACTION_PLAYER)
286 {
287 visibleUnit->getTile()->setVisible(+1);
288 visibleUnit->setVisible(true);
289 }
290 if ((visibleUnit->getFaction() == FACTION_HOSTILE && unit->getFaction() == FACTION_PLAYER)
291 || (visibleUnit->getFaction() != FACTION_HOSTILE && unit->getFaction() == FACTION_HOSTILE))
292 {
293 unit->addToVisibleUnits(visibleUnit);
294 unit->addToVisibleTiles(visibleUnit->getTile());
295
296 if (unit->getFaction() == FACTION_HOSTILE && visibleUnit->getFaction() != FACTION_HOSTILE)
297 {
298 visibleUnit->setTurnsSinceSpotted(0);
299 }
300 }
301 }
302
303 if (unit->getFaction() == FACTION_PLAYER)
304 {
305 // this sets tiles to discovered if they are in LOS - tile visibility is not calculated in voxelspace but in tilespace
306 // large units have "4 pair of eyes"
307 int size = unit->getArmor()->getSize();
308 for (int xo = 0; xo < size; xo++)
309 {
310 for (int yo = 0; yo < size; yo++)
311 {
312 Position poso = pos + Position(xo,yo,0);
313 _trajectory.clear();
314 int tst = calculateLine(poso, test, true, &_trajectory, unit, false);
315 size_t tsize = _trajectory.size();
316 if (tst>127) --tsize; //last tile is blocked thus must be cropped
317 for (size_t i = 0; i < tsize; i++)
318 {
319 Position posi = _trajectory.at(i);
320 //mark every tile of line as visible (as in original)
321 //this is needed because of bresenham narrow stroke.
322 _save->getTile(posi)->setVisible(+1);
323 _save->getTile(posi)->setDiscovered(true, 2);
324 // walls to the east or south of a visible tile, we see that too
325 Tile* t = _save->getTile(Position(posi.x + 1, posi.y, posi.z));
326 if (t) t->setDiscovered(true, 0);
327 t = _save->getTile(Position(posi.x, posi.y + 1, posi.z));
328 if (t) t->setDiscovered(true, 1);
329 }
330
331 }
332 }
333 }
334 }
335 }
336 }
337 }
338 }
339
340 // we only react when there are at least the same amount of visible units as before AND the checksum is different
341 // this way we stop if there are the same amount of visible units, but a different unit is seen
342 // or we stop if there are more visible units seen
343 if (unit->getUnitsSpottedThisTurn().size() > oldNumVisibleUnits && !unit->getVisibleUnits()->empty())
344 {
345 return true;
346 }
347
348 return false;
349
350 }
351
352 /**
353 * Gets the origin voxel of a unit's eyesight (from just one eye or something? Why is it x+7??
354 * @param currentUnit The watcher.
355 * @return Approximately an eyeball voxel.
356 */
getSightOriginVoxel(BattleUnit * currentUnit)357 Position TileEngine::getSightOriginVoxel(BattleUnit *currentUnit)
358 {
359 // determine the origin and target voxels for the raytrace
360 Position originVoxel;
361 originVoxel = Position((currentUnit->getPosition().x * 16) + 7, (currentUnit->getPosition().y * 16) + 8, currentUnit->getPosition().z*24);
362 originVoxel.z += -_save->getTile(currentUnit->getPosition())->getTerrainLevel();
363 originVoxel.z += currentUnit->getHeight() + currentUnit->getFloatHeight() - 1; //one voxel lower (eye level)
364 Tile *tileAbove = _save->getTile(currentUnit->getPosition() + Position(0,0,1));
365 if (currentUnit->getArmor()->getSize() > 1)
366 {
367 originVoxel.x += 8;
368 originVoxel.y += 8;
369 originVoxel.z += 1; //topmost voxel
370 }
371 if (originVoxel.z >= (currentUnit->getPosition().z + 1)*24 && (!tileAbove || !tileAbove->hasNoFloor(0)))
372 {
373 while (originVoxel.z >= (currentUnit->getPosition().z + 1)*24)
374 {
375 originVoxel.z--;
376 }
377 }
378
379 return originVoxel;
380 }
381
382 /**
383 * Checks for an opposing unit on this tile.
384 * @param currentUnit The watcher.
385 * @param tile The tile to check for
386 * @return True if visible.
387 */
visible(BattleUnit * currentUnit,Tile * tile)388 bool TileEngine::visible(BattleUnit *currentUnit, Tile *tile)
389 {
390 // if there is no tile or no unit, we can't see it
391 if (!tile || !tile->getUnit())
392 {
393 return false;
394 }
395
396 // aliens can see in the dark, xcom can see at a distance of 9 or less, further if there's enough light.
397 if (currentUnit->getFaction() == FACTION_PLAYER &&
398 distance(currentUnit->getPosition(), tile->getPosition()) > 9 &&
399 tile->getShade() > MAX_DARKNESS_TO_SEE_UNITS)
400 {
401 return false;
402 }
403
404 if (currentUnit->getFaction() == tile->getUnit()->getFaction()) return true; // friendlies are always seen
405
406 Position originVoxel = getSightOriginVoxel(currentUnit);
407
408 bool unitSeen = false;
409 // for large units origin voxel is in the middle
410
411 Position scanVoxel;
412 std::vector<Position> _trajectory;
413 unitSeen = canTargetUnit(&originVoxel, tile, &scanVoxel, currentUnit);
414
415 if (unitSeen)
416 {
417 // now check if we really see it taking into account smoke tiles
418 // initial smoke "density" of a smoke grenade is around 15 per tile
419 // we do density/3 to get the decay of visibility
420 // so in fresh smoke we should only have 4 tiles of visibility
421 // this is traced in voxel space, with smoke affecting visibility every step of the way
422 _trajectory.clear();
423 calculateLine(originVoxel, scanVoxel, true, &_trajectory, currentUnit);
424 Tile *t = _save->getTile(currentUnit->getPosition());
425 size_t visibleDistance = _trajectory.size();
426 for (size_t i = 0; i < _trajectory.size(); i++)
427 {
428 if (t != _save->getTile(Position(_trajectory.at(i).x/16,_trajectory.at(i).y/16, _trajectory.at(i).z/24)))
429 {
430 t = _save->getTile(Position(_trajectory.at(i).x/16,_trajectory.at(i).y/16, _trajectory.at(i).z/24));
431 }
432 if (t->getFire() == 0)
433 {
434 visibleDistance += t->getSmoke() / 3;
435 }
436 if (visibleDistance > MAX_VOXEL_VIEW_DISTANCE)
437 {
438 unitSeen = false;
439 break;
440 }
441 }
442 }
443 return unitSeen;
444 }
445
446 /**
447 * Checks for how exposed unit is for another unit.
448 * @param originVoxel Voxel of trace origin (eye or gun's barrel).
449 * @param tile The tile to check for.
450 * @param excludeUnit Is self (not to hit self).
451 * @param excludeAllBut [Optional] is unit which is the only one to be considered for ray hits.
452 * @return Degree of exposure (as percent).
453 */
checkVoxelExposure(Position * originVoxel,Tile * tile,BattleUnit * excludeUnit,BattleUnit * excludeAllBut)454 int TileEngine::checkVoxelExposure(Position *originVoxel, Tile *tile, BattleUnit *excludeUnit, BattleUnit *excludeAllBut)
455 {
456 Position targetVoxel = Position((tile->getPosition().x * 16) + 7, (tile->getPosition().y * 16) + 8, tile->getPosition().z * 24);
457 Position scanVoxel;
458 std::vector<Position> _trajectory;
459 BattleUnit *otherUnit = tile->getUnit();
460 if (otherUnit == 0) return 0; //no unit in this tile, even if it elevated and appearing in it.
461 if (otherUnit == excludeUnit) return 0; //skip self
462
463 int targetMinHeight = targetVoxel.z - tile->getTerrainLevel();
464 if (otherUnit)
465 targetMinHeight += otherUnit->getFloatHeight();
466
467 // if there is an other unit on target tile, we assume we want to check against this unit's height
468 int heightRange;
469
470 int unitRadius = otherUnit->getLoftemps(); //width == loft in default loftemps set
471 if (otherUnit->getArmor()->getSize() > 1)
472 {
473 unitRadius = 3;
474 }
475
476 // vector manipulation to make scan work in view-space
477 Position relPos = targetVoxel - *originVoxel;
478 float normal = unitRadius/sqrt((float)(relPos.x*relPos.x + relPos.y*relPos.y));
479 int relX = floor(((float)relPos.y)*normal+0.5);
480 int relY = floor(((float)-relPos.x)*normal+0.5);
481
482 int sliceTargets[10]={0,0, relX,relY, -relX,-relY};
483
484 if (!otherUnit->isOut())
485 {
486 heightRange = otherUnit->getHeight();
487 }
488 else
489 {
490 heightRange = 12;
491 }
492
493 int targetMaxHeight=targetMinHeight+heightRange;
494 // scan ray from top to bottom plus different parts of target cylinder
495 int total=0;
496 int visible=0;
497 for (int i = heightRange; i >=0; i-=2)
498 {
499 ++total;
500 scanVoxel.z=targetMinHeight+i;
501 for (int j = 0; j < 2; ++j)
502 {
503 scanVoxel.x=targetVoxel.x + sliceTargets[j*2];
504 scanVoxel.y=targetVoxel.y + sliceTargets[j*2+1];
505 _trajectory.clear();
506 int test = calculateLine(*originVoxel, scanVoxel, false, &_trajectory, excludeUnit, true, false, excludeAllBut);
507 if (test == V_UNIT)
508 {
509 //voxel of hit must be inside of scanned box
510 if (_trajectory.at(0).x/16 == scanVoxel.x/16 &&
511 _trajectory.at(0).y/16 == scanVoxel.y/16 &&
512 _trajectory.at(0).z >= targetMinHeight &&
513 _trajectory.at(0).z <= targetMaxHeight)
514 {
515 ++visible;
516 }
517 }
518 }
519 }
520 return (visible*100)/total;
521 }
522
523 /**
524 * Checks for another unit available for targeting and what particular voxel.
525 * @param originVoxel Voxel of trace origin (eye or gun's barrel).
526 * @param tile The tile to check for.
527 * @param scanVoxel is returned coordinate of hit.
528 * @param excludeUnit is self (not to hit self).
529 * @param potentialUnit is a hypothetical unit to draw a virtual line of fire for AI. if left blank, this function behaves normally.
530 * @return True if the unit can be targetted.
531 */
canTargetUnit(Position * originVoxel,Tile * tile,Position * scanVoxel,BattleUnit * excludeUnit,BattleUnit * potentialUnit)532 bool TileEngine::canTargetUnit(Position *originVoxel, Tile *tile, Position *scanVoxel, BattleUnit *excludeUnit, BattleUnit *potentialUnit)
533 {
534 Position targetVoxel = Position((tile->getPosition().x * 16) + 7, (tile->getPosition().y * 16) + 8, tile->getPosition().z * 24);
535 std::vector<Position> _trajectory;
536 bool hypothetical = potentialUnit != 0;
537 if (potentialUnit == 0)
538 {
539 potentialUnit = tile->getUnit();
540 if (potentialUnit == 0) return false; //no unit in this tile, even if it elevated and appearing in it.
541 }
542
543 if (potentialUnit == excludeUnit) return false; //skip self
544
545 int targetMinHeight = targetVoxel.z - tile->getTerrainLevel();
546 targetMinHeight += potentialUnit->getFloatHeight();
547
548 int targetMaxHeight = targetMinHeight;
549 int targetCenterHeight;
550 // if there is an other unit on target tile, we assume we want to check against this unit's height
551 int heightRange;
552
553 int unitRadius = potentialUnit->getLoftemps(); //width == loft in default loftemps set
554 int targetSize = potentialUnit->getArmor()->getSize() - 1;
555 int xOffset = potentialUnit->getPosition().x - tile->getPosition().x;
556 int yOffset = potentialUnit->getPosition().y - tile->getPosition().y;
557 if (targetSize > 0)
558 {
559 unitRadius = 3;
560 }
561 // vector manipulation to make scan work in view-space
562 Position relPos = targetVoxel - *originVoxel;
563 float normal = unitRadius/sqrt((float)(relPos.x*relPos.x + relPos.y*relPos.y));
564 int relX = floor(((float)relPos.y)*normal+0.5);
565 int relY = floor(((float)-relPos.x)*normal+0.5);
566
567 int sliceTargets[10]={0,0, relX,relY, -relX,-relY, relY,-relX, -relY,relX};
568
569 if (!potentialUnit->isOut())
570 {
571 heightRange = potentialUnit->getHeight();
572 }
573 else
574 {
575 heightRange = 12;
576 }
577
578 targetMaxHeight += heightRange;
579 targetCenterHeight=(targetMaxHeight+targetMinHeight)/2;
580 heightRange/=2;
581 if (heightRange>10) heightRange=10;
582 if (heightRange<=0) heightRange=0;
583
584 // scan ray from top to bottom plus different parts of target cylinder
585 for (int i = 0; i <= heightRange; ++i)
586 {
587 scanVoxel->z=targetCenterHeight+heightFromCenter[i];
588 for (int j = 0; j < 5; ++j)
589 {
590 if (i < (heightRange-1) && j>2) break; //skip unnecessary checks
591 scanVoxel->x=targetVoxel.x + sliceTargets[j*2];
592 scanVoxel->y=targetVoxel.y + sliceTargets[j*2+1];
593 _trajectory.clear();
594 int test = calculateLine(*originVoxel, *scanVoxel, false, &_trajectory, excludeUnit, true, false);
595 if (test == V_UNIT)
596 {
597 for (int x = 0; x <= targetSize; ++x)
598 {
599 for (int y = 0; y <= targetSize; ++y)
600 {
601 //voxel of hit must be inside of scanned box
602 if (_trajectory.at(0).x/16 == (scanVoxel->x/16) + x + xOffset &&
603 _trajectory.at(0).y/16 == (scanVoxel->y/16) + y + yOffset &&
604 _trajectory.at(0).z >= targetMinHeight &&
605 _trajectory.at(0).z <= targetMaxHeight)
606 {
607 return true;
608 }
609 }
610 }
611 }
612 else if (test == V_EMPTY && hypothetical && !_trajectory.empty())
613 {
614 return true;
615 }
616 }
617 }
618 return false;
619 }
620
621 /**
622 * Checks for a tile part available for targeting and what particular voxel.
623 * @param originVoxel Voxel of trace origin (gun's barrel).
624 * @param tile The tile to check for.
625 * @param part Tile part to check for.
626 * @param scanVoxel Is returned coordinate of hit.
627 * @param excludeUnit Is self (not to hit self).
628 * @return True if the tile can be targetted.
629 */
canTargetTile(Position * originVoxel,Tile * tile,int part,Position * scanVoxel,BattleUnit * excludeUnit)630 bool TileEngine::canTargetTile(Position *originVoxel, Tile *tile, int part, Position *scanVoxel, BattleUnit *excludeUnit)
631 {
632 static int sliceObjectSpiral[82] = {8,8, 8,6, 10,6, 10,8, 10,10, 8,10, 6,10, 6,8, 6,6, //first circle
633 8,4, 10,4, 12,4, 12,6, 12,8, 12,10, 12,12, 10,12, 8,12, 6,12, 4,12, 4,10, 4,8, 4,6, 4,4, 6,4, //second circle
634 8,1, 12,1, 15,1, 15,4, 15,8, 15,12, 15,15, 12,15, 8,15, 4,15, 1,15, 1,12, 1,8, 1,4, 1,1, 4,1}; //third circle
635 static int westWallSpiral[14] = {0,7, 0,9, 0,6, 0,11, 0,4, 0,13, 0,2};
636 static int northWallSpiral[14] = {7,0, 9,0, 6,0, 11,0, 4,0, 13,0, 2,0};
637
638 Position targetVoxel = Position((tile->getPosition().x * 16), (tile->getPosition().y * 16), tile->getPosition().z * 24);
639 std::vector<Position> _trajectory;
640
641 int *spiralArray;
642 int spiralCount;
643
644 int minZ, maxZ;
645 bool minZfound = false, maxZfound = false;
646
647 if (part == MapData::O_OBJECT)
648 {
649 spiralArray = sliceObjectSpiral;
650 spiralCount = 41;
651 }
652 else
653 if (part == MapData::O_NORTHWALL)
654 {
655 spiralArray = northWallSpiral;
656 spiralCount = 7;
657 }
658 else
659 if (part == MapData::O_WESTWALL)
660 {
661 spiralArray = westWallSpiral;
662 spiralCount = 7;
663 }
664 else if (part == MapData::O_FLOOR)
665 {
666 spiralArray = sliceObjectSpiral;
667 spiralCount = 41;
668 minZfound = true; minZ=0;
669 maxZfound = true; maxZ=0;
670 }
671 else
672 {
673 return false;
674 }
675
676 // find out height range
677
678 if (!minZfound)
679 {
680 for (int j = 1; j < 12; ++j)
681 {
682 if (minZfound) break;
683 for (int i = 0; i < spiralCount; ++i)
684 {
685 int tX = spiralArray[i*2];
686 int tY = spiralArray[i*2+1];
687 if (voxelCheck(Position(targetVoxel.x + tX, targetVoxel.y + tY, targetVoxel.z + j*2),0,true) == part) //bingo
688 {
689 if (!minZfound)
690 {
691 minZ = j*2;
692 minZfound = true;
693 break;
694 }
695 }
696 }
697 }
698 }
699
700 if (!minZfound) return false;//empty object!!!
701
702 if (!maxZfound)
703 {
704 for (int j = 10; j >= 0; --j)
705 {
706 if (maxZfound) break;
707 for (int i = 0; i < spiralCount; ++i)
708 {
709 int tX = spiralArray[i*2];
710 int tY = spiralArray[i*2+1];
711 if (voxelCheck(Position(targetVoxel.x + tX, targetVoxel.y + tY, targetVoxel.z + j*2),0,true) == part) //bingo
712 {
713 if (!maxZfound)
714 {
715 maxZ = j*2;
716 maxZfound = true;
717 break;
718 }
719 }
720 }
721 }
722 }
723
724 if (!maxZfound) return false;//it's impossible to get there
725
726 if (minZ > maxZ) minZ = maxZ;
727 int rangeZ = maxZ - minZ;
728 int centerZ = (maxZ + minZ)/2;
729
730 for (int j = 0; j <= rangeZ; ++j)
731 {
732 scanVoxel->z = targetVoxel.z + centerZ + heightFromCenter[j];
733 for (int i = 0; i < spiralCount; ++i)
734 {
735 scanVoxel->x = targetVoxel.x + spiralArray[i*2];
736 scanVoxel->y = targetVoxel.y + spiralArray[i*2+1];
737 _trajectory.clear();
738 int test = calculateLine(*originVoxel, *scanVoxel, false, &_trajectory, excludeUnit, true);
739 if (test == part) //bingo
740 {
741 if (_trajectory.at(0).x/16 == scanVoxel->x/16 &&
742 _trajectory.at(0).y/16 == scanVoxel->y/16 &&
743 _trajectory.at(0).z/24 == scanVoxel->z/24)
744 {
745 return true;
746 }
747 }
748 }
749 }
750 return false;
751 }
752
753 /**
754 * Calculates line of sight of a soldiers within range of the Position
755 * (used when terrain has changed, which can reveal new parts of terrain or units).
756 * @param position Position of the changed terrain.
757 */
calculateFOV(const Position & position)758 void TileEngine::calculateFOV(const Position &position)
759 {
760 for (std::vector<BattleUnit*>::iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
761 {
762 if (distance(position, (*i)->getPosition()) < MAX_VIEW_DISTANCE)
763 {
764 calculateFOV(*i);
765 }
766 }
767 }
768
769 /**
770 * Checks if a sniper from the opposing faction sees this unit. The unit with the highest reaction score will be compared with the current unit's reaction score.
771 * If it's higher, a shot is fired when enough time units, a weapon and ammo are available.
772 * @param unit The unit to check reaction fire upon.
773 * @return True if reaction fire took place.
774 */
checkReactionFire(BattleUnit * unit)775 bool TileEngine::checkReactionFire(BattleUnit *unit)
776 {
777 // reaction fire only triggered when the actioning unit is of the currently playing side, and is still on the map (alive)
778 if (unit->getFaction() != _save->getSide() || unit->getTile() == 0)
779 {
780 return false;
781 }
782
783 std::vector<BattleUnit *> spotters = getSpottingUnits(unit);
784 bool result = false;
785
786 // not mind controlled, or controlled by the player
787 if (unit->getFaction() == unit->getOriginalFaction()
788 || unit->getFaction() != FACTION_HOSTILE)
789 {
790 // get the first man up to bat.
791 BattleUnit *reactor = getReactor(spotters, unit);
792 // start iterating through the possible reactors until the current unit is the one with the highest score.
793 while (reactor != unit)
794 {
795 if (!tryReactionSnap(reactor, unit))
796 {
797 // can't make a reaction snapshot for whatever reason, boot this guy from the vector.
798 for (std::vector<BattleUnit *>::iterator i = spotters.begin(); i != spotters.end(); ++i)
799 {
800 if (*i == reactor)
801 {
802 spotters.erase(i);
803 break;
804 }
805 }
806 // avoid setting result to true, but carry on, just cause one unit can't react doesn't mean the rest of the units in the vector (if any) can't
807 reactor = getReactor(spotters, unit);
808 continue;
809 }
810 // nice shot, kid. don't get cocky.
811 reactor = getReactor(spotters, unit);
812 result = true;
813 }
814 }
815 return result;
816 }
817
818 /**
819 * Creates a vector of units that can spot this unit.
820 * @param unit The unit to check for spotters of.
821 * @return A vector of units that can see this unit.
822 */
getSpottingUnits(BattleUnit * unit)823 std::vector<BattleUnit *> TileEngine::getSpottingUnits(BattleUnit* unit)
824 {
825 std::vector<BattleUnit*> spotters;
826 Tile *tile = unit->getTile();
827 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
828 {
829 // not dead/unconscious
830 if (!(*i)->isOut() &&
831 // not dying
832 (*i)->getHealth() != 0 &&
833 // not about to pass out
834 (*i)->getStunlevel() < (*i)->getHealth() &&
835 // not a friend
836 (*i)->getFaction() != _save->getSide() &&
837 // closer than 20 tiles
838 distance(unit->getPosition(), (*i)->getPosition()) <= MAX_VIEW_DISTANCE)
839 {
840 Position originVoxel = _save->getTileEngine()->getSightOriginVoxel(*i);
841 originVoxel.z -= 2;
842 Position targetVoxel;
843 AlienBAIState *aggro = dynamic_cast<AlienBAIState*>((*i)->getCurrentAIState());
844 bool gotHit = (aggro != 0 && aggro->getWasHit());
845 // can actually see the target Tile, or we got hit
846 if (((*i)->checkViewSector(unit->getPosition()) || gotHit) &&
847 // can actually target the unit
848 canTargetUnit(&originVoxel, tile, &targetVoxel, *i) &&
849 // can actually see the unit
850 visible(*i, tile))
851 {
852 if ((*i)->getFaction() == FACTION_PLAYER)
853 {
854 unit->setVisible(true);
855 }
856 (*i)->addToVisibleUnits(unit);
857 // no reaction on civilian turn.
858 if (_save->getSide() != FACTION_NEUTRAL &&
859 canMakeSnap(*i, unit))
860 {
861 spotters.push_back(*i);
862 }
863 }
864 }
865 }
866 return spotters;
867 }
868
869 /**
870 * Gets the unit with the highest reaction score from the spotter vector.
871 * @param spotters The vector of spotting units.
872 * @param unit The unit to check scores against.
873 * @return The unit with the highest reactions.
874 */
getReactor(std::vector<BattleUnit * > spotters,BattleUnit * unit)875 BattleUnit* TileEngine::getReactor(std::vector<BattleUnit *> spotters, BattleUnit *unit)
876 {
877 int bestScore = -1;
878 BattleUnit *bu = 0;
879 for (std::vector<BattleUnit *>::iterator i = spotters.begin(); i != spotters.end(); ++i)
880 {
881 if (!(*i)->isOut() &&
882 canMakeSnap(*i, unit) &&
883 (*i)->getReactionScore() > bestScore)
884 {
885 bestScore = (*i)->getReactionScore();
886 bu = *i;
887 }
888 }
889 if (unit->getReactionScore() <= bestScore)
890 {
891 if (bu->getOriginalFaction() == FACTION_PLAYER)
892 {
893 bu->addReactionExp();
894 }
895 }
896 else
897 {
898 bu = unit;
899 }
900 return bu;
901 }
902
903 /**
904 * Checks the validity of a snap shot performed here.
905 * @param unit The unit to check sight from.
906 * @param target The unit to check sight TO.
907 * @return True if the target is valid.
908 */
canMakeSnap(BattleUnit * unit,BattleUnit * target)909 bool TileEngine::canMakeSnap(BattleUnit *unit, BattleUnit *target)
910 {
911 BattleItem *weapon = unit->getMainHandWeapon();
912 // has a weapon
913 if (weapon &&
914 // has a melee weapon and is in melee range
915 ((weapon->getRules()->getBattleType() == BT_MELEE &&
916 validMeleeRange(unit, target, unit->getDirection()) &&
917 unit->getTimeUnits() > unit->getActionTUs(BA_HIT, weapon)) ||
918 // has a gun capable of snap shot with ammo
919 (weapon->getRules()->getBattleType() != BT_MELEE &&
920 weapon->getRules()->getTUSnap() &&
921 weapon->getAmmoItem() &&
922 unit->getTimeUnits() > unit->getActionTUs(BA_SNAPSHOT, weapon))) &&
923 (unit->getOriginalFaction() != FACTION_PLAYER ||
924 _save->getGeoscapeSave()->isResearched(weapon->getRules()->getRequirements())))
925 {
926 return true;
927 }
928 return false;
929 }
930
931 /**
932 * Attempts to perform a reaction snap shot.
933 * @param unit The unit to check sight from.
934 * @param target The unit to check sight TO.
935 * @return True if the action should (theoretically) succeed.
936 */
tryReactionSnap(BattleUnit * unit,BattleUnit * target)937 bool TileEngine::tryReactionSnap(BattleUnit *unit, BattleUnit *target)
938 {
939 BattleAction action;
940 action.cameraPosition = _save->getBattleState()->getMap()->getCamera()->getMapOffset();
941 action.actor = unit;
942 action.weapon = unit->getMainHandWeapon();
943 if (!action.weapon)
944 {
945 return false;
946 }
947 // reaction fire is ALWAYS snap shot.
948 action.type = BA_SNAPSHOT;
949 // unless we're a melee unit.
950 if (action.weapon->getRules()->getBattleType() == BT_MELEE)
951 {
952 action.type = BA_HIT;
953 }
954 action.target = target->getPosition();
955 action.TU = unit->getActionTUs(action.type, action.weapon);
956
957 if (action.weapon->getAmmoItem() && action.weapon->getAmmoItem()->getAmmoQuantity() && unit->getTimeUnits() >= action.TU)
958 {
959 action.targeting = true;
960
961 // hostile units will go into an "aggro" state when they react.
962 if (unit->getFaction() == FACTION_HOSTILE)
963 {
964 AlienBAIState *aggro = dynamic_cast<AlienBAIState*>(unit->getCurrentAIState());
965 if (aggro == 0)
966 {
967 // should not happen, but just in case...
968 aggro = new AlienBAIState(_save, unit, 0);
969 unit->setAIState(aggro);
970 }
971
972 if (action.weapon->getAmmoItem()->getRules()->getExplosionRadius() &&
973 aggro->explosiveEfficacy(action.target, unit, action.weapon->getAmmoItem()->getRules()->getExplosionRadius(), -1) == false)
974 {
975 action.targeting = false;
976 }
977 }
978
979 if (action.targeting && unit->spendTimeUnits(action.TU))
980 {
981 action.TU = 0;
982 _save->getBattleGame()->statePushBack(new UnitTurnBState(_save->getBattleGame(), action));
983 _save->getBattleGame()->statePushBack(new ProjectileFlyBState(_save->getBattleGame(), action));
984 return true;
985 }
986 }
987 return false;
988 }
989
990 /**
991 * Handles bullet/weapon hits.
992 *
993 * A bullet/weapon hits a voxel.
994 * @param center Center of the explosion in voxelspace.
995 * @param power Power of the explosion.
996 * @param type The damage type of the explosion.
997 * @param unit The unit that caused the explosion.
998 * @return The Unit that got hit.
999 */
hit(const Position & center,int power,ItemDamageType type,BattleUnit * unit)1000 BattleUnit *TileEngine::hit(const Position ¢er, int power, ItemDamageType type, BattleUnit *unit)
1001 {
1002 Tile *tile = _save->getTile(Position(center.x/16, center.y/16, center.z/24));
1003 if(!tile)
1004 {
1005 return 0;
1006 }
1007
1008 BattleUnit *bu = tile->getUnit();
1009 int adjustedDamage = 0;
1010 const int part = voxelCheck(center, unit);
1011 if (part >= V_FLOOR && part <= V_OBJECT)
1012 {
1013 // power 25% to 75%
1014 const int rndPower = RNG::generate(power/4, (power*3)/4); //RNG::boxMuller(power, power/6)
1015 if (part == V_OBJECT && _save->getMissionType() == "STR_BASE_DEFENSE")
1016 {
1017 if (rndPower >= tile->getMapData(MapData::O_OBJECT)->getArmor() && tile->getMapData(V_OBJECT)->isBaseModule())
1018 {
1019 _save->getModuleMap()[(center.x/16)/10][(center.y/16)/10].second--;
1020 }
1021 }
1022 if (tile->damage(part, rndPower))
1023 _save->setObjectiveDestroyed(true);
1024 }
1025 else if (part == V_UNIT)
1026 {
1027 int dmgRng = (type == DT_HE || Options::TFTDDamage) ? 50 : 100;
1028 int min = power * (100 - dmgRng) / 100;
1029 int max = power * (100 + dmgRng) / 100;
1030 const int rndPower = RNG::generate(min, max);
1031 int verticaloffset = 0;
1032 if (!bu)
1033 {
1034 // it's possible we have a unit below the actual tile, when he stands on a stairs and sticks his head out to the next tile
1035 Tile *below = _save->getTile(Position(center.x/16, center.y/16, (center.z/24)-1));
1036 if (below)
1037 {
1038 BattleUnit *buBelow = below->getUnit();
1039 if (buBelow)
1040 {
1041 bu = buBelow;
1042 verticaloffset = 24;
1043 }
1044 }
1045 }
1046 if (bu)
1047 {
1048 const int sz = bu->getArmor()->getSize() * 8;
1049 const Position target = bu->getPosition() * Position(16,16,24) + Position(sz,sz, bu->getFloatHeight() - tile->getTerrainLevel());
1050 const Position relative = (center - target) - Position(0,0,verticaloffset);
1051 const int wounds = bu->getFatalWounds();
1052
1053 adjustedDamage = bu->damage(relative, rndPower, type);
1054
1055 // if it's going to bleed to death and it's not a player, give credit for the kill.
1056 if (unit && bu->getFaction() != FACTION_PLAYER && wounds < bu->getFatalWounds())
1057 {
1058 bu->killedBy(unit->getFaction());
1059 }
1060 const int bravery = (110 - bu->getStats()->bravery) / 10;
1061 const int modifier = bu->getFaction() == FACTION_PLAYER ? _save->getMoraleModifier() : 100;
1062 const int morale_loss = 100 * (adjustedDamage * bravery / 10) / modifier;
1063
1064 bu->moraleChange(-morale_loss);
1065
1066 if (bu->getSpecialAbility() == SPECAB_EXPLODEONDEATH && !bu->isOut() && (bu->getHealth() == 0 || bu->getStunlevel() >= bu->getHealth()))
1067 {
1068 if (type != DT_STUN && type != DT_HE)
1069 {
1070 Position p = Position(bu->getPosition().x * 16, bu->getPosition().y * 16, bu->getPosition().z * 24);
1071 _save->getBattleGame()->statePushNext(new ExplosionBState(_save->getBattleGame(), p, 0, bu, 0));
1072 }
1073 }
1074
1075 if (bu->getOriginalFaction() == FACTION_HOSTILE &&
1076 unit &&
1077 unit->getOriginalFaction() == FACTION_PLAYER &&
1078 type != DT_NONE &&
1079 _save->getBattleGame()->getCurrentAction()->type != BA_HIT &&
1080 _save->getBattleGame()->getCurrentAction()->type != BA_STUN)
1081 {
1082 unit->addFiringExp();
1083 }
1084 }
1085 }
1086 applyGravity(tile);
1087 calculateSunShading(); // roofs could have been destroyed
1088 calculateTerrainLighting(); // fires could have been started
1089 calculateFOV(center / Position(16,16,24));
1090 return bu;
1091 }
1092
1093 /**
1094 * Handles explosions.
1095 *
1096 * HE, smoke and fire explodes in a circular pattern on 1 level only. HE however damages floor tiles of the above level. Not the units on it.
1097 * HE destroys an object if its armor is lower than the explosive power, then it's HE blockage is applied for further propagation.
1098 * See http://www.ufopaedia.org/index.php?title=Explosions for more info.
1099 * @param center Center of the explosion in voxelspace.
1100 * @param power Power of the explosion.
1101 * @param type The damage type of the explosion.
1102 * @param maxRadius The maximum radius othe explosion.
1103 * @param unit The unit that caused the explosion.
1104 */
explode(const Position & center,int power,ItemDamageType type,int maxRadius,BattleUnit * unit)1105 void TileEngine::explode(const Position ¢er, int power, ItemDamageType type, int maxRadius, BattleUnit *unit)
1106 {
1107 double centerZ = center.z / 24 + 0.5;
1108 double centerX = center.x / 16 + 0.5;
1109 double centerY = center.y / 16 + 0.5;
1110 int power_, penetration;
1111 std::set<Tile*> tilesAffected;
1112 std::pair<std::set<Tile*>::iterator,bool> ret;
1113
1114 if (type == DT_IN)
1115 {
1116 power /= 2;
1117 }
1118
1119 int exHeight = std::max(0, std::min(3, Options::battleExplosionHeight));
1120 int vertdec = 1000; //default flat explosion
1121
1122 switch (exHeight)
1123 {
1124 case 1:
1125 vertdec = 30;
1126 break;
1127 case 2:
1128 vertdec = 10;
1129 break;
1130 case 3:
1131 vertdec = 5;
1132 }
1133
1134
1135 for (int fi = -90; fi <= 90; fi += 5)
1136 // for (int fi = 0; fi <= 0; fi += 10)
1137 {
1138 // raytrace every 3 degrees makes sure we cover all tiles in a circle.
1139 for (int te = 0; te <= 360; te += 3)
1140 {
1141 double cos_te = cos(te * M_PI / 180.0);
1142 double sin_te = sin(te * M_PI / 180.0);
1143 double sin_fi = sin(fi * M_PI / 180.0);
1144 double cos_fi = cos(fi * M_PI / 180.0);
1145
1146 Tile *origin = _save->getTile(Position(centerX, centerY, centerZ));
1147 double l = 0;
1148 double vx, vy, vz;
1149 int tileX, tileY, tileZ;
1150 power_ = power + 1;
1151 penetration = power_;
1152 while (power_ > 0 && l <= maxRadius)
1153 {
1154 vx = centerX + l * sin_te * cos_fi;
1155 vy = centerY + l * cos_te * cos_fi;
1156 vz = centerZ + l * sin_fi;
1157
1158 tileZ = int(floor(vz));
1159 tileX = int(floor(vx));
1160 tileY = int(floor(vy));
1161
1162 Tile *dest = _save->getTile(Position(tileX, tileY, tileZ));
1163 if (!dest) break; // out of map!
1164
1165
1166 // blockage by terrain is deducted from the explosion power
1167 if (std::abs(l) > 0) // no need to block epicentrum
1168 {
1169 power_ -= 10; // explosive damage decreases by 10 per tile
1170 if (origin->getPosition().z != tileZ) power_ -= vertdec; //3d explosion factor
1171 if (type == DT_IN)
1172 {
1173 int dir;
1174 Pathfinding::vectorToDirection(origin->getPosition() - dest->getPosition(), dir);
1175 if (dir != -1 && dir %2) power_ -= 5; // diagonal movement costs an extra 50% for fire.
1176 }
1177 penetration = power_ - (horizontalBlockage(origin, dest, type) + verticalBlockage(origin, dest, type)) * 2;
1178 }
1179
1180 if (penetration > 0)
1181 {
1182 if (type == DT_HE)
1183 {
1184 // explosives do 1/2 damage to terrain and 1/2 up to 3/2 random damage to units (the halving is handled elsewhere)
1185 dest->setExplosive(power_);
1186 }
1187
1188 ret = tilesAffected.insert(dest); // check if we had this tile already
1189 if (ret.second)
1190 {
1191 int dmgRng = (type == DT_HE || Options::TFTDDamage) ? 50 : 100;
1192 int min = power_ * (100 - dmgRng) / 100;
1193 int max = power_ * (100 + dmgRng) / 100;
1194 BattleUnit *bu = dest->getUnit();
1195 int wounds = 0;
1196 if (bu && unit)
1197 {
1198 wounds = bu->getFatalWounds();
1199 }
1200 switch (type)
1201 {
1202 case DT_STUN:
1203 // power 0 - 200%
1204 if (bu)
1205 {
1206 if (distance(dest->getPosition(), Position(centerX, centerY, centerZ)) < 2)
1207 {
1208 bu->damage(Position(0, 0, 0), RNG::generate(min, max), type);
1209 }
1210 else
1211 {
1212 bu->damage(Position(centerX, centerY, centerZ) - dest->getPosition(), RNG::generate(min, max), type);
1213 }
1214 }
1215 for (std::vector<BattleItem*>::iterator it = dest->getInventory()->begin(); it != dest->getInventory()->end(); ++it)
1216 {
1217 if ((*it)->getUnit())
1218 {
1219 (*it)->getUnit()->damage(Position(0, 0, 0), RNG::generate(min, max), type);
1220 }
1221 }
1222 break;
1223 case DT_HE:
1224 {
1225 // power 50 - 150%
1226 if (bu)
1227 {
1228 if (distance(dest->getPosition(), Position(centerX, centerY, centerZ)) < 2)
1229 {
1230 // ground zero effect is in effect
1231 bu->damage(Position(0, 0, 0), (int)(RNG::generate(min, max)), type);
1232 }
1233 else
1234 {
1235 // directional damage relative to explosion position.
1236 // units above the explosion will be hit in the legs, units lateral to or below will be hit in the torso
1237 bu->damage(Position(centerX, centerY, centerZ + 5) - dest->getPosition(), (int)(RNG::generate(min, max)), type);
1238 }
1239 }
1240 bool done = false;
1241 while (!done)
1242 {
1243 done = dest->getInventory()->empty();
1244 for (std::vector<BattleItem*>::iterator it = dest->getInventory()->begin(); it != dest->getInventory()->end(); )
1245 {
1246 if (power_ > (*it)->getRules()->getArmor())
1247 {
1248 if ((*it)->getUnit() && (*it)->getUnit()->getStatus() == STATUS_UNCONSCIOUS)
1249 (*it)->getUnit()->instaKill();
1250 _save->removeItem((*it));
1251 break;
1252 }
1253 else
1254 {
1255 ++it;
1256 done = it == dest->getInventory()->end();
1257 }
1258 }
1259 }
1260 }
1261 break;
1262
1263 case DT_SMOKE:
1264 // smoke from explosions always stay 6 to 14 turns - power of a smoke grenade is 60
1265 if (dest->getSmoke() < 10 && dest->getTerrainLevel() > -24)
1266 {
1267 dest->setFire(0);
1268 dest->setSmoke(RNG::generate(7, 15));
1269 }
1270 break;
1271
1272 case DT_IN:
1273 if (!dest->isVoid())
1274 {
1275 if (dest->getFire() == 0)
1276 {
1277 dest->setFire(dest->getFuel() + 1);
1278 dest->setSmoke(std::max(1, std::min(15 - (dest->getFlammability() / 10), 12)));
1279 }
1280 if (bu)
1281 {
1282 float resistance = bu->getArmor()->getDamageModifier(DT_IN);
1283 if (resistance > 0.0)
1284 {
1285 bu->damage(Position(0, 0, 12-dest->getTerrainLevel()), RNG::generate(5, 10), DT_IN, true);
1286 int burnTime = RNG::generate(0, int(5 * resistance));
1287 if (bu->getFire() < burnTime)
1288 {
1289 bu->setFire(burnTime); // catch fire and burn
1290 }
1291 }
1292 }
1293 }
1294 break;
1295 default:
1296 break;
1297 }
1298
1299 if (unit && bu && bu->getFaction() != unit->getFaction())
1300 {
1301 unit->addFiringExp();
1302 // if it's going to bleed to death and it's not a player, give credit for the kill.
1303 if (wounds < bu->getFatalWounds() && bu->getFaction() != FACTION_PLAYER)
1304 {
1305 bu->killedBy(unit->getFaction());
1306 }
1307 }
1308
1309 }
1310 }
1311 power_ = penetration;
1312 origin = dest;
1313 l++;
1314 }
1315 }
1316 }
1317 // now detonate the tiles affected with HE
1318
1319 if (type == DT_HE)
1320 {
1321 for (std::set<Tile*>::iterator i = tilesAffected.begin(); i != tilesAffected.end(); ++i)
1322 {
1323 if (detonate(*i))
1324 _save->setObjectiveDestroyed(true);
1325 applyGravity(*i);
1326 Tile *j = _save->getTile((*i)->getPosition() + Position(0,0,1));
1327 if (j)
1328 applyGravity(j);
1329 }
1330 }
1331
1332 calculateSunShading(); // roofs could have been destroyed
1333 calculateTerrainLighting(); // fires could have been started
1334 calculateFOV(center / Position(16,16,24));
1335 }
1336
1337 /**
1338 * Applies the explosive power to the tile parts. This is where the actual destruction takes place.
1339 * Must affect 7 objects (6 box sides and the object inside).
1340 * @param tile Tile affected.
1341 * @return True if the objective was destroyed.
1342 */
detonate(Tile * tile)1343 bool TileEngine::detonate(Tile* tile)
1344 {
1345 int explosive = tile->getExplosive();
1346 tile->setExplosive(0,true);
1347 bool objective = false;
1348 Tile* tiles[7];
1349 static const int parts[7]={0,1,2,0,1,2,3};
1350 Position pos = tile->getPosition();
1351 tiles[0] = _save->getTile(Position(pos.x, pos.y, pos.z+1)); //ceiling
1352 tiles[1] = _save->getTile(Position(pos.x+1, pos.y, pos.z)); //east wall
1353 tiles[2] = _save->getTile(Position(pos.x, pos.y+1, pos.z)); //south wall
1354 tiles[3] = tiles[4] = tiles[5] = tiles[6] = tile;
1355 if (explosive)
1356 {
1357 int remainingPower = explosive;
1358 int flam = tile->getFlammability();
1359 int fuel = tile->getFuel() + 1;
1360 // explosions create smoke which only stays 1 or 2 turns
1361 tile->setSmoke(std::max(1, std::min(tile->getSmoke() + RNG::generate(0,2), 15)));
1362 for (int i = 0; i < 7; ++i)
1363 {
1364 if(tiles[i] && tiles[i]->getMapData(parts[i]))
1365 {
1366 remainingPower = explosive;
1367 while (remainingPower >= 0 && tiles[i]->getMapData(parts[i]))
1368 {
1369 remainingPower -= 2 * tiles[i]->getMapData(parts[i])->getArmor();
1370 if (remainingPower >= 0)
1371 {
1372 int volume = 0;
1373 // get the volume of the object by checking it's loftemps objects.
1374 for (int j=0; j < 12; j++)
1375 {
1376 if (tiles[i]->getMapData(parts[i])->getLoftID(j) != 0)
1377 ++volume;
1378 }
1379
1380 if (i > 3)
1381 {
1382 tiles[i]->setFire(0);
1383 int smoke = RNG::generate(0, (volume / 2) + 2);
1384 smoke += (volume / 2) + 1;
1385 if (smoke > tiles[i]->getSmoke())
1386 {
1387 tiles[i]->setSmoke(std::max(0, std::min(smoke, 15)));
1388 }
1389 }
1390
1391 if (_save->getMissionType() == "STR_BASE_DEFENSE" && i == 6 && tile->getMapData(MapData::O_OBJECT) && tile->getMapData(MapData::O_OBJECT)->isBaseModule())
1392 {
1393 _save->getModuleMap()[tile->getPosition().x/10][tile->getPosition().y/10].second--;
1394 }
1395 if (tiles[i]->destroy(parts[i]))
1396 {
1397 objective = true;
1398 }
1399 if (tiles[i]->getMapData(parts[i]))
1400 {
1401 flam = tiles[i]->getFlammability();
1402 fuel = tiles[i]->getFuel() + 1;
1403 }
1404 }
1405 }
1406 if (i > 3 && 2 * flam < remainingPower && (tile->getMapData(MapData::O_FLOOR) || tile->getMapData(MapData::O_OBJECT)))
1407 {
1408 tile->setFire(fuel);
1409 tile->setSmoke(std::max(1, std::min(15 - (flam / 10), 12)));
1410 }
1411 }
1412 }
1413 }
1414
1415 return objective;
1416 }
1417
1418
1419 /**
1420 * Checks for chained explosions.
1421 *
1422 * Chained explosions are explosions which occur after an explosive map object is destroyed.
1423 * May be due a direct hit, other explosion or fire.
1424 * @return tile on which a explosion occurred
1425 */
checkForTerrainExplosions()1426 Tile *TileEngine::checkForTerrainExplosions()
1427 {
1428 for (int i = 0; i < _save->getMapSizeXYZ(); ++i)
1429 {
1430 if (_save->getTiles()[i]->getExplosive())
1431 {
1432 return _save->getTiles()[i];
1433 }
1434 }
1435 return 0;
1436 }
1437
1438 /**
1439 * Calculates the amount of power that is blocked going from one tile to another on a different level.
1440 * Can cross more than one level. Only floor tiles are taken into account.
1441 * @param startTile The tile where the power starts.
1442 * @param endTile The adjacent tile where the power ends.
1443 * @param type The type of power/damage.
1444 * @return Amount of blockage of this power.
1445 */
verticalBlockage(Tile * startTile,Tile * endTile,ItemDamageType type)1446 int TileEngine::verticalBlockage(Tile *startTile, Tile *endTile, ItemDamageType type)
1447 {
1448 int block = 0;
1449
1450 // safety check
1451 if (startTile == 0 || endTile == 0) return 0;
1452 int direction = endTile->getPosition().z - startTile->getPosition().z;
1453
1454 if (direction == 0 ) return 0;
1455
1456 int x = startTile->getPosition().x;
1457 int y = startTile->getPosition().y;
1458
1459 if (direction < 0) // down
1460 {
1461 for (int z = startTile->getPosition().z; z > endTile->getPosition().z; z--)
1462 {
1463 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_FLOOR, type);
1464 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_OBJECT, type, Pathfinding::DIR_DOWN);
1465 }
1466 if (x != endTile->getPosition().x || y != endTile->getPosition().y)
1467 {
1468 x = endTile->getPosition().x;
1469 y = endTile->getPosition().y;
1470 int z = startTile->getPosition().z;
1471 block += horizontalBlockage(startTile, _save->getTile(Position(x, y, z)), type);
1472 for (; z > endTile->getPosition().z; z--)
1473 {
1474 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_FLOOR, type);
1475 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_OBJECT, type);
1476 }
1477 }
1478 }
1479 else if (direction > 0) // up
1480 {
1481 for (int z = startTile->getPosition().z + 1; z <= endTile->getPosition().z; z++)
1482 {
1483 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_FLOOR, type);
1484 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_OBJECT, type, Pathfinding::DIR_UP);
1485 }
1486 if (x != endTile->getPosition().x || y != endTile->getPosition().y)
1487 {
1488 x = endTile->getPosition().x;
1489 y = endTile->getPosition().y;
1490 int z = startTile->getPosition().z;
1491 block += horizontalBlockage(startTile, _save->getTile(Position(x, y, z)), type);
1492 for (z = startTile->getPosition().z + 1; z <= endTile->getPosition().z; z++)
1493 {
1494 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_FLOOR, type);
1495 block += blockage(_save->getTile(Position(x, y, z)), MapData::O_OBJECT, type);
1496 }
1497 }
1498 }
1499
1500 return block;
1501 }
1502
1503 /**
1504 * Calculates the amount of power that is blocked going from one tile to another on the same level.
1505 * @param startTile The tile where the power starts.
1506 * @param endTile The adjacent tile where the power ends.
1507 * @param type The type of power/damage.
1508 * @return Amount of blockage.
1509 */
horizontalBlockage(Tile * startTile,Tile * endTile,ItemDamageType type)1510 int TileEngine::horizontalBlockage(Tile *startTile, Tile *endTile, ItemDamageType type)
1511 {
1512 static const Position oneTileNorth = Position(0, -1, 0);
1513 static const Position oneTileEast = Position(1, 0, 0);
1514 static const Position oneTileSouth = Position(0, 1, 0);
1515 static const Position oneTileWest = Position(-1, 0, 0);
1516
1517 // safety check
1518 if (startTile == 0 || endTile == 0) return 0;
1519 if (startTile->getPosition().z != endTile->getPosition().z) return 0;
1520
1521 int direction;
1522 Pathfinding::vectorToDirection(endTile->getPosition() - startTile->getPosition(), direction);
1523 if (direction == -1) return 0;
1524 int block = 0;
1525
1526 switch(direction)
1527 {
1528 case 0: // north
1529 block = blockage(startTile, MapData::O_NORTHWALL, type);
1530 break;
1531 case 1: // north east
1532 if (type == DT_NONE) //this is two-way diagonal visiblity check, used in original game
1533 {
1534 block = blockage(startTile, MapData::O_NORTHWALL, type) + blockage(endTile, MapData::O_WESTWALL, type); //up+right
1535 block += blockage(_save->getTile(startTile->getPosition() + oneTileNorth), MapData::O_OBJECT, type, 3);
1536 if (block == 0) break; //this way is opened
1537 block = blockage(_save->getTile(startTile->getPosition() + oneTileEast), MapData::O_NORTHWALL, type)
1538 + blockage(_save->getTile(startTile->getPosition() + oneTileEast), MapData::O_WESTWALL, type); //right+up
1539 block += blockage(_save->getTile(startTile->getPosition() + oneTileEast), MapData::O_OBJECT, type, 7);
1540 }
1541 else
1542 {
1543 block = (blockage(startTile,MapData::O_NORTHWALL, type) + blockage(endTile,MapData::O_WESTWALL, type))/2
1544 + (blockage(_save->getTile(startTile->getPosition() + oneTileEast),MapData::O_WESTWALL, type)
1545 + blockage(_save->getTile(startTile->getPosition() + oneTileEast),MapData::O_NORTHWALL, type))/2;
1546
1547 if (!endTile->getMapData(MapData::O_OBJECT))
1548 {
1549 block += (blockage(_save->getTile(startTile->getPosition() + oneTileEast),MapData::O_OBJECT, type, direction)
1550 + blockage(_save->getTile(startTile->getPosition() + oneTileNorth),MapData::O_OBJECT, type, 4)
1551 + blockage(_save->getTile(startTile->getPosition() + oneTileNorth),MapData::O_OBJECT, type, 2))/2;
1552 }
1553 }
1554 break;
1555 case 2: // east
1556 block = blockage(endTile,MapData::O_WESTWALL, type);
1557 break;
1558 case 3: // south east
1559 if (type == DT_NONE)
1560 {
1561 block = blockage(_save->getTile(startTile->getPosition() + oneTileSouth), MapData::O_NORTHWALL, type)
1562 + blockage(endTile, MapData::O_WESTWALL, type); //down+right
1563 block += blockage(_save->getTile(startTile->getPosition() + oneTileSouth), MapData::O_OBJECT, type, 1);
1564 if (block == 0) break; //this way is opened
1565 block = blockage(_save->getTile(startTile->getPosition() + oneTileEast), MapData::O_WESTWALL, type)
1566 + blockage(endTile, MapData::O_NORTHWALL, type); //right+down
1567 block += blockage(_save->getTile(startTile->getPosition() + oneTileEast), MapData::O_OBJECT, type, 5);
1568 }
1569 else
1570 {
1571 block = (blockage(endTile,MapData::O_WESTWALL, type) + blockage(endTile,MapData::O_NORTHWALL, type))/2
1572 + (blockage(_save->getTile(startTile->getPosition() + oneTileEast),MapData::O_WESTWALL, type)
1573 + blockage(_save->getTile(startTile->getPosition() + oneTileSouth),MapData::O_NORTHWALL, type))/2;
1574
1575 if (!endTile->getMapData(MapData::O_OBJECT))
1576 {
1577 block += (blockage(_save->getTile(startTile->getPosition() + oneTileSouth),MapData::O_OBJECT, type, 2)
1578 + blockage(_save->getTile(startTile->getPosition() + oneTileEast),MapData::O_OBJECT, type, 4))/2;
1579 }
1580 }
1581 break;
1582 case 4: // south
1583 block = blockage(endTile,MapData::O_NORTHWALL, type);
1584 break;
1585 case 5: // south west
1586 if (type == DT_NONE)
1587 {
1588 block = blockage(_save->getTile(startTile->getPosition() + oneTileSouth), MapData::O_NORTHWALL, type)
1589 + blockage(_save->getTile(startTile->getPosition() + oneTileSouth), MapData::O_WESTWALL, type); //down+left
1590 block += blockage(_save->getTile(startTile->getPosition() + oneTileSouth), MapData::O_OBJECT, type, 7);
1591 if (block == 0) break; //this way is opened
1592 block = blockage(startTile, MapData::O_WESTWALL, type) + blockage(endTile, MapData::O_NORTHWALL, type); //left+down
1593 block += blockage(_save->getTile(startTile->getPosition() + oneTileWest), MapData::O_OBJECT, type, 3);
1594 }
1595 else
1596 {
1597 block = (blockage(endTile,MapData::O_NORTHWALL, type) + blockage(startTile,MapData::O_WESTWALL, type))/2
1598 + (blockage(_save->getTile(startTile->getPosition() + oneTileSouth),MapData::O_WESTWALL, type)
1599 + blockage(_save->getTile(startTile->getPosition() + oneTileSouth),MapData::O_NORTHWALL, type))/2;
1600 if (!endTile->getMapData(MapData::O_OBJECT))
1601 {
1602 block += (blockage(_save->getTile(startTile->getPosition() + oneTileSouth),MapData::O_OBJECT, type, direction)
1603 + blockage(_save->getTile(startTile->getPosition() + oneTileWest),MapData::O_OBJECT, type, 2)
1604 + blockage(_save->getTile(startTile->getPosition() + oneTileWest),MapData::O_OBJECT, type, 4))/2;
1605 }
1606 }
1607 break;
1608 case 6: // west
1609 block = blockage(startTile,MapData::O_WESTWALL, type);
1610 break;
1611 case 7: // north west
1612
1613 if (type == DT_NONE)
1614 {
1615 block = blockage(startTile, MapData::O_NORTHWALL, type)
1616 + blockage(_save->getTile(startTile->getPosition() + oneTileNorth), MapData::O_WESTWALL, type); //up+left
1617 block += blockage(_save->getTile(startTile->getPosition() + oneTileNorth), MapData::O_OBJECT, type, 5);
1618 if (block == 0) break; //this way is opened
1619 block = blockage(startTile, MapData::O_WESTWALL, type)
1620 + blockage(_save->getTile(startTile->getPosition() + oneTileWest), MapData::O_NORTHWALL, type); //left+up
1621 block += blockage(_save->getTile(startTile->getPosition() + oneTileWest), MapData::O_OBJECT, type, 1);
1622 }
1623 else
1624 {
1625 block = (blockage(startTile,MapData::O_WESTWALL, type) + blockage(startTile,MapData::O_NORTHWALL, type))/2
1626 + (blockage(_save->getTile(startTile->getPosition() + oneTileNorth),MapData::O_WESTWALL, type)
1627 + blockage(_save->getTile(startTile->getPosition() + oneTileWest),MapData::O_NORTHWALL, type))/2;
1628
1629 if (!endTile->getMapData(MapData::O_OBJECT))
1630 {
1631 block += (blockage(_save->getTile(startTile->getPosition() + oneTileNorth),MapData::O_OBJECT, type, 4)
1632 + blockage(_save->getTile(startTile->getPosition() + oneTileWest),MapData::O_OBJECT, type, 2))/2;
1633 }
1634 }
1635 break;
1636 }
1637
1638 block += blockage(startTile,MapData::O_OBJECT, type, direction, true);
1639 if (type != DT_NONE)
1640 {
1641 direction += 4;
1642 if (direction > 7)
1643 direction -= 8;
1644 block += blockage(endTile,MapData::O_OBJECT, type, direction);
1645 }
1646 else
1647 {
1648 if ( block <= 127 )
1649 {
1650 direction += 4;
1651 if (direction > 7)
1652 direction -= 8;
1653 if (blockage(endTile,MapData::O_OBJECT, type, direction) > 127){
1654 return -1; //hit bigwall, reveal bigwall tile
1655 }
1656 }
1657 }
1658
1659 return block;
1660 }
1661
1662 /**
1663 * Calculates the amount this certain wall or floor-part of the tile blocks.
1664 * @param startTile The tile where the power starts.
1665 * @param part The part of the tile the power needs to go through.
1666 * @param type The type of power/damage.
1667 * @param direction Direction the power travels.
1668 * @return Amount of blockage.
1669 */
blockage(Tile * tile,const int part,ItemDamageType type,int direction,bool checkingFromOrigin)1670 int TileEngine::blockage(Tile *tile, const int part, ItemDamageType type, int direction, bool checkingFromOrigin)
1671 {
1672 int blockage = 0;
1673
1674 if (tile == 0) return 0; // probably outside the map here
1675 if (tile->getMapData(part))
1676 {
1677 bool check = true;
1678 int wall = -1;
1679 if (direction != -1)
1680 {
1681 wall = tile->getMapData(MapData::O_OBJECT)->getBigWall();
1682
1683 if (checkingFromOrigin &&
1684 (wall == Pathfinding::BIGWALLNESW ||
1685 wall == Pathfinding::BIGWALLNWSE))
1686 {
1687 check = false;
1688 }
1689 switch (direction)
1690 {
1691 case 0: // north
1692 if (wall == Pathfinding::BIGWALLWEST ||
1693 wall == Pathfinding::BIGWALLEAST ||
1694 wall == Pathfinding::BIGWALLSOUTH ||
1695 wall == Pathfinding::BIGWALLEASTANDSOUTH)
1696 {
1697 check = false;
1698 }
1699 break;
1700 case 1: // north east
1701 if (wall == Pathfinding::BIGWALLWEST ||
1702 wall == Pathfinding::BIGWALLSOUTH)
1703 {
1704 check = false;
1705 }
1706 break;
1707 case 2: // east
1708 if (wall == Pathfinding::BIGWALLNORTH ||
1709 wall == Pathfinding::BIGWALLSOUTH ||
1710 wall == Pathfinding::BIGWALLWEST)
1711 {
1712 check = false;
1713 }
1714 break;
1715 case 3: // south east
1716 if (wall == Pathfinding::BIGWALLNORTH ||
1717 wall == Pathfinding::BIGWALLWEST)
1718 {
1719 check = false;
1720 }
1721 break;
1722 case 4: // south
1723 if (wall == Pathfinding::BIGWALLWEST ||
1724 wall == Pathfinding::BIGWALLEAST ||
1725 wall == Pathfinding::BIGWALLNORTH)
1726 {
1727 check = false;
1728 }
1729 break;
1730 case 5: // south west
1731 if (wall == Pathfinding::BIGWALLNORTH ||
1732 wall == Pathfinding::BIGWALLEAST)
1733 {
1734 check = false;
1735 }
1736 break;
1737 case 6: // west
1738 if (wall == Pathfinding::BIGWALLNORTH ||
1739 wall == Pathfinding::BIGWALLSOUTH ||
1740 wall == Pathfinding::BIGWALLEAST ||
1741 wall == Pathfinding::BIGWALLEASTANDSOUTH)
1742 {
1743 check = false;
1744 }
1745 break;
1746 case 7: // north west
1747 if (wall == Pathfinding::BIGWALLSOUTH ||
1748 wall == Pathfinding::BIGWALLEAST ||
1749 wall == Pathfinding::BIGWALLEASTANDSOUTH)
1750 {
1751 check = false;
1752 }
1753 break;
1754 case 8: // up
1755 case 9: // down
1756 if (wall != 0 && wall != Pathfinding::BLOCK)
1757 {
1758 check = false;
1759 }
1760 break;
1761 default:
1762 break;
1763 }
1764 }
1765
1766 if (check)
1767 {
1768 // -1 means we have a regular wall, and anything over 0 means we have a bigwall.
1769 if (type == DT_SMOKE && wall != 0 && !tile->isUfoDoorOpen(part))
1770 {
1771 return 256;
1772 }
1773 blockage += tile->getMapData(part)->getBlock(type);
1774 }
1775 }
1776
1777 // open ufo doors are actually still closed behind the scenes
1778 // so a special trick is needed to see if they are open, if they are, they obviously don't block anything
1779 if (tile->isUfoDoorOpen(part))
1780 blockage = 0;
1781
1782 return blockage;
1783 }
1784
1785 /**
1786 * Opens a door (if any) by rightclick, or by walking through it. The unit has to face in the right direction.
1787 * @param unit Unit.
1788 * @param rClick Whether the player right clicked.
1789 * @param dir Direction.
1790 * @return -1 there is no door, you can walk through;
1791 * 0 normal door opened, make a squeaky sound and you can walk through;
1792 * 1 ufo door is starting to open, make a whoosh sound, don't walk through;
1793 * 3 ufo door is still opening, don't walk through it yet. (have patience, futuristic technology...)
1794 * 4 not enough TUs
1795 * 5 would contravene fire reserve
1796 */
unitOpensDoor(BattleUnit * unit,bool rClick,int dir)1797 int TileEngine::unitOpensDoor(BattleUnit *unit, bool rClick, int dir)
1798 {
1799 int door = -1;
1800 int TUCost = 0;
1801 int size = unit->getArmor()->getSize();
1802 int z = unit->getTile()->getTerrainLevel() < -12 ? 1 : 0; // if we're standing on stairs, check the tile above instead.
1803 if (size > 1 && rClick)
1804 return door;
1805 if (dir == -1)
1806 {
1807 dir = unit->getDirection();
1808 }
1809 for (int x = 0; x < size && door == -1; x++)
1810 {
1811 for (int y = 0; y < size && door == -1; y++)
1812 {
1813 std::vector<std::pair<Position, int> > checkPositions;
1814 Tile *tile = _save->getTile(unit->getPosition() + Position(x,y,z));
1815 if (!tile) continue;
1816
1817 switch (dir)
1818 {
1819 case 0: // north
1820 checkPositions.push_back(std::make_pair(Position(0, 0, 0), MapData::O_NORTHWALL)); // origin
1821 if (x != 0)
1822 {
1823 checkPositions.push_back(std::make_pair(Position(0, -1, 0), MapData::O_WESTWALL)); // one tile north
1824 }
1825 break;
1826 case 1: // north east
1827 checkPositions.push_back(std::make_pair(Position(0, 0, 0), MapData::O_NORTHWALL)); // origin
1828 checkPositions.push_back(std::make_pair(Position(1, -1, 0), MapData::O_WESTWALL)); // one tile north-east
1829 if (rClick)
1830 {
1831 checkPositions.push_back(std::make_pair(Position(1, 0, 0), MapData::O_WESTWALL)); // one tile east
1832 checkPositions.push_back(std::make_pair(Position(1, 0, 0), MapData::O_NORTHWALL)); // one tile east
1833 }
1834 break;
1835 case 2: // east
1836 checkPositions.push_back(std::make_pair(Position(1, 0, 0), MapData::O_WESTWALL)); // one tile east
1837 break;
1838 case 3: // south-east
1839 if (!y)
1840 checkPositions.push_back(std::make_pair(Position(1, 1, 0), MapData::O_WESTWALL)); // one tile south-east
1841 if (!x)
1842 checkPositions.push_back(std::make_pair(Position(1, 1, 0), MapData::O_NORTHWALL)); // one tile south-east
1843 if (rClick)
1844 {
1845 checkPositions.push_back(std::make_pair(Position(1, 0, 0), MapData::O_WESTWALL)); // one tile east
1846 checkPositions.push_back(std::make_pair(Position(0, 1, 0), MapData::O_NORTHWALL)); // one tile south
1847 }
1848 break;
1849 case 4: // south
1850 checkPositions.push_back(std::make_pair(Position(0, 1, 0), MapData::O_NORTHWALL)); // one tile south
1851 break;
1852 case 5: // south-west
1853 checkPositions.push_back(std::make_pair(Position(0, 0, 0), MapData::O_WESTWALL)); // origin
1854 checkPositions.push_back(std::make_pair(Position(-1, 1, 0), MapData::O_NORTHWALL)); // one tile south-west
1855 if (rClick)
1856 {
1857 checkPositions.push_back(std::make_pair(Position(0, 1, 0), MapData::O_WESTWALL)); // one tile south
1858 checkPositions.push_back(std::make_pair(Position(0, 1, 0), MapData::O_NORTHWALL)); // one tile south
1859 }
1860 break;
1861 case 6: // west
1862 checkPositions.push_back(std::make_pair(Position(0, 0, 0), MapData::O_WESTWALL)); // origin
1863 if (y != 0)
1864 {
1865 checkPositions.push_back(std::make_pair(Position(-1, 0, 0), MapData::O_NORTHWALL)); // one tile west
1866 }
1867 break;
1868 case 7: // north-west
1869 checkPositions.push_back(std::make_pair(Position(0, 0, 0), MapData::O_WESTWALL)); // origin
1870 checkPositions.push_back(std::make_pair(Position(0, 0, 0), MapData::O_NORTHWALL)); // origin
1871 if (x)
1872 {
1873 checkPositions.push_back(std::make_pair(Position(-1, -1, 0), MapData::O_WESTWALL)); // one tile north
1874 }
1875 if (y)
1876 {
1877 checkPositions.push_back(std::make_pair(Position(-1, -1, 0), MapData::O_NORTHWALL)); // one tile north
1878 }
1879 if (rClick)
1880 {
1881 checkPositions.push_back(std::make_pair(Position(0, -1, 0), MapData::O_WESTWALL)); // one tile north
1882 checkPositions.push_back(std::make_pair(Position(-1, 0, 0), MapData::O_NORTHWALL)); // one tile west
1883 }
1884 break;
1885 default:
1886 break;
1887 }
1888
1889 int part = 0;
1890 for (std::vector<std::pair<Position, int> >::const_iterator i = checkPositions.begin(); i != checkPositions.end() && door == -1; ++i)
1891 {
1892 tile = _save->getTile(unit->getPosition() + Position(x,y,z) + i->first);
1893 if (tile)
1894 {
1895 door = tile->openDoor(i->second, unit, _save->getBattleGame()->getReservedAction());
1896 if (door != -1)
1897 {
1898 part = i->second;
1899 if (door == 1)
1900 {
1901 checkAdjacentDoors(unit->getPosition() + Position(x,y,z) + i->first, i->second);
1902 }
1903 }
1904 }
1905 }
1906 if (door == 0 && rClick)
1907 {
1908 if (part == MapData::O_WESTWALL)
1909 {
1910 part = MapData::O_NORTHWALL;
1911 }
1912 else
1913 {
1914 part = MapData::O_WESTWALL;
1915 }
1916 TUCost = tile->getTUCost(part, unit->getArmor()->getMovementType());
1917 }
1918 else if (door == 1 || door == 4)
1919 {
1920 TUCost = tile->getTUCost(part, unit->getArmor()->getMovementType());
1921 }
1922 }
1923 }
1924
1925 if (TUCost != 0)
1926 {
1927 if (_save->getBattleGame()->checkReservedTU(unit, TUCost))
1928 {
1929 if (unit->spendTimeUnits(TUCost))
1930 {
1931 calculateFOV(unit->getPosition());
1932 // look from the other side (may be need check reaction fire?)
1933 std::vector<BattleUnit*> *vunits = unit->getVisibleUnits();
1934 for (size_t i = 0; i < vunits->size(); ++i)
1935 {
1936 calculateFOV(vunits->at(i));
1937 }
1938 }
1939 else return 4;
1940 }
1941 else return 5;
1942 }
1943
1944 return door;
1945 }
1946
1947 /**
1948 * Opens any doors connected to this part at this position,
1949 * Keeps processing til it hits a non-ufo-door.
1950 * @param pos The starting position
1951 * @param part The part to open, defines which direction to check.
1952 */
checkAdjacentDoors(Position pos,int part)1953 void TileEngine::checkAdjacentDoors(Position pos, int part)
1954 {
1955 Position offset;
1956 bool westSide = (part == 1);
1957 for (int i = 1;; ++i)
1958 {
1959 offset = westSide ? Position(0,i,0):Position(i,0,0);
1960 Tile *tile = _save->getTile(pos + offset);
1961 if (tile && tile->getMapData(part) && tile->getMapData(part)->isUFODoor())
1962 {
1963 tile->openDoor(part);
1964 }
1965 else break;
1966 }
1967 for (int i = -1;; --i)
1968 {
1969 offset = westSide ? Position(0,i,0):Position(i,0,0);
1970 Tile *tile = _save->getTile(pos + offset);
1971 if (tile && tile->getMapData(part) && tile->getMapData(part)->isUFODoor())
1972 {
1973 tile->openDoor(part);
1974 }
1975 else break;
1976 }
1977 }
1978
1979 /**
1980 * Closes ufo doors.
1981 * @return Whether doors are closed.
1982 */
closeUfoDoors()1983 int TileEngine::closeUfoDoors()
1984 {
1985 int doorsclosed = 0;
1986
1987 // prepare a list of tiles on fire/smoke & close any ufo doors
1988 for (int i = 0; i < _save->getMapSizeXYZ(); ++i)
1989 {
1990 if (_save->getTiles()[i]->getUnit() && _save->getTiles()[i]->getUnit()->getArmor()->getSize() > 1)
1991 {
1992 BattleUnit *bu = _save->getTiles()[i]->getUnit();
1993 Tile *tile = _save->getTiles()[i];
1994 Tile *oneTileNorth = _save->getTile(tile->getPosition() + Position(0, -1, 0));
1995 Tile *oneTileWest = _save->getTile(tile->getPosition() + Position(-1, 0, 0));
1996 if ((tile->isUfoDoorOpen(MapData::O_NORTHWALL) && oneTileNorth && oneTileNorth->getUnit() && oneTileNorth->getUnit() == bu) ||
1997 (tile->isUfoDoorOpen(MapData::O_WESTWALL) && oneTileWest && oneTileWest->getUnit() && oneTileWest->getUnit() == bu))
1998 {
1999 continue;
2000 }
2001 }
2002 doorsclosed += _save->getTiles()[i]->closeUfoDoor();
2003 }
2004
2005 return doorsclosed;
2006 }
2007
2008 /**
2009 * Calculates a line trajectory, using bresenham algorithm in 3D.
2010 * @param origin Origin (voxel??).
2011 * @param target Target (also voxel??).
2012 * @param storeTrajectory True will store the whole trajectory - otherwise it just stores the last position.
2013 * @param trajectory A vector of positions in which the trajectory is stored.
2014 * @param excludeUnit Excludes this unit in the collision detection.
2015 * @param doVoxelCheck Check against voxel or tile blocking? (first one for units visibility and line of fire, second one for terrain visibility).
2016 * @param onlyVisible Skip invisible units? used in FPS view.
2017 * @param excludeAllBut [Optional] The only unit to be considered for ray hits.
2018 * @return the objectnumber(0-3) or unit(4) or out of map (5) or -1(hit nothing).
2019 */
calculateLine(const Position & origin,const Position & target,bool storeTrajectory,std::vector<Position> * trajectory,BattleUnit * excludeUnit,bool doVoxelCheck,bool onlyVisible,BattleUnit * excludeAllBut)2020 int TileEngine::calculateLine(const Position& origin, const Position& target, bool storeTrajectory, std::vector<Position> *trajectory, BattleUnit *excludeUnit, bool doVoxelCheck, bool onlyVisible, BattleUnit *excludeAllBut)
2021 {
2022 int x, x0, x1, delta_x, step_x;
2023 int y, y0, y1, delta_y, step_y;
2024 int z, z0, z1, delta_z, step_z;
2025 int swap_xy, swap_xz;
2026 int drift_xy, drift_xz;
2027 int cx, cy, cz;
2028 Position lastPoint(origin);
2029 int result;
2030
2031 //start and end points
2032 x0 = origin.x; x1 = target.x;
2033 y0 = origin.y; y1 = target.y;
2034 z0 = origin.z; z1 = target.z;
2035
2036 //'steep' xy Line, make longest delta x plane
2037 swap_xy = abs(y1 - y0) > abs(x1 - x0);
2038 if (swap_xy)
2039 {
2040 std::swap(x0, y0);
2041 std::swap(x1, y1);
2042 }
2043
2044 //do same for xz
2045 swap_xz = abs(z1 - z0) > abs(x1 - x0);
2046 if (swap_xz)
2047 {
2048 std::swap(x0, z0);
2049 std::swap(x1, z1);
2050 }
2051
2052 //delta is Length in each plane
2053 delta_x = abs(x1 - x0);
2054 delta_y = abs(y1 - y0);
2055 delta_z = abs(z1 - z0);
2056
2057 //drift controls when to step in 'shallow' planes
2058 //starting value keeps Line centred
2059 drift_xy = (delta_x / 2);
2060 drift_xz = (delta_x / 2);
2061
2062 //direction of line
2063 step_x = 1; if (x0 > x1) { step_x = -1; }
2064 step_y = 1; if (y0 > y1) { step_y = -1; }
2065 step_z = 1; if (z0 > z1) { step_z = -1; }
2066
2067 //starting point
2068 y = y0;
2069 z = z0;
2070
2071 //step through longest delta (which we have swapped to x)
2072 for (x = x0; x != (x1+step_x); x += step_x)
2073 {
2074 //copy position
2075 cx = x; cy = y; cz = z;
2076
2077 //unswap (in reverse)
2078 if (swap_xz) std::swap(cx, cz);
2079 if (swap_xy) std::swap(cx, cy);
2080
2081 if (storeTrajectory && trajectory)
2082 {
2083 trajectory->push_back(Position(cx, cy, cz));
2084 }
2085 //passes through this point?
2086 if (doVoxelCheck)
2087 {
2088 result = voxelCheck(Position(cx, cy, cz), excludeUnit, false, onlyVisible, excludeAllBut);
2089 if (result != V_EMPTY)
2090 {
2091 if (trajectory)
2092 { // store the position of impact
2093 trajectory->push_back(Position(cx, cy, cz));
2094 }
2095 return result;
2096 }
2097 }
2098 else
2099 {
2100 int temp_res = verticalBlockage(_save->getTile(lastPoint), _save->getTile(Position(cx, cy, cz)), DT_NONE);
2101 result = horizontalBlockage(_save->getTile(lastPoint), _save->getTile(Position(cx, cy, cz)), DT_NONE);
2102 if (result == -1)
2103 {
2104 if (temp_res > 127)
2105 {
2106 result = 0;
2107 } else {
2108 return result; // We hit a big wall
2109 }
2110 }
2111 result += temp_res;
2112 if (result > 127)
2113 {
2114 return result;
2115 }
2116
2117 lastPoint = Position(cx, cy, cz);
2118 }
2119 //update progress in other planes
2120 drift_xy = drift_xy - delta_y;
2121 drift_xz = drift_xz - delta_z;
2122
2123 //step in y plane
2124 if (drift_xy < 0)
2125 {
2126 y = y + step_y;
2127 drift_xy = drift_xy + delta_x;
2128
2129 //check for xy diagonal intermediate voxel step
2130 if (doVoxelCheck)
2131 {
2132 cx = x; cz = z; cy = y;
2133 if (swap_xz) std::swap(cx, cz);
2134 if (swap_xy) std::swap(cx, cy);
2135 result = voxelCheck(Position(cx, cy, cz), excludeUnit, false, onlyVisible, excludeAllBut);
2136 if (result != V_EMPTY)
2137 {
2138 if (trajectory != 0)
2139 { // store the position of impact
2140 trajectory->push_back(Position(cx, cy, cz));
2141 }
2142 return result;
2143 }
2144 }
2145 }
2146
2147 //same in z
2148 if (drift_xz < 0)
2149 {
2150 z = z + step_z;
2151 drift_xz = drift_xz + delta_x;
2152
2153 //check for xz diagonal intermediate voxel step
2154 if (doVoxelCheck)
2155 {
2156 cx = x; cz = z; cy = y;
2157 if (swap_xz) std::swap(cx, cz);
2158 if (swap_xy) std::swap(cx, cy);
2159 result = voxelCheck(Position(cx, cy, cz), excludeUnit, false, onlyVisible, excludeAllBut);
2160 if (result != V_EMPTY)
2161 {
2162 if (trajectory != 0)
2163 { // store the position of impact
2164 trajectory->push_back(Position(cx, cy, cz));
2165 }
2166 return result;
2167 }
2168 }
2169 }
2170 }
2171
2172 return V_EMPTY;
2173 }
2174
2175 /**
2176 * Calculates a parabola trajectory, used for throwing items.
2177 * @param origin Orign in voxelspace.
2178 * @param target Target in voxelspace.
2179 * @param storeTrajectory True will store the whole trajectory - otherwise it just stores the last position.
2180 * @param trajectory A vector of positions in which the trajectory is stored.
2181 * @param excludeUnit Makes sure the trajectory does not hit the shooter itself.
2182 * @param curvature How high the parabola goes: 1.0 is almost straight throw, 3.0 is a very high throw, to throw over a fence for example.
2183 * @param delta Is the deviation of the angles it should take into account, 0,0,0 is perfection.
2184 * @return The objectnumber(0-3) or unit(4) or out of map (5) or -1(hit nothing).
2185 */
calculateParabola(const Position & origin,const Position & target,bool storeTrajectory,std::vector<Position> * trajectory,BattleUnit * excludeUnit,double curvature,const Position delta)2186 int TileEngine::calculateParabola(const Position& origin, const Position& target, bool storeTrajectory, std::vector<Position> *trajectory, BattleUnit *excludeUnit, double curvature, const Position delta)
2187 {
2188 double ro = sqrt((double)((target.x - origin.x) * (target.x - origin.x) + (target.y - origin.y) * (target.y - origin.y) + (target.z - origin.z) * (target.z - origin.z)));
2189
2190 if (AreSame(ro, 0.0)) return V_EMPTY;//just in case
2191
2192 double fi = acos((double)(target.z - origin.z) / ro);
2193 double te = atan2((double)(target.y - origin.y), (double)(target.x - origin.x));
2194
2195 te += (delta.x / ro) / 2 * M_PI; //horizontal magic value
2196 fi += ((delta.z + delta.y) / ro) / 14 * M_PI * curvature; //another magic value (vertical), to make it in line with fire spread
2197
2198 double zA = sqrt(ro)*curvature;
2199 double zK = 4.0 * zA / ro / ro;
2200
2201 int x = origin.x;
2202 int y = origin.y;
2203 int z = origin.z;
2204 int i = 8;
2205 Position lastPosition = Position(x,y,z);
2206 while (z > 0)
2207 {
2208 x = (int)((double)origin.x + (double)i * cos(te) * sin(fi));
2209 y = (int)((double)origin.y + (double)i * sin(te) * sin(fi));
2210 z = (int)((double)origin.z + (double)i * cos(fi) - zK * ((double)i - ro / 2.0) * ((double)i - ro / 2.0) + zA);
2211 if (storeTrajectory && trajectory)
2212 {
2213 trajectory->push_back(Position(x, y, z));
2214 }
2215 //passes through this point?
2216 Position nextPosition = Position(x,y,z);
2217 int result = calculateLine(lastPosition, nextPosition, false, 0, excludeUnit);
2218 if (result != V_EMPTY)
2219 {
2220 if (lastPosition.z < nextPosition.z)
2221 {
2222 result = V_OUTOFBOUNDS;
2223 }
2224 if (!storeTrajectory && trajectory != 0)
2225 { // store the position of impact
2226 trajectory->push_back(nextPosition);
2227 }
2228 return result;
2229 }
2230 lastPosition = Position(x,y,z);
2231 ++i;
2232 }
2233 if (!storeTrajectory && trajectory != 0)
2234 { // store the position of impact
2235 trajectory->push_back(Position(x, y, z));
2236 }
2237 return V_EMPTY;
2238 }
2239
2240 /**
2241 * Calculates z "grounded" value for a particular voxel (used for projectile shadow).
2242 * @param voxel The voxel to trace down.
2243 * @return z coord of "ground".
2244 */
castedShade(const Position & voxel)2245 int TileEngine::castedShade(const Position& voxel)
2246 {
2247 int zstart = voxel.z;
2248 Position tmpCoord = voxel / Position(16,16,24);
2249 Tile *t = _save->getTile(tmpCoord);
2250 while (t && t->isVoid() && !t->getUnit())
2251 {
2252 zstart = tmpCoord.z* 24;
2253 --tmpCoord.z;
2254 t = _save->getTile(tmpCoord);
2255 }
2256
2257 Position tmpVoxel = voxel;
2258 int z;
2259
2260 for (z = zstart; z>0; z--)
2261 {
2262 tmpVoxel.z = z;
2263 if (voxelCheck(tmpVoxel, 0) != V_EMPTY) break;
2264 }
2265 return z;
2266 }
2267
2268 /**
2269 * Traces voxel visibility.
2270 * @param voxel Voxel coordinates.
2271 * @return True if visible.
2272 */
2273
isVoxelVisible(const Position & voxel)2274 bool TileEngine::isVoxelVisible(const Position& voxel)
2275 {
2276 int zstart = voxel.z+3; // slight Z adjust
2277 if ((zstart/24)!=(voxel.z/24))
2278 return true; // visble!
2279 Position tmpVoxel = voxel;
2280 int zend = (zstart/24)*24 +24;
2281 for (int z = zstart; z<zend; z++)
2282 {
2283 tmpVoxel.z=z;
2284 // only OBJECT can cause additional occlusion (because of any shape)
2285 if (voxelCheck(tmpVoxel, 0) == V_OBJECT) return false;
2286 ++tmpVoxel.x;
2287 if (voxelCheck(tmpVoxel, 0) == V_OBJECT) return false;
2288 ++tmpVoxel.y;
2289 if (voxelCheck(tmpVoxel, 0) == V_OBJECT) return false;
2290 }
2291 return true;
2292 }
2293
2294 /**
2295 * Checks if we hit a voxel.
2296 * @param voxel The voxel to check.
2297 * @param excludeUnit Don't do checks on this unit.
2298 * @param excludeAllUnits Don't do checks on any unit.
2299 * @param onlyVisible Whether to consider only visible units.
2300 * @param excludeAllBut If set, the only unit to be considered for ray hits.
2301 * @return The objectnumber(0-3) or unit(4) or out of map (5) or -1 (hit nothing).
2302 */
voxelCheck(const Position & voxel,BattleUnit * excludeUnit,bool excludeAllUnits,bool onlyVisible,BattleUnit * excludeAllBut)2303 int TileEngine::voxelCheck(const Position& voxel, BattleUnit *excludeUnit, bool excludeAllUnits, bool onlyVisible, BattleUnit *excludeAllBut)
2304 {
2305 Tile *tile = _save->getTile(voxel / Position(16, 16, 24));
2306 // check if we are not out of the map
2307 if (tile == 0 || voxel.x < 0 || voxel.y < 0 || voxel.z < 0)
2308 {
2309 return V_OUTOFBOUNDS;
2310 }
2311 Tile *tileBelow = _save->getTile(tile->getPosition() + Position(0,0,-1));
2312 if (tile->isVoid() && tile->getUnit() == 0 && (!tileBelow || tileBelow->getUnit() == 0))
2313 {
2314 return V_EMPTY;
2315 }
2316
2317 if ((voxel.z % 24 == 0 || voxel.z % 24 == 1) && tile->getMapData(MapData::O_FLOOR) && tile->getMapData(MapData::O_FLOOR)->isGravLift())
2318 {
2319 if ((tile->getPosition().z == 0) || (tileBelow && tileBelow->getMapData(MapData::O_FLOOR) && !tileBelow->getMapData(MapData::O_FLOOR)->isGravLift()))
2320 return V_FLOOR;
2321 }
2322
2323 // first we check terrain voxel data, not to allow 2x2 units stick through walls
2324 for (int i=0; i< 4; ++i)
2325 {
2326 MapData *mp = tile->getMapData(i);
2327 if (tile->isUfoDoorOpen(i))
2328 continue;
2329 if (mp != 0)
2330 {
2331 int x = 15 - voxel.x%16;
2332 int y = voxel.y%16;
2333 int idx = (mp->getLoftID((voxel.z%24)/2)*16) + y;
2334 if (_voxelData->at(idx) & (1 << x))
2335 {
2336 return i;
2337 }
2338 }
2339 }
2340
2341 if (!excludeAllUnits)
2342 {
2343 BattleUnit *unit = tile->getUnit();
2344 // sometimes there is unit on the tile below, but sticks up to this tile with his head,
2345 // in this case we couldn't have unit standing at current tile.
2346 if (unit == 0 && tile->hasNoFloor(0))
2347 {
2348 tile = _save->getTile(Position(voxel.x/16, voxel.y/16, (voxel.z/24)-1)); //below
2349 if (tile) unit = tile->getUnit();
2350 }
2351
2352 if (unit != 0 && unit != excludeUnit && (!excludeAllBut || unit == excludeAllBut) && (!onlyVisible || unit->getVisible() ) )
2353 {
2354 Position tilepos;
2355 Position unitpos = unit->getPosition();
2356 int tz = unitpos.z*24 + unit->getFloatHeight()+(-tile->getTerrainLevel());//bottom
2357 if ((voxel.z > tz) && (voxel.z <= tz + unit->getHeight()) )
2358 {
2359 int x = voxel.x%16;
2360 int y = voxel.y%16;
2361 int part = 0;
2362 if (unit->getArmor()->getSize() > 1)
2363 {
2364 tilepos = tile->getPosition();
2365 part = tilepos.x - unitpos.x + (tilepos.y - unitpos.y)*2;
2366 }
2367 int idx = (unit->getLoftemps(part) * 16) + y;
2368 if (_voxelData->at(idx) & (1 << x))
2369 {
2370 return V_UNIT;
2371 }
2372 }
2373 }
2374 }
2375 return V_EMPTY;
2376 }
2377
2378 /**
2379 * Toggles personal lighting on / off.
2380 */
togglePersonalLighting()2381 void TileEngine::togglePersonalLighting()
2382 {
2383 _personalLighting = !_personalLighting;
2384 calculateUnitLighting();
2385 }
2386
2387 /**
2388 * Calculates the distance between 2 points. Rounded up to first INT.
2389 * @param pos1 Position of first square.
2390 * @param pos2 Position of second square.
2391 * @return Distance.
2392 */
distance(const Position & pos1,const Position & pos2) const2393 int TileEngine::distance(const Position &pos1, const Position &pos2) const
2394 {
2395 int x = pos1.x - pos2.x;
2396 int y = pos1.y - pos2.y;
2397 return (int)Round(sqrt(float(x*x + y*y)));
2398 }
2399
2400 /**
2401 * Calculates the distance squared between 2 points. No sqrt(), not floating point math, and sometimes it's all you need.
2402 * @param pos1 Position of first square.
2403 * @param pos2 Position of second square.
2404 * @param considerZ Whether to consider the z coordinate.
2405 * @return Distance.
2406 */
distanceSq(const Position & pos1,const Position & pos2,bool considerZ) const2407 int TileEngine::distanceSq(const Position &pos1, const Position &pos2, bool considerZ) const
2408 {
2409 int x = pos1.x - pos2.x;
2410 int y = pos1.y - pos2.y;
2411 int z = considerZ ? (pos1.z - pos2.z) : 0;
2412 return x*x + y*y + z*z;
2413 }
2414
2415 /**
2416 * Attempts a panic or mind control action.
2417 * @param action Pointer to an action.
2418 * @return Whether it failed or succeeded.
2419 */
psiAttack(BattleAction * action)2420 bool TileEngine::psiAttack(BattleAction *action)
2421 {
2422 BattleUnit *victim = _save->getTile(action->target)->getUnit();
2423 if (!victim)
2424 return false;
2425 double attackStrength = action->actor->getStats()->psiStrength * action->actor->getStats()->psiSkill / 50.0;
2426 double defenseStrength = victim->getStats()->psiStrength
2427 + ((victim->getStats()->psiSkill > 0) ? 10.0 + victim->getStats()->psiSkill / 5.0 : 10.0);
2428 double d = distance(action->actor->getPosition(), action->target);
2429 attackStrength -= d;
2430 attackStrength += RNG::generate(0,55);
2431
2432 if (action->type == BA_MINDCONTROL)
2433 {
2434 defenseStrength += 20;
2435 }
2436
2437 action->actor->addPsiExp();
2438 if (attackStrength > defenseStrength)
2439 {
2440 action->actor->addPsiExp();
2441 action->actor->addPsiExp();
2442 if (action->type == BA_PANIC)
2443 {
2444 int moraleLoss = (110-_save->getTile(action->target)->getUnit()->getStats()->bravery);
2445 if (moraleLoss > 0)
2446 _save->getTile(action->target)->getUnit()->moraleChange(-moraleLoss);
2447 }
2448 else// if (action->type == BA_MINDCONTROL)
2449 {
2450 victim->convertToFaction(action->actor->getFaction());
2451 calculateFOV(victim->getPosition());
2452 calculateUnitLighting();
2453 victim->setTimeUnits(victim->getStats()->tu);
2454 victim->allowReselect();
2455 victim->abortTurn(); // resets unit status to STANDING
2456 // if all units from either faction are mind controlled - auto-end the mission.
2457 if (_save->getSide() == FACTION_PLAYER && Options::battleAutoEnd && Options::allowPsionicCapture)
2458 {
2459 int liveAliens = 0;
2460 int liveSoldiers = 0;
2461 _save->getBattleGame()->tallyUnits(liveAliens, liveSoldiers, false);
2462 if (liveAliens == 0 || liveSoldiers == 0)
2463 {
2464 _save->setSelectedUnit(0);
2465 _save->getBattleGame()->cancelCurrentAction(true);
2466 _save->getBattleGame()->requestEndTurn();
2467 }
2468 }
2469 }
2470 return true;
2471 }
2472 return false;
2473 }
2474
2475 /**
2476 * Applies gravity to a tile. Causes items and units to drop.
2477 * @param t Tile.
2478 * @return Tile where the items end up in eventually.
2479 */
applyGravity(Tile * t)2480 Tile *TileEngine::applyGravity(Tile *t)
2481 {
2482 if (!t || (t->getInventory()->empty() && !t->getUnit())) return t; // skip this if there are no items
2483
2484 Position p = t->getPosition();
2485 Tile *rt = t;
2486 Tile *rtb;
2487 BattleUnit *occupant = t->getUnit();
2488
2489 if (occupant)
2490 {
2491 Position unitpos = occupant->getPosition();
2492 while (unitpos.z >= 0)
2493 {
2494 bool canFall = true;
2495 for (int y = 0; y < occupant->getArmor()->getSize() && canFall; ++y)
2496 {
2497 for (int x = 0; x < occupant->getArmor()->getSize() && canFall; ++x)
2498 {
2499 rt = _save->getTile(Position(unitpos.x+x, unitpos.y+y, unitpos.z));
2500 rtb = _save->getTile(Position(unitpos.x+x, unitpos.y+y, unitpos.z-1)); //below
2501 if (!rt->hasNoFloor(rtb))
2502 {
2503 canFall = false;
2504 }
2505 }
2506 }
2507 if (!canFall)
2508 break;
2509 unitpos.z--;
2510 }
2511 if (unitpos != occupant->getPosition())
2512 {
2513 if (occupant->getHealth() != 0 && occupant->getStunlevel() < occupant->getHealth())
2514 {
2515 if (occupant->getArmor()->getMovementType() == MT_FLY)
2516 {
2517 // move to the position you're already in. this will unset the kneeling flag, set teh floating flag, etc.
2518 occupant->startWalking(occupant->getDirection(), occupant->getPosition(), _save->getTile(occupant->getPosition() + Position(0,0,-1)), true);
2519 // and set our status to standing (rather than walking or flying) to avoid weirdness.
2520 occupant->abortTurn();
2521 }
2522 else
2523 {
2524 occupant->startWalking(Pathfinding::DIR_DOWN, occupant->getPosition() + Position(0,0,-1), _save->getTile(occupant->getPosition() + Position(0,0,-1)), true);
2525 _save->addFallingUnit(occupant);
2526 }
2527 }
2528 else if (occupant->isOut())
2529 {
2530 Position origin = occupant->getPosition();
2531 for (int y = occupant->getArmor()->getSize()-1; y >= 0; --y)
2532 {
2533 for (int x = occupant->getArmor()->getSize()-1; x >= 0; --x)
2534 {
2535 _save->getTile(origin + Position(x, y, 0))->setUnit(0);
2536 }
2537 }
2538 occupant->setPosition(unitpos);
2539 }
2540 }
2541 }
2542 rt = t;
2543 bool canFall = true;
2544 while (p.z >= 0 && canFall)
2545 {
2546 rt = _save->getTile(p);
2547 rtb = _save->getTile(Position(p.x, p.y, p.z-1)); //below
2548 if (!rt->hasNoFloor(rtb))
2549 canFall = false;
2550 p.z--;
2551 }
2552
2553 for (std::vector<BattleItem*>::iterator it = t->getInventory()->begin(); it != t->getInventory()->end(); ++it)
2554 {
2555 if ((*it)->getUnit() && t->getPosition() == (*it)->getUnit()->getPosition())
2556 {
2557 (*it)->getUnit()->setPosition(rt->getPosition());
2558 }
2559 if (t != rt)
2560 {
2561 rt->addItem(*it, (*it)->getSlot());
2562 }
2563 }
2564
2565 if (t != rt)
2566 {
2567 // clear tile
2568 t->getInventory()->clear();
2569 }
2570
2571 return rt;
2572 }
2573
2574 /**
2575 * Validates the melee range between two units.
2576 * @param attacker The attacking unit.
2577 * @param target The unit we want to attack.
2578 * @param dir Direction to check.
2579 * @return True when the range is valid.
2580 */
validMeleeRange(BattleUnit * attacker,BattleUnit * target,int dir)2581 bool TileEngine::validMeleeRange(BattleUnit *attacker, BattleUnit *target, int dir)
2582 {
2583 return validMeleeRange(attacker->getPosition(), dir, attacker, target, 0);
2584 }
2585
2586 /**
2587 * Validates the melee range between a tile and a unit.
2588 * @param pos Position to check from.
2589 * @param direction Direction to check.
2590 * @param attacker The attacking unit.
2591 * @param target The unit we want to attack, 0 for any unit.
2592 * @param dest Destination position.
2593 * @return True when the range is valid.
2594 */
validMeleeRange(Position pos,int direction,BattleUnit * attacker,BattleUnit * target,Position * dest)2595 bool TileEngine::validMeleeRange(Position pos, int direction, BattleUnit *attacker, BattleUnit *target, Position *dest)
2596 {
2597 if (direction < 0 || direction > 7)
2598 {
2599 return false;
2600 }
2601 Position p;
2602 int size = attacker->getArmor()->getSize() - 1;
2603 Pathfinding::directionToVector(direction, &p);
2604 for (int x = 0; x <= size; ++x)
2605 {
2606 for (int y = 0; y <= size; ++y)
2607 {
2608 Tile *origin (_save->getTile(Position(pos + Position(x, y, 0))));
2609 Tile *targetTile (_save->getTile(Position(pos + Position(x, y, 0) + p)));
2610 Tile *aboveTargetTile (_save->getTile(Position(pos + Position(x, y, 1) + p)));
2611 Tile *belowTargetTile (_save->getTile(Position(pos + Position(x, y, -1) + p)));
2612
2613 if (targetTile && origin)
2614 {
2615 if (origin->getTerrainLevel() <= -16 && aboveTargetTile && !aboveTargetTile->hasNoFloor(targetTile))
2616 {
2617 targetTile = aboveTargetTile;
2618 }
2619 else if (belowTargetTile && targetTile->hasNoFloor(belowTargetTile) && !targetTile->getUnit() && belowTargetTile->getTerrainLevel() <= -16)
2620 {
2621 targetTile = belowTargetTile;
2622 }
2623 if (targetTile->getUnit())
2624 {
2625 if (target == 0 || targetTile->getUnit() == target)
2626 {
2627 Position originVoxel = Position(origin->getPosition() * Position(16,16,24))
2628 + Position(8,8,attacker->getHeight() + attacker->getFloatHeight() - 4 -origin->getTerrainLevel());
2629 Position targetVoxel;
2630 if (canTargetUnit(&originVoxel, targetTile, &targetVoxel, attacker))
2631 {
2632 if (dest)
2633 {
2634 *dest = targetTile->getPosition();
2635 }
2636 return true;
2637 }
2638 }
2639 }
2640 }
2641 }
2642 }
2643 return false;
2644 }
2645
2646 /**
2647 * Gets the AI to look through a window.
2648 * @param position Current position.
2649 * @return Direction or -1 when no window found.
2650 */
faceWindow(const Position & position)2651 int TileEngine::faceWindow(const Position &position)
2652 {
2653 static const Position oneTileEast = Position(1, 0, 0);
2654 static const Position oneTileSouth = Position(0, 1, 0);
2655
2656 Tile *tile = _save->getTile(position);
2657 if (tile && tile->getMapData(MapData::O_NORTHWALL) && tile->getMapData(MapData::O_NORTHWALL)->getBlock(DT_NONE)==0) return 0;
2658 tile = _save->getTile(position + oneTileEast);
2659 if (tile && tile->getMapData(MapData::O_WESTWALL) && tile->getMapData(MapData::O_WESTWALL)->getBlock(DT_NONE)==0) return 2;
2660 tile = _save->getTile(position + oneTileSouth);
2661 if (tile && tile->getMapData(MapData::O_NORTHWALL) && tile->getMapData(MapData::O_NORTHWALL)->getBlock(DT_NONE)==0) return 4;
2662 tile = _save->getTile(position);
2663 if (tile && tile->getMapData(MapData::O_WESTWALL) && tile->getMapData(MapData::O_WESTWALL)->getBlock(DT_NONE)==0) return 6;
2664
2665 return -1;
2666 }
2667
2668 /**
2669 * Validates a throw action.
2670 * @param action The action to validate.
2671 * @param originVoxel The origin point of the action.
2672 * @param targetVoxel The target point of the action.
2673 * @param curve The curvature of the throw.
2674 * @param voxelType The type of voxel at which this parabola terminates.
2675 * @return Validity of action.
2676 */
validateThrow(BattleAction & action,Position originVoxel,Position targetVoxel,double * curve,int * voxelType)2677 bool TileEngine::validateThrow(BattleAction &action, Position originVoxel, Position targetVoxel, double *curve, int *voxelType)
2678 {
2679 bool foundCurve = false;
2680 double curvature = 0.5;
2681 if (action.type == BA_THROW)
2682 {
2683 curvature = std::max(0.48, 1.73 / sqrt(sqrt((double)(action.actor->getStats()->strength) / (double)(action.weapon->getRules()->getWeight()))) + (action.actor->isKneeled()? 0.1 : 0.0));
2684 }
2685 Tile *targetTile = _save->getTile(action.target);
2686 // object blocking - can't throw here
2687 if ((action.type == BA_THROW
2688 && targetTile
2689 && targetTile->getMapData(MapData::O_OBJECT)
2690 && targetTile->getMapData(MapData::O_OBJECT)->getTUCost(MT_WALK) == 255)
2691 || ProjectileFlyBState::validThrowRange(&action, originVoxel, targetTile) == false)
2692 {
2693 return false;
2694 }
2695
2696 // we try 8 different curvatures to try and reach our goal.
2697 int test = V_OUTOFBOUNDS;
2698 while (!foundCurve && curvature < 5.0)
2699 {
2700 std::vector<Position> trajectory;
2701 test = calculateParabola(originVoxel, targetVoxel, false, &trajectory, action.actor, curvature, Position(0,0,0));
2702 if (test != V_OUTOFBOUNDS && (trajectory.at(0) / Position(16, 16, 24)) == (targetVoxel / Position(16, 16, 24)))
2703 {
2704 if (voxelType)
2705 {
2706 *voxelType = test;
2707 }
2708 foundCurve = true;
2709 }
2710 else
2711 {
2712 curvature += 0.5;
2713 }
2714 }
2715 if (curvature >= 5.0)
2716 {
2717 return false;
2718 }
2719 if (curve)
2720 {
2721 *curve = curvature;
2722 }
2723
2724 return true;
2725 }
2726
2727 /**
2728 * Recalculates FOV of all units in-game.
2729 */
recalculateFOV()2730 void TileEngine::recalculateFOV()
2731 {
2732 for (std::vector<BattleUnit*>::iterator bu = _save->getUnits()->begin(); bu != _save->getUnits()->end(); ++bu)
2733 {
2734 if ((*bu)->getTile() != 0)
2735 {
2736 calculateFOV(*bu);
2737 }
2738 }
2739 }
2740
2741 /**
2742 * Returns the direction from origin to target.
2743 * @param origin The origin point of the action.
2744 * @param target The target point of the action.
2745 * @return direction.
2746 */
getDirectionTo(const Position & origin,const Position & target) const2747 int TileEngine::getDirectionTo(const Position &origin, const Position &target) const
2748 {
2749 double ox = target.x - origin.x;
2750 double oy = target.y - origin.y;
2751 double angle = atan2(ox, -oy);
2752 // divide the pie in 4 angles each at 1/8th before each quarter
2753 double pie[4] = {(M_PI_4 * 4.0) - M_PI_4 / 2.0, (M_PI_4 * 3.0) - M_PI_4 / 2.0, (M_PI_4 * 2.0) - M_PI_4 / 2.0, (M_PI_4 * 1.0) - M_PI_4 / 2.0};
2754 int dir = 0;
2755
2756 if (angle > pie[0] || angle < -pie[0])
2757 {
2758 dir = 4;
2759 }
2760 else if (angle > pie[1])
2761 {
2762 dir = 3;
2763 }
2764 else if (angle > pie[2])
2765 {
2766 dir = 2;
2767 }
2768 else if (angle > pie[3])
2769 {
2770 dir = 1;
2771 }
2772 else if (angle < -pie[1])
2773 {
2774 dir = 5;
2775 }
2776 else if (angle < -pie[2])
2777 {
2778 dir = 6;
2779 }
2780 else if (angle < -pie[3])
2781 {
2782 dir = 7;
2783 }
2784 else if (angle < pie[0])
2785 {
2786 dir = 0;
2787 }
2788 return dir;
2789 }
2790
2791 /**
2792 * Gets the origin voxel of a certain action.
2793 * @param action Battle action.
2794 * @param tile Pointer to the action tile.
2795 * @return origin position.
2796 */
getOriginVoxel(BattleAction & action,Tile * tile)2797 Position TileEngine::getOriginVoxel(BattleAction &action, Tile *tile)
2798 {
2799
2800 const int dirYshift[24] = {1, 3, 9, 15, 15, 13, 7, 1, 1, 1, 7, 13, 15, 15, 9, 3, 1, 2, 8, 14, 15, 14, 8, 2};
2801 const int dirXshift[24] = {9, 15, 15, 13, 8, 1, 1, 3, 7, 13, 15, 15, 9, 3, 1, 1, 8, 14, 15, 14, 8, 2, 1, 2};
2802 if (!tile)
2803 {
2804 tile = action.actor->getTile();
2805 }
2806
2807 Position origin = tile->getPosition();
2808 Tile *tileAbove = _save->getTile(origin + Position(0,0,1));
2809 Position originVoxel = Position(origin.x*16, origin.y*16, origin.z*24);
2810
2811 // take into account soldier height and terrain level if the projectile is launched from a soldier
2812 if (action.actor->getPosition() == origin || action.type != BA_LAUNCH)
2813 {
2814 // calculate offset of the starting point of the projectile
2815 originVoxel.z += -tile->getTerrainLevel();
2816
2817 originVoxel.z += action.actor->getHeight() + action.actor->getFloatHeight();
2818
2819 if (action.type == BA_THROW)
2820 {
2821 originVoxel.z -= 3;
2822 }
2823 else
2824 {
2825 originVoxel.z -= 4;
2826 }
2827
2828 if (originVoxel.z >= (origin.z + 1)*24)
2829 {
2830 if (tileAbove && tileAbove->hasNoFloor(0))
2831 {
2832 origin.z++;
2833 }
2834 else
2835 {
2836 while (originVoxel.z >= (origin.z + 1)*24)
2837 {
2838 originVoxel.z--;
2839 }
2840 originVoxel.z -= 4;
2841 }
2842 }
2843 int offset = 0;
2844 if (action.actor->getArmor()->getSize() > 1)
2845 {
2846 offset = 16;
2847 }
2848 else if(action.weapon == action.weapon->getOwner()->getItem("STR_LEFT_HAND") && !action.weapon->getRules()->isTwoHanded())
2849 {
2850 offset = 8;
2851 }
2852 int direction = getDirectionTo(origin, action.target);
2853 originVoxel.x += dirXshift[direction+offset]*action.actor->getArmor()->getSize();
2854 originVoxel.y += dirYshift[direction+offset]*action.actor->getArmor()->getSize();
2855 }
2856 else
2857 {
2858 // don't take into account soldier height and terrain level if the projectile is not launched from a soldier(from a waypoint)
2859 originVoxel.x += 8;
2860 originVoxel.y += 8;
2861 originVoxel.z += 16;
2862 }
2863 return originVoxel;
2864 }
2865
2866 /**
2867 * mark a region of the map as "dangerous" for a turn.
2868 * @param pos is the epicenter of the explosion.
2869 * @param radius how far to spread out.
2870 * @param unit the unit that is triggering this action.
2871 */
setDangerZone(Position pos,int radius,BattleUnit * unit)2872 void TileEngine::setDangerZone(Position pos, int radius, BattleUnit *unit)
2873 {
2874 Tile *tile = _save->getTile(pos);
2875 if (!tile)
2876 {
2877 return;
2878 }
2879 // set the epicenter as dangerous
2880 tile->setDangerous();
2881 Position originVoxel = (pos * Position(16,16,24)) + Position(8,8,12 + -tile->getTerrainLevel());
2882 Position targetVoxel;
2883 for (int x = -radius; x != radius; ++x)
2884 {
2885 for (int y = -radius; y != radius; ++y)
2886 {
2887 // we can skip the epicenter
2888 if (x != 0 || y != 0)
2889 {
2890 // make sure we're within the radius
2891 if ((x*x)+(y*y) <= (radius*radius))
2892 {
2893 tile = _save->getTile(pos + Position(x,y,0));
2894 if (tile)
2895 {
2896 targetVoxel = ((pos + Position(x,y,0)) * Position(16,16,24)) + Position(8,8,12 + -tile->getTerrainLevel());
2897 std::vector<Position> trajectory;
2898 // we'll trace a line here, ignoring all units, to check if the explosion will reach this point
2899 // granted this won't properly account for explosions tearing through walls, but then we can't really
2900 // know that kind of information before the fact, so let's have the AI assume that the wall (or tree)
2901 // is enough to protect them.
2902 if (calculateLine(originVoxel, targetVoxel, false, &trajectory, unit, true, false, unit) == V_EMPTY)
2903 {
2904 if (trajectory.size() && (trajectory.back() / Position(16,16,24)) == pos + Position(x,y,0))
2905 {
2906 tile->setDangerous();
2907 }
2908 }
2909 }
2910 }
2911 }
2912 }
2913 }
2914 }
2915
2916 }
2917