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