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 <cmath>
21 #include "Projectile.h"
22 #include "TileEngine.h"
23 #include "../fmath.h"
24 #include "../Engine/SurfaceSet.h"
25 #include "../Engine/Surface.h"
26 #include "../Resource/ResourcePack.h"
27 #include "../Ruleset/Unit.h"
28 #include "../Ruleset/RuleSoldier.h"
29 #include "../Ruleset/RuleItem.h"
30 #include "../Ruleset/MapData.h"
31 #include "../Savegame/BattleUnit.h"
32 #include "../Savegame/BattleItem.h"
33 #include "../Savegame/Soldier.h"
34 #include "../Savegame/SavedBattleGame.h"
35 #include "../Savegame/Tile.h"
36 #include "../Engine/RNG.h"
37 #include "../Engine/Options.h"
38 #include "../Ruleset/Armor.h"
39 #include "../Engine/Game.h"
40
41 namespace OpenXcom
42 {
43
44 /**
45 * Sets up a UnitSprite with the specified size and position.
46 * @param res Pointer to resourcepack.
47 * @param save Pointer to battlesavegame.
48 * @param action An action.
49 * @param origin Position the projectile originates from.
50 * @param targetVoxel Position the projectile is targeting.
51 */
Projectile(ResourcePack * res,SavedBattleGame * save,BattleAction action,Position origin,Position targetVoxel)52 Projectile::Projectile(ResourcePack *res, SavedBattleGame *save, BattleAction action, Position origin, Position targetVoxel) : _res(res), _save(save), _action(action), _origin(origin), _targetVoxel(targetVoxel), _position(0)
53 {
54 // this is the number of pixels the sprite will move between frames
55 _speed = Options::battleFireSpeed;
56
57 if (_action.weapon)
58 {
59 if (_action.type == BA_THROW)
60 {
61 _sprite = _res->getSurfaceSet("FLOOROB.PCK")->getFrame(getItem()->getRules()->getFloorSprite());
62 }
63 else
64 {
65 if (_action.weapon->getRules()->getBulletSpeed() != 0)
66 {
67 _speed = std::max(1, _speed + _action.weapon->getRules()->getBulletSpeed());
68 }
69 else if (_action.weapon->getAmmoItem() && _action.weapon->getAmmoItem()->getRules()->getBulletSpeed() != 0)
70 {
71 _speed = std::max(1, _speed + _action.weapon->getAmmoItem()->getRules()->getBulletSpeed());
72 }
73 }
74 }
75 }
76
77 /**
78 * Deletes the Projectile.
79 */
~Projectile()80 Projectile::~Projectile()
81 {
82
83 }
84
85 /**
86 * Calculates the trajectory for a straight path.
87 * @param accuracy The unit's accuracy.
88 * @return The objectnumber(0-3) or unit(4) or out of map (5) or -1 (no line of fire).
89 */
90
calculateTrajectory(double accuracy)91 int Projectile::calculateTrajectory(double accuracy)
92 {
93 Position originVoxel = _save->getTileEngine()->getOriginVoxel(_action, _save->getTile(_origin));
94 return calculateTrajectory(accuracy, originVoxel);
95 }
96
calculateTrajectory(double accuracy,Position originVoxel)97 int Projectile::calculateTrajectory(double accuracy, Position originVoxel)
98 {
99 Tile *targetTile = _save->getTile(_action.target);
100 BattleUnit *bu = _action.actor;
101
102 int test = _save->getTileEngine()->calculateLine(originVoxel, _targetVoxel, false, &_trajectory, bu);
103 if (test != V_EMPTY &&
104 !_trajectory.empty() &&
105 _action.actor->getFaction() == FACTION_PLAYER &&
106 _action.autoShotCounter == 1 &&
107 ((SDL_GetModState() & KMOD_CTRL) == 0 || !Options::forceFire) &&
108 _save->getBattleGame()->getPanicHandled() &&
109 _action.type != BA_LAUNCH)
110 {
111 Position hitPos = Position(_trajectory.at(0).x/16, _trajectory.at(0).y/16, _trajectory.at(0).z/24);
112 if (test == V_UNIT && _save->getTile(hitPos) && _save->getTile(hitPos)->getUnit() == 0) //no unit? must be lower
113 {
114 hitPos = Position(hitPos.x, hitPos.y, hitPos.z-1);
115 }
116
117 if (hitPos != _action.target && _action.result == "")
118 {
119 if (test == V_NORTHWALL)
120 {
121 if (hitPos.y - 1 != _action.target.y)
122 {
123 _trajectory.clear();
124 return V_EMPTY;
125 }
126 }
127 else if (test == V_WESTWALL)
128 {
129 if (hitPos.x - 1 != _action.target.x)
130 {
131 _trajectory.clear();
132 return V_EMPTY;
133 }
134 }
135 else if (test == V_UNIT)
136 {
137 BattleUnit *hitUnit = _save->getTile(hitPos)->getUnit();
138 BattleUnit *targetUnit = targetTile->getUnit();
139 if (hitUnit != targetUnit)
140 {
141 _trajectory.clear();
142 return V_EMPTY;
143 }
144 }
145 else
146 {
147 _trajectory.clear();
148 return V_EMPTY;
149 }
150 }
151 }
152
153 _trajectory.clear();
154
155 bool extendLine = true;
156 // even guided missiles drift, but how much is based on
157 // the shooter's faction, rather than accuracy.
158 if (_action.type == BA_LAUNCH)
159 {
160 if (_action.actor->getFaction() == FACTION_PLAYER)
161 {
162 accuracy = 0.60;
163 }
164 else
165 {
166 accuracy = 0.55;
167 }
168 extendLine = _action.waypoints.size() <= 1;
169 }
170
171 // apply some accuracy modifiers.
172 // This will results in a new target voxel
173 applyAccuracy(originVoxel, &_targetVoxel, accuracy, false, targetTile, extendLine);
174
175 // finally do a line calculation and store this trajectory.
176 return _save->getTileEngine()->calculateLine(originVoxel, _targetVoxel, true, &_trajectory, bu);
177 }
178
179 /**
180 * Calculates the trajectory for a curved path.
181 * @param accuracy The unit's accuracy.
182 * @return True when a trajectory is possible.
183 */
calculateThrow(double accuracy)184 int Projectile::calculateThrow(double accuracy)
185 {
186 Tile *targetTile = _save->getTile(_action.target);
187
188 Position originVoxel = _save->getTileEngine()->getOriginVoxel(_action, 0);
189 Position targetVoxel = _action.target * Position(16,16,24) + Position(8,8, (2 + -targetTile->getTerrainLevel()));
190
191 if (_action.type != BA_THROW)
192 {
193 BattleUnit *tu = targetTile->getUnit();
194 if(!tu && _action.target.z > 0 && targetTile->hasNoFloor(0))
195 tu = _save->getTile(_action.target - Position(0, 0, 1))->getUnit();
196 if (tu)
197 {
198 targetVoxel.z += ((tu->getHeight()/2) + tu->getFloatHeight()) - 2;
199 }
200 }
201
202 double curvature = 0.0;
203 int retVal = V_OUTOFBOUNDS;
204 if (_save->getTileEngine()->validateThrow(_action, originVoxel, targetVoxel, &curvature, &retVal))
205 {
206 int test = V_OUTOFBOUNDS;
207 // finally do a line calculation and store this trajectory, make sure it's valid.
208 while (test == V_OUTOFBOUNDS)
209 {
210 Position deltas = targetVoxel;
211 // apply some accuracy modifiers
212 applyAccuracy(originVoxel, &deltas, accuracy, true, _save->getTile(_action.target), false); //calling for best flavor
213 deltas -= targetVoxel;
214 _trajectory.clear();
215 test = _save->getTileEngine()->calculateParabola(originVoxel, targetVoxel, true, &_trajectory, _action.actor, curvature, deltas);
216
217 Position endPoint = _trajectory.back();
218 endPoint.x /= 16;
219 endPoint.y /= 16;
220 endPoint.z /= 24;
221 Tile *endTile = _save->getTile(endPoint);
222 // check if the item would land on a tile with a blocking object
223 if (_action.type == BA_THROW
224 && endTile
225 && endTile->getMapData(MapData::O_OBJECT)
226 && endTile->getMapData(MapData::O_OBJECT)->getTUCost(MT_WALK) == 255)
227 {
228 test = V_OUTOFBOUNDS;
229 }
230 }
231 return retVal;
232 }
233 return V_OUTOFBOUNDS;
234 }
235
236 /**
237 * Calculates the new target in voxel space, based on the given accuracy modifier.
238 * @param origin Startposition of the trajectory in voxels.
239 * @param target Endpoint of the trajectory in voxels.
240 * @param accuracy Accuracy modifier.
241 * @param keepRange Whether range affects accuracy.
242 * @param targetTile Tile of target. Default = 0.
243 * @param extendLine should this line get extended to maximum distance?
244 */
applyAccuracy(const Position & origin,Position * target,double accuracy,bool keepRange,Tile * targetTile,bool extendLine)245 void Projectile::applyAccuracy(const Position& origin, Position *target, double accuracy, bool keepRange, Tile *targetTile, bool extendLine)
246 {
247 int xdiff = origin.x - target->x;
248 int ydiff = origin.y - target->y;
249 double realDistance = sqrt((double)(xdiff*xdiff)+(double)(ydiff*ydiff));
250 // maxRange is the maximum range a projectile shall ever travel in voxel space
251 double maxRange = keepRange?realDistance:16*1000; // 1000 tiles
252 maxRange = _action.type == BA_HIT?46:maxRange; // up to 2 tiles diagonally (as in the case of reaper v reaper)
253 RuleItem *weapon = _action.weapon->getRules();
254
255 if (_action.type != BA_THROW && _action.type != BA_HIT)
256 {
257 double modifier = 0.0;
258 int upperLimit = weapon->getAimRange();
259 int lowerLimit = weapon->getMinRange();
260 if (Options::battleUFOExtenderAccuracy)
261 {
262 if (_action.type == BA_AUTOSHOT)
263 {
264 upperLimit = weapon->getAutoRange();
265 }
266 else if (_action.type == BA_SNAPSHOT)
267 {
268 upperLimit = weapon->getSnapRange();
269 }
270 }
271 if (realDistance / 16 < lowerLimit)
272 {
273 modifier = (weapon->getDropoff() * (lowerLimit - realDistance / 16)) / 100;
274 }
275 else if (upperLimit < realDistance / 16)
276 {
277 modifier = (weapon->getDropoff() * (realDistance / 16 - upperLimit)) / 100;
278 }
279 accuracy = std::max(0.0, accuracy - modifier);
280 }
281
282 int xDist = abs(origin.x - target->x);
283 int yDist = abs(origin.y - target->y);
284 int zDist = abs(origin.z - target->z);
285 int xyShift, zShift;
286
287 if (xDist / 2 <= yDist) //yes, we need to add some x/y non-uniformity
288 xyShift = xDist / 4 + yDist; //and don't ask why, please. it's The Commandment
289 else
290 xyShift = (xDist + yDist) / 2; //that's uniform part of spreading
291
292 if (xyShift <= zDist) //slight z deviation
293 zShift = xyShift / 2 + zDist;
294 else
295 zShift = xyShift + zDist / 2;
296
297 int deviation = RNG::generate(0, 100) - (accuracy * 100);
298
299 if (deviation >= 0)
300 deviation += 50; // add extra spread to "miss" cloud
301 else
302 deviation += 10; //accuracy of 109 or greater will become 1 (tightest spread)
303
304 deviation = std::max(1, zShift * deviation / 200); //range ratio
305
306 target->x += RNG::generate(0, deviation) - deviation / 2;
307 target->y += RNG::generate(0, deviation) - deviation / 2;
308 target->z += RNG::generate(0, deviation / 2) / 2 - deviation / 8;
309
310 if (extendLine)
311 {
312 double rotation, tilt;
313 rotation = atan2(double(target->y - origin.y), double(target->x - origin.x)) * 180 / M_PI;
314 tilt = atan2(double(target->z - origin.z),
315 sqrt(double(target->x - origin.x)*double(target->x - origin.x)+double(target->y - origin.y)*double(target->y - origin.y))) * 180 / M_PI;
316 // calculate new target
317 // this new target can be very far out of the map, but we don't care about that right now
318 double cos_fi = cos(tilt * M_PI / 180.0);
319 double sin_fi = sin(tilt * M_PI / 180.0);
320 double cos_te = cos(rotation * M_PI / 180.0);
321 double sin_te = sin(rotation * M_PI / 180.0);
322 target->x = (int)(origin.x + maxRange * cos_te * cos_fi);
323 target->y = (int)(origin.y + maxRange * sin_te * cos_fi);
324 target->z = (int)(origin.z + maxRange * sin_fi);
325 }
326 }
327 /**
328 * Moves further in the trajectory.
329 * @return false if the trajectory is finished - no new position exists in the trajectory.
330 */
move()331 bool Projectile::move()
332 {
333 for (int i = 0; i < _speed; ++i)
334 {
335 _position++;
336 if (_position == _trajectory.size())
337 {
338 _position--;
339 return false;
340 }
341 }
342 return true;
343 }
344
345 /**
346 * Gets the current position in voxel space.
347 * @param offset Offset.
348 * @return Position in voxel space.
349 */
getPosition(int offset) const350 Position Projectile::getPosition(int offset) const
351 {
352 int posOffset = (int)_position + offset;
353 if (posOffset >= 0 && posOffset < (int)_trajectory.size())
354 return _trajectory.at(posOffset);
355 else
356 return _trajectory.at(_position);
357 }
358
359 /**
360 * Gets a particle reference from the projectile surfaces.
361 * @param i Index.
362 * @return Particle id.
363 */
getParticle(int i) const364 int Projectile::getParticle(int i) const
365 {
366 if (_action.weapon->getAmmoItem() && _action.weapon->getAmmoItem()->getRules()->getBulletSprite() != -1)
367 return _action.weapon->getAmmoItem()->getRules()->getBulletSprite() + i;
368 else if (_action.weapon->getRules()->getBulletSprite() == -1)
369 return -1;
370 else
371 return _action.weapon->getRules()->getBulletSprite() + i;
372 }
373
374 /**
375 * Gets the project tile item.
376 * Returns 0 when there is no item thrown.
377 * @return Pointer to BattleItem.
378 */
getItem() const379 BattleItem *Projectile::getItem() const
380 {
381 if (_action.type == BA_THROW)
382 return _action.weapon;
383 else
384 return 0;
385 }
386
387 /**
388 * Gets the bullet sprite.
389 * @return Pointer to Surface.
390 */
getSprite() const391 Surface *Projectile::getSprite() const
392 {
393 return _sprite;
394 }
395
396 /**
397 * Skips to the end of the trajectory.
398 */
skipTrajectory()399 void Projectile::skipTrajectory()
400 {
401 _position = _trajectory.size() - 1;
402 }
403
404 /**
405 * Gets the Position of origin for the projectile
406 * @return origin as a tile position.
407 */
getOrigin()408 Position Projectile::getOrigin()
409 {
410 // instead of using the actor's position, we'll use the voxel origin translated to a tile position
411 // this is a workaround for large units.
412 return _trajectory.front() / Position(16,16,24);
413 }
414
415 /**
416 * Gets the INTENDED target for this projectile
417 * it is important to note that we do not use the final position of the projectile here,
418 * but rather the targetted tile
419 * @return target as a tile position.
420 */
getTarget()421 Position Projectile::getTarget()
422 {
423 return _action.target;
424 }
425 }
426