1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3 
4 #include "Missile.h"
5 
6 #include "Game.h"
7 #include "Lang.h"
8 #include "Pi.h"
9 #include "Sfx.h"
10 #include "Ship.h"
11 #include "ShipAICmd.h"
12 #include "Space.h"
13 #include "collider/CollisionContact.h"
14 #include "core/Log.h"
15 #include "lua/LuaEvent.h"
16 
Missile(const ShipType::Id & shipId,Body * owner,int power)17 Missile::Missile(const ShipType::Id &shipId, Body *owner, int power)
18 {
19 	AddFeature(Feature::PROPULSION); // add component propulsion
20 	if (power < 0) {
21 		m_power = 0;
22 		if (shipId == ShipType::MISSILE_GUIDED) m_power = 1;
23 		if (shipId == ShipType::MISSILE_SMART) m_power = 2;
24 		if (shipId == ShipType::MISSILE_NAVAL) m_power = 3;
25 	} else
26 		m_power = power;
27 
28 	m_owner = owner;
29 	m_type = &ShipType::types[shipId];
30 
31 	SetMass(m_type->hullMass * 1000);
32 
33 	SetModel(m_type->modelName.c_str());
34 	SetMassDistributionFromModel();
35 
36 	SetLabel(Lang::MISSILE);
37 
38 	Disarm();
39 
40 	GetPropulsion()->SetFuel(1.0);
41 	GetPropulsion()->SetFuelReserve(0.0);
42 
43 	m_curAICmd = 0;
44 	m_aiMessage = AIERROR_NONE;
45 	m_decelerating = false;
46 
47 	GetPropulsion()->Init(this, GetModel(), m_type->fuelTankMass, m_type->effectiveExhaustVelocity, m_type->linThrust, m_type->angThrust);
48 }
49 
Missile(const Json & jsonObj,Space * space)50 Missile::Missile(const Json &jsonObj, Space *space) :
51 	DynamicBody(jsonObj, space)
52 {
53 	AddFeature(Feature::PROPULSION);
54 	GetPropulsion()->LoadFromJson(jsonObj, space);
55 	Json missileObj = jsonObj["missile"];
56 
57 	try {
58 		m_type = &ShipType::types[missileObj["ship_type_id"]];
59 		SetModel(m_type->modelName.c_str());
60 
61 		m_curAICmd = 0;
62 		m_curAICmd = AICommand::LoadFromJson(missileObj);
63 		m_aiMessage = AIError(missileObj["ai_message"]);
64 
65 		m_ownerIndex = missileObj["index_for_body"];
66 		m_power = missileObj["power"];
67 		m_armed = missileObj["armed"];
68 	} catch (Json::type_error &) {
69 		throw SavedGameCorruptException();
70 	}
71 
72 	GetPropulsion()->Init(this, GetModel(), m_type->fuelTankMass, m_type->effectiveExhaustVelocity, m_type->linThrust, m_type->angThrust);
73 }
74 
SaveToJson(Json & jsonObj,Space * space)75 void Missile::SaveToJson(Json &jsonObj, Space *space)
76 {
77 	DynamicBody::SaveToJson(jsonObj, space);
78 	GetPropulsion()->SaveToJson(jsonObj, space);
79 	Json missileObj = Json::object(); // Create JSON object to contain missile data.
80 
81 	if (m_curAICmd) m_curAICmd->SaveToJson(missileObj);
82 
83 	missileObj["ai_message"] = int(m_aiMessage);
84 	missileObj["index_for_body"] = space->GetIndexForBody(m_owner);
85 	missileObj["power"] = m_power;
86 	missileObj["armed"] = m_armed;
87 	missileObj["ship_type_id"] = m_type->id;
88 
89 	jsonObj["missile"] = missileObj; // Add missile object to supplied object.
90 }
91 
PostLoadFixup(Space * space)92 void Missile::PostLoadFixup(Space *space)
93 {
94 	DynamicBody::PostLoadFixup(space);
95 	m_owner = space->GetBodyByIndex(m_ownerIndex);
96 	if (m_curAICmd) m_curAICmd->PostLoadFixup(space);
97 }
98 
~Missile()99 Missile::~Missile()
100 {
101 	if (m_curAICmd) delete m_curAICmd;
102 }
103 
ECMAttack(int power_val)104 void Missile::ECMAttack(int power_val)
105 {
106 	if (power_val > m_power) {
107 		CollisionContact dummy;
108 		OnDamage(0, 1.0f, dummy);
109 	}
110 }
111 
StaticUpdate(const float timeStep)112 void Missile::StaticUpdate(const float timeStep)
113 {
114 	// Note: direct call to AI->TimeStepUpdate
115 
116 	if (!m_curAICmd) {
117 		GetPropulsion()->ClearLinThrusterState();
118 		GetPropulsion()->ClearAngThrusterState();
119 	} else if (m_curAICmd->TimeStepUpdate()) {
120 		delete m_curAICmd;
121 		m_curAICmd = nullptr;
122 	}
123 	//Add smoke trails for missiles on thruster state
124 	static double s_timeAccum = 0.0;
125 	s_timeAccum += timeStep;
126 	if (!is_equal_exact(GetPropulsion()->GetLinThrusterState().LengthSqr(), 0.0) && (s_timeAccum > 4 || 0.1 * Pi::rng.Double() < timeStep)) {
127 		s_timeAccum = 0.0;
128 		const vector3d pos = GetOrient() * vector3d(0, 0, 5);
129 		const float speed = std::min(10.0 * GetVelocity().Length() * std::max(1.0, fabs(GetPropulsion()->GetLinThrusterState().z)), 100.0);
130 		SfxManager::AddThrustSmoke(this, speed, pos);
131 	}
132 }
133 
TimeStepUpdate(const float timeStep)134 void Missile::TimeStepUpdate(const float timeStep)
135 {
136 
137 	const vector3d thrust = GetPropulsion()->GetActualLinThrust();
138 	AddRelForce(thrust);
139 	AddRelTorque(GetPropulsion()->GetActualAngThrust());
140 
141 	DynamicBody::TimeStepUpdate(timeStep);
142 	GetPropulsion()->UpdateFuel(timeStep);
143 
144 	const float MISSILE_DETECTION_RADIUS = 100.0f;
145 	if (!m_owner) {
146 		Explode();
147 	} else if (m_armed) {
148 		Space::BodyNearList nearby = Pi::game->GetSpace()->GetBodiesMaybeNear(this, MISSILE_DETECTION_RADIUS);
149 		for (Body *body : nearby) {
150 			if (body == this) continue;
151 			double dist = (body->GetPosition() - GetPosition()).Length();
152 			if (dist < MISSILE_DETECTION_RADIUS) {
153 				Explode();
154 				break;
155 			}
156 		}
157 	}
158 }
159 
OnCollision(Body * o,Uint32 flags,double relVel)160 bool Missile::OnCollision(Body *o, Uint32 flags, double relVel)
161 {
162 	if (!IsDead()) {
163 		Explode();
164 	}
165 	return true;
166 }
167 
OnDamage(Body * attacker,float kgDamage,const CollisionContact & contactData)168 bool Missile::OnDamage(Body *attacker, float kgDamage, const CollisionContact &contactData)
169 {
170 	if (!IsDead()) {
171 		Explode();
172 	}
173 	return true;
174 }
175 
calcAreaSphere(const double r)176 double calcAreaSphere(const double r)
177 {
178 	return 4.0 * M_PI * r * r;
179 }
180 
calcAreaCircle(const double r)181 double calcAreaCircle(const double r)
182 {
183 	return M_PI * r * r;
184 }
185 
Explode()186 void Missile::Explode()
187 {
188 	Pi::game->GetSpace()->KillBody(this);
189 
190 	// how much energy was converted in the explosion?
191 	double mjYield = 4.184 * 200; // defaults to 200kg of TNT
192 	Properties().Get("missile_yield_cap", mjYield);
193 
194 	double queryRadius = 2000.0; // defaults to 2 km, this is sufficient for most explosions
195 	Properties().Get("missile_explosion_radius_cap", queryRadius);
196 
197 	CollisionContact dummy;
198 	Space::BodyNearList nearby = Pi::game->GetSpace()->GetBodiesMaybeNear(this, queryRadius);
199 	for (Body *body : nearby) {
200 		const double distSqr = (body->GetPosition() - GetPosition()).LengthSqr();
201 		if (body->GetFrame() != GetFrame() || body == this || distSqr >= queryRadius * queryRadius)
202 			continue;
203 		const double dist = (body->GetPosition() - GetPosition()).Length(); // distance from explosion in meter
204 		const double targetRadius = body->GetPhysRadius();					// radius of the hit target in meter
205 
206 		const double areaSphere = calcAreaSphere(std::max(0.0, dist - targetRadius));
207 		const double crossSectionTarget = calcAreaCircle(targetRadius);
208 		double ratioArea = crossSectionTarget / areaSphere; // compute ratio of areas to know how much energy was transfered to target
209 		ratioArea = std::min(ratioArea, 1.0);				// we must limit received energy to finite amount
210 
211 		const double mjReceivedEnergy = ratioArea * mjYield; // compute received energy by blast
212 
213 		double kgDamage = mjReceivedEnergy * 16.18033; // received energy back to damage in pioneer "kg" unit, using Phi*10 because we can
214 		if (kgDamage < 5.0)
215 			continue; // early-out if we're dealing a negligable amount of damage
216 		// Log::Info("Missile impact on {}\n\ttarget.radius={} dist={} sphereArea={} crossSection={} (ratio={}) => received energy {}mj={}kgD\n",
217 		// 	body->GetLabel(), targetRadius, dist, areaSphere, crossSectionTarget, ratioArea, mjReceivedEnergy, kgDamage);
218 
219 		body->OnDamage(m_owner, kgDamage, dummy);
220 		if (body->IsType(ObjectType::SHIP))
221 			LuaEvent::Queue("onShipHit", dynamic_cast<Ship *>(body), m_owner);
222 	}
223 
224 	SfxManager::Add(this, TYPE_EXPLOSION);
225 }
226 
NotifyRemoved(const Body * const removedBody)227 void Missile::NotifyRemoved(const Body *const removedBody)
228 {
229 	if (m_curAICmd) m_curAICmd->OnDeleted(removedBody);
230 	if (m_owner == removedBody) {
231 		m_owner = 0;
232 	}
233 	DynamicBody::NotifyRemoved(removedBody);
234 }
235 
Arm()236 void Missile::Arm()
237 {
238 	m_armed = true;
239 	Properties().Set("isArmed", true);
240 }
241 
Disarm()242 void Missile::Disarm()
243 {
244 	m_armed = false;
245 	Properties().Set("isArmed", false);
246 }
247 
Render(Graphics::Renderer * renderer,const Camera * camera,const vector3d & viewCoords,const matrix4x4d & viewTransform)248 void Missile::Render(Graphics::Renderer *renderer, const Camera *camera, const vector3d &viewCoords, const matrix4x4d &viewTransform)
249 {
250 	if (IsDead()) return;
251 
252 	GetPropulsion()->Render(renderer, camera, viewCoords, viewTransform);
253 	RenderModel(renderer, camera, viewCoords, viewTransform);
254 }
255 
AIKamikaze(Body * target)256 void Missile::AIKamikaze(Body *target)
257 {
258 	//AIClearInstructions();
259 	if (m_curAICmd != 0)
260 		delete m_curAICmd;
261 	m_curAICmd = new AICmdKamikaze(this, target);
262 }
263