1 /*
2  Copyright (c) 2013 yvt
3  based on code of pysnip (c) Mathias Kaerlev 2011-2012.
4 
5  This file is part of OpenSpades.
6 
7  OpenSpades is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  OpenSpades is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with OpenSpades.  If not, see <http://www.gnu.org/licenses/>.
19 
20  */
21 
22 #include <cmath>
23 #include <cstdlib>
24 #include <iterator>
25 
26 #include "Client.h"
27 
28 #include <Core/ConcurrentDispatch.h>
29 #include <Core/Settings.h>
30 #include <Core/Strings.h>
31 
32 #include "IAudioChunk.h"
33 #include "IAudioDevice.h"
34 
35 #include "CenterMessageView.h"
36 #include "ChatWindow.h"
37 #include "ClientPlayer.h"
38 #include "ClientUI.h"
39 #include "Corpse.h"
40 #include "FallingBlock.h"
41 #include "HurtRingView.h"
42 #include "ILocalEntity.h"
43 #include "LimboView.h"
44 #include "MapView.h"
45 #include "PaletteView.h"
46 #include "ParticleSpriteEntity.h"
47 #include "SmokeSpriteEntity.h"
48 
49 #include "GameMap.h"
50 #include "Grenade.h"
51 #include "Weapon.h"
52 #include "World.h"
53 
54 #include "NetClient.h"
55 
56 DEFINE_SPADES_SETTING(cg_blood, "1");
57 DEFINE_SPADES_SETTING(cg_particles, "2");
58 DEFINE_SPADES_SETTING(cg_waterImpact, "1");
59 SPADES_SETTING(cg_manualFocus);
60 DEFINE_SPADES_SETTING(cg_autoFocusSpeed, "0.4");
61 
62 namespace spades {
63 	namespace client {
64 
65 #pragma mark - Local Entities / Effects
66 
RemoveAllCorpses()67 		void Client::RemoveAllCorpses() {
68 			SPADES_MARK_FUNCTION();
69 
70 			corpses.clear();
71 			lastMyCorpse = nullptr;
72 		}
73 
RemoveAllLocalEntities()74 		void Client::RemoveAllLocalEntities() {
75 			SPADES_MARK_FUNCTION();
76 
77 			localEntities.clear();
78 		}
79 
RemoveInvisibleCorpses()80 		void Client::RemoveInvisibleCorpses() {
81 			SPADES_MARK_FUNCTION();
82 
83 			decltype(corpses)::iterator it;
84 			std::vector<decltype(it)> its;
85 			int cnt = (int)corpses.size() - corpseSoftLimit;
86 			for (it = corpses.begin(); it != corpses.end(); it++) {
87 				if (cnt <= 0)
88 					break;
89 				auto &c = *it;
90 				if (!c->IsVisibleFrom(lastSceneDef.viewOrigin)) {
91 					if (c.get() == lastMyCorpse)
92 						lastMyCorpse = nullptr;
93 					its.push_back(it);
94 				}
95 				cnt--;
96 			}
97 
98 			for (size_t i = 0; i < its.size(); i++)
99 				corpses.erase(its[i]);
100 		}
101 
RemoveCorpseForPlayer(int playerId)102 		void Client::RemoveCorpseForPlayer(int playerId) {
103 			for (auto it = corpses.begin(); it != corpses.end();) {
104 				auto cur = it;
105 				++it;
106 
107 				auto &c = *cur;
108 				if (c->GetPlayerId() == playerId) {
109 					corpses.erase(cur);
110 				}
111 			}
112 		}
113 
HotTrackedPlayer(hitTag_t * hitFlag)114 		Player *Client::HotTrackedPlayer(hitTag_t *hitFlag) {
115 			if (!IsFirstPerson(GetCameraMode()))
116 				return nullptr;
117 
118 			auto &p = GetCameraTargetPlayer();
119 
120 			Vector3 origin = p.GetEye();
121 			Vector3 dir = p.GetFront();
122 			World::WeaponRayCastResult result = world->WeaponRayCast(origin, dir, &p);
123 
124 			if (result.hit == false || result.player == nullptr)
125 				return nullptr;
126 
127 			// don't hot track enemies (non-spectator only)
128 			if (result.player->GetTeamId() != p.GetTeamId() && p.GetTeamId() < 2)
129 				return nullptr;
130 			if (hitFlag) {
131 				*hitFlag = result.hitFlag;
132 			}
133 			return result.player;
134 		}
135 
IsMuted()136 		bool Client::IsMuted() {
137 			// prevent to play loud sound at connection
138 			// caused by saved packets
139 			return time < worldSetTime + .05f;
140 		}
141 
Bleed(spades::Vector3 v)142 		void Client::Bleed(spades::Vector3 v) {
143 			SPADES_MARK_FUNCTION();
144 
145 			if (!cg_blood)
146 				return;
147 
148 			// distance cull
149 			if ((v - lastSceneDef.viewOrigin).GetPoweredLength() > 150.f * 150.f)
150 				return;
151 
152 			if ((int)cg_particles < 1)
153 				return;
154 
155 			Handle<IImage> img = renderer->RegisterImage("Gfx/White.tga");
156 			Vector4 color = {0.5f, 0.02f, 0.04f, 1.f};
157 			for (int i = 0; i < 10; i++) {
158 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
159 				ent->SetTrajectory(v,
160 				                   MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
161                                                SampleRandomFloat() - SampleRandomFloat(),
162 				                               SampleRandomFloat() - SampleRandomFloat()) *
163 				                     10.f,
164 				                   1.f, 0.7f);
165 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
166 				ent->SetRadius(0.1f + SampleRandomFloat() * SampleRandomFloat() * 0.2f);
167 				ent->SetLifeTime(3.f, 0.f, 1.f);
168 				localEntities.emplace_back(ent);
169 			}
170 
171 			if ((int)cg_particles < 2)
172 				return;
173 
174 			color = MakeVector4(.7f, .35f, .37f, .6f);
175 			for (int i = 0; i < 2; i++) {
176 				ParticleSpriteEntity *ent =
177 				  new SmokeSpriteEntity(this, color, 100.f, SmokeSpriteEntity::Type::Explosion);
178 				ent->SetTrajectory(v,
179 				                   MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
180                                                SampleRandomFloat() - SampleRandomFloat(),
181 				                               SampleRandomFloat() - SampleRandomFloat()) *
182 				                     .7f,
183 				                   .8f, 0.f);
184 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
185 				ent->SetRadius(.5f + SampleRandomFloat() * SampleRandomFloat() * 0.2f, 2.f);
186 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
187 				ent->SetLifeTime(.20f + SampleRandomFloat() * .2f, 0.06f, .20f);
188 				localEntities.emplace_back(ent);
189 			}
190 
191 			color.w *= .1f;
192 			for (int i = 0; i < 1; i++) {
193 				ParticleSpriteEntity *ent =
194 				  new SmokeSpriteEntity(this, color, 40.f, SmokeSpriteEntity::Type::Steady);
195 				ent->SetTrajectory(v,
196 				                   MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
197                                                SampleRandomFloat() - SampleRandomFloat(),
198 				                               SampleRandomFloat() - SampleRandomFloat()) *
199 				                     .7f,
200 				                   .8f, 0.f);
201 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
202 				ent->SetRadius(.7f + SampleRandomFloat() * SampleRandomFloat() * 0.2f, 2.f, 0.1f);
203 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
204 				ent->SetLifeTime(.80f + SampleRandomFloat() * 0.4f, 0.06f, 1.0f);
205 				localEntities.emplace_back(ent);
206 			}
207 		}
208 
EmitBlockFragments(Vector3 origin,IntVector3 c)209 		void Client::EmitBlockFragments(Vector3 origin, IntVector3 c) {
210 			SPADES_MARK_FUNCTION();
211 
212 			// distance cull
213 			float distPowered = (origin - lastSceneDef.viewOrigin).GetPoweredLength();
214 			if (distPowered > 150.f * 150.f)
215 				return;
216 
217 			if ((int)cg_particles < 1)
218 				return;
219 
220 			Handle<IImage> img = renderer->RegisterImage("Gfx/White.tga");
221 			Vector4 color = {c.x / 255.f, c.y / 255.f, c.z / 255.f, 1.f};
222 			for (int i = 0; i < 7; i++) {
223 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
224 				ent->SetTrajectory(origin,
225 				                   MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
226                                                SampleRandomFloat() - SampleRandomFloat(),
227 				                               SampleRandomFloat() - SampleRandomFloat()) *
228 				                     7.f,
229 				                   1.f, .9f);
230 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
231 				ent->SetRadius(0.2f + SampleRandomFloat() * SampleRandomFloat() * 0.1f);
232 				ent->SetLifeTime(2.f, 0.f, 1.f);
233 				if (distPowered < 16.f * 16.f)
234 					ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak);
235 				localEntities.emplace_back(ent);
236 			}
237 
238 			if ((int)cg_particles < 2)
239 				return;
240 
241 			if (distPowered < 32.f * 32.f) {
242 				for (int i = 0; i < 16; i++) {
243 					ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
244 					ent->SetTrajectory(origin, MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
245 					                                       SampleRandomFloat() - SampleRandomFloat(),
246 					                                       SampleRandomFloat() - SampleRandomFloat()) *
247 					                             12.f,
248 					                   1.f, .9f);
249 					ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
250 					ent->SetRadius(0.1f + SampleRandomFloat() * SampleRandomFloat() * 0.14f);
251 					ent->SetLifeTime(2.f, 0.f, 1.f);
252 					if (distPowered < 16.f * 16.f)
253 						ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak);
254 					localEntities.emplace_back(ent);
255 				}
256 			}
257 
258 			color += (MakeVector4(1, 1, 1, 1) - color) * .2f;
259 			color.w *= .2f;
260 			for (int i = 0; i < 2; i++) {
261 				ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 100.f);
262 				ent->SetTrajectory(origin,
263 				                   MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
264                                                SampleRandomFloat() - SampleRandomFloat(),
265 				                               SampleRandomFloat() - SampleRandomFloat()) *
266 				                     .7f,
267 				                   1.f, 0.f);
268 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
269 				ent->SetRadius(.6f + SampleRandomFloat() * SampleRandomFloat() * 0.2f, 0.8f);
270 				ent->SetLifeTime(.3f + SampleRandomFloat() * .3f, 0.06f, .4f);
271 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
272 				localEntities.emplace_back(ent);
273 			}
274 		}
275 
EmitBlockDestroyFragments(IntVector3 blk,IntVector3 c)276 		void Client::EmitBlockDestroyFragments(IntVector3 blk, IntVector3 c) {
277 			SPADES_MARK_FUNCTION();
278 
279 			Vector3 origin = {blk.x + .5f, blk.y + .5f, blk.z + .5f};
280 
281 			// distance cull
282 			if ((origin - lastSceneDef.viewOrigin).GetPoweredLength() > 150.f * 150.f)
283 				return;
284 
285 			if ((int)cg_particles < 1)
286 				return;
287 
288 			Handle<IImage> img = renderer->RegisterImage("Gfx/White.tga");
289 			Vector4 color = {c.x / 255.f, c.y / 255.f, c.z / 255.f, 1.f};
290 			for (int i = 0; i < 8; i++) {
291 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
292 				ent->SetTrajectory(origin,
293 				                   MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
294                                                SampleRandomFloat() - SampleRandomFloat(),
295 				                               SampleRandomFloat() - SampleRandomFloat()) *
296 				                     7.f,
297 				                   1.f, 1.f);
298 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
299 				ent->SetRadius(0.3f + SampleRandomFloat() * SampleRandomFloat() * 0.2f);
300 				ent->SetLifeTime(2.f, 0.f, 1.f);
301 				ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak);
302 				localEntities.emplace_back(ent);
303 			}
304 		}
305 
MuzzleFire(spades::Vector3 origin,spades::Vector3 dir,bool local)306 		void Client::MuzzleFire(spades::Vector3 origin, spades::Vector3 dir, bool local) {
307 			DynamicLightParam l;
308 			l.origin = origin;
309 			l.radius = 5.f;
310 			l.type = DynamicLightTypePoint;
311 			l.color = MakeVector3(3.f, 1.6f, 0.5f);
312 			flashDlights.push_back(l);
313 
314 			if ((int)cg_particles < 1)
315 				return;
316 
317 			Vector4 color;
318 			Vector3 velBias = {0, 0, -0.5f};
319 			color = MakeVector4(.8f, .8f, .8f, .3f);
320 
321 			// rapid smoke
322 			for (int i = 0; i < 2; i++) {
323 				ParticleSpriteEntity *ent =
324 				  new SmokeSpriteEntity(this, color, 120.f, SmokeSpriteEntity::Type::Explosion);
325 				ent->SetTrajectory(
326 				  origin, (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
327                                        SampleRandomFloat() - SampleRandomFloat(),
328 				                       SampleRandomFloat() - SampleRandomFloat()) +
329 				           velBias * .5f) *
330 				            0.3f,
331 				  1.f, 0.f);
332 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
333 				ent->SetRadius(.4f, 3.f, 0.0000005f);
334 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
335 				ent->SetLifeTime(0.2f + SampleRandomFloat() * 0.1f, 0.f, .30f);
336 				localEntities.emplace_back(ent);
337 			}
338 		}
339 
KickCamera(float strength)340 		void Client::KickCamera(float strength) {
341 			grenadeVibration = std::min(grenadeVibration + strength, 0.4f);
342 			grenadeVibrationSlow = std::min(grenadeVibrationSlow + strength * 5.f, 0.4f);
343 		}
344 
GrenadeExplosion(spades::Vector3 origin)345 		void Client::GrenadeExplosion(spades::Vector3 origin) {
346 			float dist = (origin - lastSceneDef.viewOrigin).GetLength();
347 			if (dist > 170.f)
348 				return;
349 			KickCamera(2.f / (dist + 5.f));
350 
351 			DynamicLightParam l;
352 			l.origin = origin;
353 			l.radius = 16.f;
354 			l.type = DynamicLightTypePoint;
355 			l.color = MakeVector3(3.f, 1.6f, 0.5f);
356 			l.useLensFlare = true;
357 			flashDlights.push_back(l);
358 
359 			if ((int)cg_particles < 1)
360 				return;
361 
362 			Vector3 velBias = {0, 0, 0};
363 			if (!map->ClipBox(origin.x, origin.y, origin.z)) {
364 				if (map->ClipBox(origin.x + 1.f, origin.y, origin.z)) {
365 					velBias.x -= 1.f;
366 				}
367 				if (map->ClipBox(origin.x - 1.f, origin.y, origin.z)) {
368 					velBias.x += 1.f;
369 				}
370 				if (map->ClipBox(origin.x, origin.y + 1.f, origin.z)) {
371 					velBias.y -= 1.f;
372 				}
373 				if (map->ClipBox(origin.x, origin.y - 1.f, origin.z)) {
374 					velBias.y += 1.f;
375 				}
376 				if (map->ClipBox(origin.x, origin.y, origin.z + 1.f)) {
377 					velBias.z -= 1.f;
378 				}
379 				if (map->ClipBox(origin.x, origin.y, origin.z - 1.f)) {
380 					velBias.z += 1.f;
381 				}
382 			}
383 
384 			Vector4 color;
385 			color = MakeVector4(.6f, .6f, .6f, 1.f);
386 			// rapid smoke
387 			for (int i = 0; i < 4; i++) {
388 				ParticleSpriteEntity *ent =
389 				  new SmokeSpriteEntity(this, color, 60.f, SmokeSpriteEntity::Type::Explosion);
390 				ent->SetTrajectory(
391 				  origin, (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
392                                        SampleRandomFloat() - SampleRandomFloat(),
393 				                       SampleRandomFloat() - SampleRandomFloat()) +
394 				           velBias * .5f) *
395 				            2.f,
396 				  1.f, 0.f);
397 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
398 				ent->SetRadius(.6f + SampleRandomFloat() * SampleRandomFloat() * 0.4f, 2.f, .2f);
399 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
400 				ent->SetLifeTime(1.8f + SampleRandomFloat() * 0.1f, 0.f, .20f);
401 				localEntities.emplace_back(ent);
402 			}
403 
404 			// slow smoke
405 			color.w = .25f;
406 			for (int i = 0; i < 8; i++) {
407 				ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 20.f);
408 				ent->SetTrajectory(
409 				  origin, (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
410                                        SampleRandomFloat() - SampleRandomFloat(),
411 				                       (SampleRandomFloat() - SampleRandomFloat()) * .2f)) *
412 				            2.f,
413 				  1.f, 0.f);
414 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
415 				ent->SetRadius(1.5f + SampleRandomFloat() * SampleRandomFloat() * 0.8f, 0.2f);
416 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
417 				switch ((int)cg_particles) {
418 					case 1: ent->SetLifeTime(0.8f + SampleRandomFloat() * 1.f, 0.1f, 8.f); break;
419 					case 2: ent->SetLifeTime(1.5f + SampleRandomFloat() * 2.f, 0.1f, 8.f); break;
420 					case 3:
421 					default: ent->SetLifeTime(2.f + SampleRandomFloat() * 5.f, 0.1f, 8.f); break;
422 				}
423 				localEntities.emplace_back(ent);
424 			}
425 
426 			// fragments
427 			Handle<IImage> img = renderer->RegisterImage("Gfx/White.tga");
428 			color = MakeVector4(0.01, 0.03, 0, 1.f);
429 			for (int i = 0; i < 42; i++) {
430 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
431 				Vector3 dir = MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
432                                           SampleRandomFloat() - SampleRandomFloat(),
433 				                          SampleRandomFloat() - SampleRandomFloat());
434 				dir += velBias * .5f;
435 				float radius = 0.1f + SampleRandomFloat() * SampleRandomFloat() * 0.2f;
436 				ent->SetTrajectory(origin + dir * .2f, dir * 20.f, .1f + radius * 3.f, 1.f);
437 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
438 				ent->SetRadius(radius);
439 				ent->SetLifeTime(3.5f + SampleRandomFloat() * 2.f, 0.f, 1.f);
440 				ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak);
441 				localEntities.emplace_back(ent);
442 			}
443 
444 			// fire smoke
445 			color = MakeVector4(1.f, .7f, .4f, .2f) * 5.f;
446 			for (int i = 0; i < 4; i++) {
447 				ParticleSpriteEntity *ent =
448 				  new SmokeSpriteEntity(this, color, 120.f, SmokeSpriteEntity::Type::Explosion);
449 				ent->SetTrajectory(
450 				  origin, (MakeVector3(SampleRandomFloat() - SampleRandomFloat(), SampleRandomFloat() - SampleRandomFloat(),
451 				                       SampleRandomFloat() - SampleRandomFloat()) +
452 				           velBias) *
453 				            6.f,
454 				  1.f, 0.f);
455 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
456 				ent->SetRadius(.3f + SampleRandomFloat() * SampleRandomFloat() * 0.4f, 3.f, .1f);
457 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
458 				ent->SetLifeTime(.18f + SampleRandomFloat() * 0.03f, 0.f, .10f);
459 				// ent->SetAdditive(true);
460 				localEntities.emplace_back(ent);
461 			}
462 		}
463 
GrenadeExplosionUnderwater(spades::Vector3 origin)464 		void Client::GrenadeExplosionUnderwater(spades::Vector3 origin) {
465 			float dist = (origin - lastSceneDef.viewOrigin).GetLength();
466 			if (dist > 170.f)
467 				return;
468 			KickCamera(1.5f / (dist + 5.f));
469 
470 			if ((int)cg_particles < 1)
471 				return;
472 
473 			Vector3 velBias = {0, 0, 0};
474 
475 			Vector4 color;
476 			color = MakeVector4(.95f, .95f, .95f, .6f);
477 			// water1
478 			Handle<IImage> img = renderer->RegisterImage("Textures/WaterExpl.png");
479 			if ((int)cg_particles < 2)
480 				color.w = .3f;
481 			for (int i = 0; i < 7; i++) {
482 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
483 				ent->SetTrajectory(origin,
484 				                   (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
485 				                                SampleRandomFloat() - SampleRandomFloat(), -SampleRandomFloat() * 7.f)) *
486 				                     2.5f,
487 				                   .3f, .6f);
488 				ent->SetRotation(0.f);
489 				ent->SetRadius(1.5f + SampleRandomFloat() * SampleRandomFloat() * 0.4f, 1.3f);
490 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
491 				ent->SetLifeTime(3.f + SampleRandomFloat() * 0.3f, 0.f, .60f);
492 				localEntities.emplace_back(ent);
493 			}
494 
495 			// water2
496 			img = renderer->RegisterImage("Textures/Fluid.png");
497 			color.w = .9f;
498 			if ((int)cg_particles < 2)
499 				color.w = .4f;
500 			for (int i = 0; i < 16; i++) {
501 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
502 				ent->SetTrajectory(origin,
503 				                   (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
504 				                                SampleRandomFloat() - SampleRandomFloat(), -SampleRandomFloat() * 10.f)) *
505 				                     3.5f,
506 				                   1.f, 1.f);
507 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
508 				ent->SetRadius(0.9f + SampleRandomFloat() * SampleRandomFloat() * 0.4f, 0.7f);
509 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
510 				ent->SetLifeTime(3.f + SampleRandomFloat() * 0.3f, .7f, .60f);
511 				localEntities.emplace_back(ent);
512 			}
513 
514 			// slow smoke
515 			color.w = .4f;
516 			if ((int)cg_particles < 2)
517 				color.w = .2f;
518 			for (int i = 0; i < 8; i++) {
519 				ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 20.f);
520 				ent->SetTrajectory(
521 				  origin, (MakeVector3(SampleRandomFloat() - SampleRandomFloat(), SampleRandomFloat() - SampleRandomFloat(),
522 				                       (SampleRandomFloat() - SampleRandomFloat()) * .2f)) *
523 				            2.f,
524 				  1.f, 0.f);
525 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
526 				ent->SetRadius(1.4f + SampleRandomFloat() * SampleRandomFloat() * 0.8f, 0.2f);
527 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
528 				switch ((int)cg_particles) {
529 					case 1: ent->SetLifeTime(3.f + SampleRandomFloat() * 5.f, 0.1f, 8.f); break;
530 					case 2:
531 					case 3:
532 					default: ent->SetLifeTime(6.f + SampleRandomFloat() * 5.f, 0.1f, 8.f); break;
533 				}
534 				localEntities.emplace_back(ent);
535 			}
536 
537 			// fragments
538 			img = renderer->RegisterImage("Gfx/White.tga");
539 			color = MakeVector4(1, 1, 1, 0.7f);
540 			for (int i = 0; i < 42; i++) {
541 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
542 				Vector3 dir = MakeVector3(SampleRandomFloat() - SampleRandomFloat(), SampleRandomFloat() - SampleRandomFloat(),
543 				                          -SampleRandomFloat() * 3.f);
544 				dir += velBias * .5f;
545 				float radius = 0.1f + SampleRandomFloat() * SampleRandomFloat() * 0.2f;
546 				ent->SetTrajectory(origin + dir * .2f + MakeVector3(0, 0, -1.2f), dir * 13.f,
547 				                   .1f + radius * 3.f, 1.f);
548 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
549 				ent->SetRadius(radius);
550 				ent->SetLifeTime(3.5f + SampleRandomFloat() * 2.f, 0.f, 1.f);
551 				ent->SetBlockHitAction(ParticleSpriteEntity::Delete);
552 				localEntities.emplace_back(ent);
553 			}
554 
555 			// TODO: wave?
556 		}
557 
BulletHitWaterSurface(spades::Vector3 origin)558 		void Client::BulletHitWaterSurface(spades::Vector3 origin) {
559 			float dist = (origin - lastSceneDef.viewOrigin).GetLength();
560 			if (dist > 150.f)
561 				return;
562 			if (!cg_waterImpact)
563 				return;
564 
565 			if ((int)cg_particles < 1)
566 				return;
567 
568 			Vector4 color;
569 			color = MakeVector4(.95f, .95f, .95f, .3f);
570 			// water1
571 			Handle<IImage> img = renderer->RegisterImage("Textures/WaterExpl.png");
572 			if ((int)cg_particles < 2)
573 				color.w = .2f;
574 			for (int i = 0; i < 2; i++) {
575 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
576 				ent->SetTrajectory(origin,
577 				                   (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
578 				                                SampleRandomFloat() - SampleRandomFloat(), -SampleRandomFloat() * 7.f)) *
579 				                     1.f,
580 				                   .3f, .6f);
581 				ent->SetRotation(0.f);
582 				ent->SetRadius(0.6f + SampleRandomFloat() * SampleRandomFloat() * 0.4f, .7f);
583 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
584 				ent->SetLifeTime(3.f + SampleRandomFloat() * 0.3f, 0.1f, .60f);
585 				localEntities.emplace_back(ent);
586 			}
587 
588 			// water2
589 			img = renderer->RegisterImage("Textures/Fluid.png");
590 			color.w = .9f;
591 			if ((int)cg_particles < 2)
592 				color.w = .4f;
593 			for (int i = 0; i < 6; i++) {
594 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
595 				ent->SetTrajectory(origin,
596 				                   (MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
597 				                                SampleRandomFloat() - SampleRandomFloat(), -SampleRandomFloat() * 10.f)) *
598 				                     2.f,
599 				                   1.f, 1.f);
600 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
601 				ent->SetRadius(0.6f + SampleRandomFloat() * SampleRandomFloat() * 0.6f, 0.6f);
602 				ent->SetBlockHitAction(ParticleSpriteEntity::Ignore);
603 				ent->SetLifeTime(3.f + SampleRandomFloat() * 0.3f, SampleRandomFloat() * 0.3f, .60f);
604 				localEntities.emplace_back(ent);
605 			}
606 
607 			// fragments
608 			img = renderer->RegisterImage("Gfx/White.tga");
609 			color = MakeVector4(1, 1, 1, 0.7f);
610 			for (int i = 0; i < 10; i++) {
611 				ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color);
612 				Vector3 dir = MakeVector3(SampleRandomFloat() - SampleRandomFloat(),
613                                           SampleRandomFloat() - SampleRandomFloat(),
614 				                          -SampleRandomFloat() * 3.f);
615 				float radius = 0.03f + SampleRandomFloat() * SampleRandomFloat() * 0.05f;
616 				ent->SetTrajectory(origin + dir * .2f + MakeVector3(0, 0, -1.2f), dir * 5.f,
617 				                   .1f + radius * 3.f, 1.f);
618 				ent->SetRotation(SampleRandomFloat() * (float)M_PI * 2.f);
619 				ent->SetRadius(radius);
620 				ent->SetLifeTime(3.5f + SampleRandomFloat() * 2.f, 0.f, 1.f);
621 				ent->SetBlockHitAction(ParticleSpriteEntity::Delete);
622 				localEntities.emplace_back(ent);
623 			}
624 
625 			// TODO: wave?
626 		}
627 
628 #pragma mark - Camera Control
629 
630 		enum { AutoFocusPoints = 4 };
UpdateAutoFocus(float dt)631 		void Client::UpdateAutoFocus(float dt) {
632 			if (autoFocusEnabled && world && (int)cg_manualFocus) {
633 				// Compute focal length
634 				float measureRange = tanf(lastSceneDef.fovY * .5f) * .2f;
635 				const Vector3 camOrigin = lastSceneDef.viewOrigin;
636 				const float lenScale = 1.f / lastSceneDef.viewAxis[2].GetLength();
637 				const Vector3 camDir = lastSceneDef.viewAxis[2].Normalize();
638 				const Vector3 camX = lastSceneDef.viewAxis[0].Normalize() * measureRange;
639 				const Vector3 camY = lastSceneDef.viewAxis[1].Normalize() * measureRange;
640 
641 				float distances[AutoFocusPoints * AutoFocusPoints];
642 				std::size_t numValidDistances = 0;
643 				Vector3 camDir1 = camDir - camX - camY;
644 				const Vector3 camDX = camX * (2.f / (AutoFocusPoints - 1));
645 				const Vector3 camDY = camY * (2.f / (AutoFocusPoints - 1));
646 				for (int x = 0; x < AutoFocusPoints; ++x) {
647 					Vector3 camDir2 = camDir1;
648 					for (int y = 0; y < AutoFocusPoints; ++y) {
649 						float dist = RayCastForAutoFocus(camOrigin, camDir2);
650 
651 						dist *= lenScale;
652 
653 						if (std::isfinite(dist) && dist > 0.8f) {
654 							distances[numValidDistances++] = dist;
655 						}
656 
657 						camDir2 += camDY;
658 					}
659 					camDir1 += camDX;
660 				}
661 
662 				if (numValidDistances > 0) {
663 					// Take median
664 					std::sort(distances, distances + numValidDistances);
665 
666 					float dist = (numValidDistances & 1)
667 					               ? distances[numValidDistances >> 1]
668 					               : (distances[numValidDistances >> 1] +
669 					                  distances[(numValidDistances >> 1) - 1]) *
670 					                   0.5f;
671 
672 					targetFocalLength = dist;
673 				}
674 			}
675 
676 			// Change the actual focal length slowly
677 			{
678 				float dist = 1.f / targetFocalLength;
679 				float curDist = 1.f / focalLength;
680 				const float maxSpeed = cg_autoFocusSpeed;
681 
682 				if (dist > curDist) {
683 					curDist = std::min(dist, curDist + maxSpeed * dt);
684 				} else {
685 					curDist = std::max(dist, curDist - maxSpeed * dt);
686 				}
687 
688 				focalLength = 1.f / curDist;
689 			}
690 		}
RayCastForAutoFocus(const Vector3 & origin,const Vector3 & direction)691 		float Client::RayCastForAutoFocus(const Vector3 &origin, const Vector3 &direction) {
692 			SPAssert(world);
693 
694 			const auto &lastSceneDef = this->lastSceneDef;
695 			World::WeaponRayCastResult result = world->WeaponRayCast(origin, direction, nullptr);
696 			if (result.hit) {
697 				return Vector3::Dot(result.hitPos - origin, lastSceneDef.viewAxis[2]);
698 			}
699 
700 			return std::nan(nullptr);
701 		}
702 	}
703 }
704