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 "Client.h"
23 
24 #include <Core/ConcurrentDispatch.h>
25 #include <Core/Settings.h>
26 #include <Core/Strings.h>
27 
28 #include "IAudioChunk.h"
29 #include "IAudioDevice.h"
30 
31 #include "CenterMessageView.h"
32 #include "ChatWindow.h"
33 #include "ClientPlayer.h"
34 #include "ClientUI.h"
35 #include "Corpse.h"
36 #include "FallingBlock.h"
37 #include "HurtRingView.h"
38 #include "ILocalEntity.h"
39 #include "LimboView.h"
40 #include "MapView.h"
41 #include "PaletteView.h"
42 #include "Tracer.h"
43 
44 #include "GameMap.h"
45 #include "Grenade.h"
46 #include "Weapon.h"
47 #include "World.h"
48 
49 #include "NetClient.h"
50 
51 DEFINE_SPADES_SETTING(cg_ragdoll, "1");
52 SPADES_SETTING(cg_blood);
53 DEFINE_SPADES_SETTING(cg_ejectBrass, "1");
54 DEFINE_SPADES_SETTING(cg_hitFeedbackSoundGain, "0.2");
55 
56 SPADES_SETTING(cg_alerts);
57 SPADES_SETTING(cg_centerMessage);
58 
59 SPADES_SETTING(cg_shake);
60 
61 SPADES_SETTING(cg_holdAimDownSight);
62 
63 namespace spades {
64 	namespace client {
65 
66 #pragma mark - World States
67 
GetSprintState()68 		float Client::GetSprintState() {
69 			if (!world)
70 				return 0.f;
71 			if (!world->GetLocalPlayer())
72 				return 0.f;
73 
74 			ClientPlayer *p = clientPlayers[(int)world->GetLocalPlayerIndex()];
75 			if (!p)
76 				return 0.f;
77 			return p->GetSprintState();
78 		}
79 
GetAimDownState()80 		float Client::GetAimDownState() {
81 			if (!world)
82 				return 0.f;
83 			if (!world->GetLocalPlayer())
84 				return 0.f;
85 
86 			ClientPlayer *p = clientPlayers[(int)world->GetLocalPlayerIndex()];
87 			if (!p)
88 				return 0.f;
89 			return p->GetAimDownState();
90 		}
91 
CanLocalPlayerUseToolNow()92 		bool Client::CanLocalPlayerUseToolNow() {
93 			if (!world || !world->GetLocalPlayer() || !world->GetLocalPlayer()->IsAlive()) {
94 				return false;
95 			}
96 
97 			if (GetSprintState() > 0 || world->GetLocalPlayer()->GetInput().sprint) {
98 				// Player is unable to use a tool while/soon after sprinting
99 				return false;
100 			}
101 
102 			auto *clientPlayer = GetLocalClientPlayer();
103 			SPAssert(clientPlayer);
104 
105 			if (clientPlayer->IsChangingTool()) {
106 				// Player is unable to use a tool while switching to another tool
107 				return false;
108 			}
109 
110 			return true;
111 		}
112 
GetLocalClientPlayer()113 		ClientPlayer *Client::GetLocalClientPlayer() {
114 			if (!world || !world->GetLocalPlayer()) {
115 				return nullptr;
116 			}
117 			return clientPlayers.at(static_cast<std::size_t>(world->GetLocalPlayerIndex()));
118 		}
119 
120 #pragma mark - World Actions
121 		/** Captures the color of the block player is looking at. */
CaptureColor()122 		void Client::CaptureColor() {
123 			if (!world)
124 				return;
125 			Player *p = world->GetLocalPlayer();
126 			if (!p)
127 				return;
128 			if (!p->IsAlive())
129 				return;
130 
131 			IntVector3 outBlockCoord;
132 			uint32_t col;
133 			if (!world->GetMap()->CastRay(p->GetEye(), p->GetFront(), 256.f, outBlockCoord)) {
134 				auto c = world->GetFogColor();
135 				col = c.x | c.y << 8 | c.z << 16;
136 			} else {
137 				col = world->GetMap()->GetColorWrapped(outBlockCoord.x, outBlockCoord.y,
138 				                                       outBlockCoord.z);
139 			}
140 
141 			IntVector3 colV;
142 			colV.x = (uint8_t)(col);
143 			colV.y = (uint8_t)(col >> 8);
144 			colV.z = (uint8_t)(col >> 16);
145 
146 			p->SetHeldBlockColor(colV);
147 			net->SendHeldBlockColor();
148 		}
149 
SetSelectedTool(Player::ToolType type,bool quiet)150 		void Client::SetSelectedTool(Player::ToolType type, bool quiet) {
151 			if (type == world->GetLocalPlayer()->GetTool())
152 				return;
153 			lastTool = world->GetLocalPlayer()->GetTool();
154 			hasLastTool = true;
155 
156 			world->GetLocalPlayer()->SetTool(type);
157 			net->SendTool();
158 
159 			if (!quiet) {
160 				Handle<IAudioChunk> c =
161 				  audioDevice->RegisterSound("Sounds/Weapons/SwitchLocal.opus");
162 				audioDevice->PlayLocal(c, MakeVector3(.4f, -.3f, .5f), AudioParam());
163 			}
164 		}
165 
166 #pragma mark - World Update
167 
UpdateWorld(float dt)168 		void Client::UpdateWorld(float dt) {
169 			SPADES_MARK_FUNCTION();
170 
171 			Player *player = world->GetLocalPlayer();
172 
173 			if (player) {
174 
175 				// disable input when UI is open
176 				if (scriptedUI->NeedsInput()) {
177 					weapInput.primary = false;
178 					if (player->GetTeamId() >= 2 || player->GetTool() != Player::ToolWeapon) {
179 						weapInput.secondary = false;
180 					}
181 					playerInput = PlayerInput();
182 				}
183 
184 				if (player->GetTeamId() >= 2) {
185 					UpdateLocalSpectator(dt);
186 				} else {
187 					UpdateLocalPlayer(dt);
188 				}
189 			}
190 
191 #if 0
192 			// dynamic time step
193 			// physics diverges from server
194 			world->Advance(dt);
195 #else
196 			// accurately resembles server's physics
197 			// but not smooth
198 			if (dt > 0.f)
199 				worldSubFrame += dt;
200 
201 			float frameStep = 1.f / 60.f;
202 			while (worldSubFrame >= frameStep) {
203 				world->Advance(frameStep);
204 				worldSubFrame -= frameStep;
205 			}
206 #endif
207 
208 			// update player view (doesn't affect physics/game logics)
209 			for (size_t i = 0; i < clientPlayers.size(); i++) {
210 				if (clientPlayers[i]) {
211 					clientPlayers[i]->Update(dt);
212 				}
213 			}
214 
215 			// corpse never accesses audio nor renderer, so
216 			// we can do it in the separate thread
217 			class CorpseUpdateDispatch : public ConcurrentDispatch {
218 				Client *client;
219 				float dt;
220 
221 			public:
222 				CorpseUpdateDispatch(Client *c, float dt) : client(c), dt(dt) {}
223 				void Run() override {
224 					for (auto &c : client->corpses) {
225 						for (int i = 0; i < 4; i++)
226 							c->Update(dt / 4.f);
227 					}
228 				}
229 			};
230 			CorpseUpdateDispatch corpseDispatch(this, dt);
231 			corpseDispatch.Start();
232 
233 			// local entities should be done in the client thread
234 			{
235 				decltype(localEntities)::iterator it;
236 				std::vector<decltype(it)> its;
237 				for (it = localEntities.begin(); it != localEntities.end(); it++) {
238 					if (!(*it)->Update(dt))
239 						its.push_back(it);
240 				}
241 				for (size_t i = 0; i < its.size(); i++) {
242 					localEntities.erase(its[i]);
243 				}
244 			}
245 
246 			corpseDispatch.Join();
247 
248 			if (grenadeVibration > 0.f) {
249 				grenadeVibration -= dt;
250 				if (grenadeVibration < 0.f)
251 					grenadeVibration = 0.f;
252 			}
253 
254 			if (grenadeVibrationSlow > 0.f) {
255 				grenadeVibrationSlow -= dt;
256 				if (grenadeVibrationSlow < 0.f)
257 					grenadeVibrationSlow = 0.f;
258 			}
259 
260 			if (hitFeedbackIconState > 0.f) {
261 				hitFeedbackIconState -= dt * 4.f;
262 				if (hitFeedbackIconState < 0.f)
263 					hitFeedbackIconState = 0.f;
264 			}
265 
266 			if (time > lastPosSentTime + 1.f && world->GetLocalPlayer()) {
267 				Player *p = world->GetLocalPlayer();
268 				if (p->IsAlive() && p->GetTeamId() < 2) {
269 					net->SendPosition();
270 					lastPosSentTime = time;
271 				}
272 			}
273 		}
274 
275 		/** Handles movement of spectating local player. */
UpdateLocalSpectator(float dt)276 		void Client::UpdateLocalSpectator(float dt) {
277 			SPADES_MARK_FUNCTION();
278 
279 			auto &sharedState = followAndFreeCameraState;
280 			auto &freeState = freeCameraState;
281 
282 			Vector3 lastPos = freeState.position;
283 			freeState.velocity *= powf(.3f, dt);
284 			freeState.position += freeState.velocity * dt;
285 
286 			if (freeState.position.x < 0.f) {
287 				freeState.velocity.x = fabsf(freeState.velocity.x) * 0.2f;
288 				freeState.position = lastPos + freeState.velocity * dt;
289 			}
290 			if (freeState.position.y < 0.f) {
291 				freeState.velocity.y = fabsf(freeState.velocity.y) * 0.2f;
292 				freeState.position = lastPos + freeState.velocity * dt;
293 			}
294 			if (freeState.position.x > (float)GetWorld()->GetMap()->Width()) {
295 				freeState.velocity.x = fabsf(freeState.velocity.x) * -0.2f;
296 				freeState.position = lastPos + freeState.velocity * dt;
297 			}
298 			if (freeState.position.y > (float)GetWorld()->GetMap()->Height()) {
299 				freeState.velocity.y = fabsf(freeState.velocity.y) * -0.2f;
300 				freeState.position = lastPos + freeState.velocity * dt;
301 			}
302 
303 			GameMap::RayCastResult minResult;
304 			float minDist = 1.e+10f;
305 			Vector3 minShift;
306 
307 			// check collision
308 			if (freeState.velocity.GetLength() < .01) {
309 				freeState.position = lastPos;
310 				freeState.velocity *= 0.f;
311 			} else {
312 				for (int sx = -1; sx <= 1; sx++)
313 					for (int sy = -1; sy <= 1; sy++)
314 						for (int sz = -1; sz <= 1; sz++) {
315 							GameMap::RayCastResult result;
316 							Vector3 shift = {sx * .1f, sy * .1f, sz * .1f};
317 							result = map->CastRay2(lastPos + shift, freeState.position - lastPos, 256);
318 							if (result.hit && !result.startSolid &&
319 							    Vector3::Dot(result.hitPos - freeState.position - shift,
320 							                 freeState.position - lastPos) < 0.f) {
321 
322 								float dist = Vector3::Dot(result.hitPos - freeState.position - shift,
323 								                          (freeState.position - lastPos).Normalize());
324 								if (dist < minDist) {
325 									minResult = result;
326 									minDist = dist;
327 									minShift = shift;
328 								}
329 							}
330 						}
331 			}
332 			if (minDist < 1.e+9f) {
333 				GameMap::RayCastResult result = minResult;
334 				Vector3 shift = minShift;
335 				freeState.position = result.hitPos - shift;
336 				freeState.position.x += result.normal.x * .02f;
337 				freeState.position.y += result.normal.y * .02f;
338 				freeState.position.z += result.normal.z * .02f;
339 
340 				// bounce
341 				Vector3 norm = {(float)result.normal.x, (float)result.normal.y,
342 				                (float)result.normal.z};
343 				float dot = Vector3::Dot(freeState.velocity, norm);
344 				freeState.velocity -= norm * (dot * 1.2f);
345 			}
346 
347 			// acceleration
348 			Vector3 front;
349 			Vector3 up = {0, 0, -1};
350 
351 			front.x = -cosf(sharedState.yaw) * cosf(sharedState.pitch);
352 			front.y = -sinf(sharedState.yaw) * cosf(sharedState.pitch);
353 			front.z = sinf(sharedState.pitch);
354 
355 			Vector3 right = -Vector3::Cross(up, front).Normalize();
356 			Vector3 up2 = Vector3::Cross(right, front).Normalize();
357 
358 			float scale = 10.f * dt;
359 			if (playerInput.sprint) {
360 				scale *= 3.f;
361 			}
362 			front *= scale;
363 			right *= scale;
364 			up2 *= scale;
365 
366 			if (playerInput.moveForward) {
367 				freeState.velocity += front;
368 			} else if (playerInput.moveBackward) {
369 				freeState.velocity -= front;
370 			}
371 			if (playerInput.moveLeft) {
372 				freeState.velocity -= right;
373 			} else if (playerInput.moveRight) {
374 				freeState.velocity += right;
375 			}
376 			if (playerInput.jump) {
377 				freeState.velocity += up2;
378 			} else if (playerInput.crouch) {
379 				freeState.velocity -= up2;
380 			}
381 
382 			SPAssert(freeState.velocity.GetLength() < 100.f);
383 		}
384 
385 		/** Handles movement of joined local player. */
UpdateLocalPlayer(float dt)386 		void Client::UpdateLocalPlayer(float dt) {
387 			SPADES_MARK_FUNCTION();
388 
389 			auto *player = world->GetLocalPlayer();
390 			auto clientPlayer = clientPlayers[world->GetLocalPlayerIndex()];
391 
392 			PlayerInput inp = playerInput;
393 			WeaponInput winp = weapInput;
394 
395 			Vector3 velocity = player->GetVelocty();
396 			Vector3 horizontalVelocity{velocity.x, velocity.y, 0.0f};
397 
398 			if (horizontalVelocity.GetLength() < 0.1f) {
399 				inp.sprint = false;
400 			}
401 
402 			// Can't use a tool while sprinting or switching to another tool, etc.
403 			if (!CanLocalPlayerUseToolNow()) {
404 				winp.primary = false;
405 				winp.secondary = false;
406 			}
407 
408 			// don't allow to stand up when ceilings are too low
409 			if (inp.crouch == false) {
410 				if (player->GetInput().crouch) {
411 					if (!player->TryUncrouch(false)) {
412 						inp.crouch = true;
413 					}
414 				}
415 			}
416 
417 			// don't allow jumping in the air
418 			if (inp.jump) {
419 				if (!player->IsOnGroundOrWade())
420 					inp.jump = false;
421 			}
422 
423 			if (player->GetTool() == Player::ToolWeapon) {
424 				// disable weapon while reloading (except shotgun)
425 				if (player->IsAwaitingReloadCompletion() && !player->GetWeapon()->IsReloadSlow()) {
426 					winp.primary = false;
427 					winp.secondary = false;
428 				}
429 
430 				// stop firing if the player is out of ammo
431 				if (player->GetWeapon()->GetAmmo() == 0) {
432 					winp.primary = false;
433 				}
434 			}
435 
436 			player->SetInput(inp);
437 			player->SetWeaponInput(winp);
438 
439 			// send player input
440 			{
441 				PlayerInput sentInput = inp;
442 				WeaponInput sentWeaponInput = winp;
443 
444 				// FIXME: send only there are any changed?
445 				net->SendPlayerInput(sentInput);
446 				net->SendWeaponInput(sentWeaponInput);
447 			}
448 
449 			if (hasDelayedReload) {
450 				world->GetLocalPlayer()->Reload();
451 				net->SendReload();
452 				hasDelayedReload = false;
453 			}
454 
455 			// PlayerInput actualInput = player->GetInput();
456 			WeaponInput actualWeapInput = player->GetWeaponInput();
457 
458 			if (!(actualWeapInput.secondary && player->IsToolWeapon() && player->IsAlive()) &&
459 				!(cg_holdAimDownSight && weapInput.secondary)) {
460 				if (player->IsToolWeapon()) {
461 					// there is a possibility that player has respawned or something.
462 					// stop aiming down
463 					weapInput.secondary = false;
464 				}
465 			}
466 
467 			// is the selected tool no longer usable (ex. out of ammo)?
468 			if (!player->IsToolSelectable(player->GetTool())) {
469 				// release mouse button before auto-switching tools
470 				winp.primary = false;
471 				winp.secondary = false;
472 				weapInput = winp;
473 				net->SendWeaponInput(weapInput);
474 				actualWeapInput = winp = player->GetWeaponInput();
475 
476 				// select another tool
477 				Player::ToolType t = player->GetTool();
478 				do {
479 					switch (t) {
480 						case Player::ToolSpade: t = Player::ToolGrenade; break;
481 						case Player::ToolBlock: t = Player::ToolSpade; break;
482 						case Player::ToolWeapon: t = Player::ToolBlock; break;
483 						case Player::ToolGrenade: t = Player::ToolWeapon; break;
484 					}
485 				} while (!world->GetLocalPlayer()->IsToolSelectable(t));
486 				SetSelectedTool(t);
487 			}
488 
489 			// send orientation
490 			Vector3 curFront = player->GetFront();
491 			if (curFront.x != lastFront.x || curFront.y != lastFront.y ||
492 			    curFront.z != lastFront.z) {
493 				lastFront = curFront;
494 				net->SendOrientation(curFront);
495 			}
496 
497 			lastKills = world->GetPlayerPersistent(player->GetId()).kills;
498 
499 			// show block count when building block lines.
500 			if (player->IsAlive() && player->GetTool() == Player::ToolBlock &&
501 			    player->GetWeaponInput().secondary && player->IsBlockCursorDragging()) {
502 				if (player->IsBlockCursorActive()) {
503 					auto blocks = world->CubeLine(player->GetBlockCursorDragPos(),
504 												  player->GetBlockCursorPos(), 256);
505 					auto msg = _TrN("Client", "{0} block", "{0} blocks", blocks.size());
506 					AlertType type = static_cast<int>(blocks.size()) > player->GetNumBlocks()
507 					                   ? AlertType::Warning
508 					                   : AlertType::Notice;
509 					ShowAlert(msg, type, 0.f, true);
510 				} else {
511 					// invalid
512 					auto msg = _Tr("Client", "-- blocks");
513 					AlertType type = AlertType::Warning;
514 					ShowAlert(msg, type, 0.f, true);
515 				}
516 			}
517 
518 			if (player->IsAlive())
519 				lastAliveTime = time;
520 
521 			if (player->GetHealth() < lastHealth) {
522 				// ouch!
523 				lastHealth = player->GetHealth();
524 				lastHurtTime = world->GetTime();
525 
526 				Handle<IAudioChunk> c;
527 				switch (SampleRandomInt(0, 3)) {
528 					case 0:
529 						c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal1.opus");
530 						break;
531 					case 1:
532 						c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal2.opus");
533 						break;
534 					case 2:
535 						c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal3.opus");
536 						break;
537 					case 3:
538 						c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal4.opus");
539 						break;
540 				}
541 				audioDevice->PlayLocal(c, AudioParam());
542 
543 				float hpper = player->GetHealth() / 100.f;
544 				int cnt = 18 - (int)(player->GetHealth() / 100.f * 8.f);
545 				hurtSprites.resize(std::max(cnt, 6));
546 				for (size_t i = 0; i < hurtSprites.size(); i++) {
547 					HurtSprite &spr = hurtSprites[i];
548 					spr.angle = SampleRandomFloat() * (2.f * static_cast<float>(M_PI));
549 					spr.scale = .2f + SampleRandomFloat() * SampleRandomFloat() * .7f;
550 					spr.horzShift = SampleRandomFloat();
551 					spr.strength = .3f + SampleRandomFloat() * .7f;
552 					if (hpper > .5f) {
553 						spr.strength *= 1.5f - hpper;
554 					}
555 				}
556 
557 			} else {
558 				lastHealth = player->GetHealth();
559 			}
560 
561 			inp.jump = false;
562 		}
563 
564 #pragma mark - IWorldListener Handlers
565 
PlayerObjectSet(int id)566 		void Client::PlayerObjectSet(int id) {
567 			if (clientPlayers[id]) {
568 				clientPlayers[id]->Invalidate();
569 				clientPlayers[id] = nullptr;
570 			}
571 
572 			Player *p = world->GetPlayer(id);
573 			if (p)
574 				clientPlayers[id].Set(new ClientPlayer(p, this), false);
575 		}
576 
PlayerJumped(spades::client::Player * p)577 		void Client::PlayerJumped(spades::client::Player *p) {
578 			SPADES_MARK_FUNCTION();
579 
580 			if (!IsMuted()) {
581 
582 				Handle<IAudioChunk> c =
583 				  p->GetWade() ? audioDevice->RegisterSound("Sounds/Player/WaterJump.opus")
584 				               : audioDevice->RegisterSound("Sounds/Player/Jump.opus");
585 				audioDevice->Play(c, p->GetOrigin(), AudioParam());
586 			}
587 		}
588 
PlayerLanded(spades::client::Player * p,bool hurt)589 		void Client::PlayerLanded(spades::client::Player *p, bool hurt) {
590 			SPADES_MARK_FUNCTION();
591 
592 			if (!IsMuted()) {
593 				Handle<IAudioChunk> c;
594 				if (hurt)
595 					c = audioDevice->RegisterSound("Sounds/Player/FallHurt.opus");
596 				else if (p->GetWade())
597 					c = audioDevice->RegisterSound("Sounds/Player/WaterLand.opus");
598 				else
599 					c = audioDevice->RegisterSound("Sounds/Player/Land.opus");
600 				audioDevice->Play(c, p->GetOrigin(), AudioParam());
601 			}
602 		}
603 
PlayerMadeFootstep(spades::client::Player * p)604 		void Client::PlayerMadeFootstep(spades::client::Player *p) {
605 			SPADES_MARK_FUNCTION();
606 
607 			if (!IsMuted()) {
608                 std::array<const char *, 8> snds = {"Sounds/Player/Footstep1.opus", "Sounds/Player/Footstep2.opus",
609 				                      "Sounds/Player/Footstep3.opus", "Sounds/Player/Footstep4.opus",
610 				                      "Sounds/Player/Footstep5.opus", "Sounds/Player/Footstep6.opus",
611 				                      "Sounds/Player/Footstep7.opus", "Sounds/Player/Footstep8.opus"};
612 				std::array<const char *, 12> rsnds = {
613 				  "Sounds/Player/Run1.opus",  "Sounds/Player/Run2.opus",  "Sounds/Player/Run3.opus",
614 				  "Sounds/Player/Run4.opus",  "Sounds/Player/Run5.opus",  "Sounds/Player/Run6.opus",
615 				  "Sounds/Player/Run7.opus",  "Sounds/Player/Run8.opus",  "Sounds/Player/Run9.opus",
616 				  "Sounds/Player/Run10.opus", "Sounds/Player/Run11.opus", "Sounds/Player/Run12.opus",
617 				};
618 				std::array<const char *, 8> wsnds = {"Sounds/Player/Wade1.opus", "Sounds/Player/Wade2.opus",
619 				                       "Sounds/Player/Wade3.opus", "Sounds/Player/Wade4.opus",
620 				                       "Sounds/Player/Wade5.opus", "Sounds/Player/Wade6.opus",
621 				                       "Sounds/Player/Wade7.opus", "Sounds/Player/Wade8.opus"};
622 				bool sprinting = clientPlayers[p->GetId()]
623 				                   ? clientPlayers[p->GetId()]->GetSprintState() > 0.5f
624 				                   : false;
625 				Handle<IAudioChunk> c =
626 				  p->GetWade() ? audioDevice->RegisterSound(SampleRandomElement(wsnds))
627 				               : audioDevice->RegisterSound(SampleRandomElement(snds));
628 				audioDevice->Play(c, p->GetOrigin(), AudioParam());
629 				if (sprinting && !p->GetWade()) {
630 					AudioParam param;
631 					param.volume *= clientPlayers[p->GetId()]->GetSprintState();
632 					c = audioDevice->RegisterSound(SampleRandomElement(rsnds));
633 					audioDevice->Play(c, p->GetOrigin(), param);
634 				}
635 			}
636 		}
637 
PlayerFiredWeapon(spades::client::Player * p)638 		void Client::PlayerFiredWeapon(spades::client::Player *p) {
639 			SPADES_MARK_FUNCTION();
640 
641 			if (p == world->GetLocalPlayer()) {
642 				localFireVibrationTime = time;
643 			}
644 
645 			clientPlayers[p->GetId()]->FiredWeapon();
646 		}
PlayerDryFiredWeapon(spades::client::Player * p)647 		void Client::PlayerDryFiredWeapon(spades::client::Player *p) {
648 			SPADES_MARK_FUNCTION();
649 
650 			if (!IsMuted()) {
651 				bool isLocal = p == world->GetLocalPlayer();
652 				Handle<IAudioChunk> c = audioDevice->RegisterSound("Sounds/Weapons/DryFire.opus");
653 				if (isLocal)
654 					audioDevice->PlayLocal(c, MakeVector3(.4f, -.3f, .5f), AudioParam());
655 				else
656 					audioDevice->Play(c, p->GetEye() + p->GetFront() * 0.5f - p->GetUp() * .3f +
657 					                       p->GetRight() * .4f,
658 					                  AudioParam());
659 			}
660 		}
661 
PlayerReloadingWeapon(spades::client::Player * p)662 		void Client::PlayerReloadingWeapon(spades::client::Player *p) {
663 			SPADES_MARK_FUNCTION();
664 
665 			clientPlayers[p->GetId()]->ReloadingWeapon();
666 		}
667 
PlayerReloadedWeapon(spades::client::Player * p)668 		void Client::PlayerReloadedWeapon(spades::client::Player *p) {
669 			SPADES_MARK_FUNCTION();
670 
671 			clientPlayers[p->GetId()]->ReloadedWeapon();
672 		}
673 
PlayerChangedTool(spades::client::Player * p)674 		void Client::PlayerChangedTool(spades::client::Player *p) {
675 			SPADES_MARK_FUNCTION();
676 
677 			if (!IsMuted()) {
678 				bool isLocal = p == world->GetLocalPlayer();
679 				Handle<IAudioChunk> c;
680 				if (isLocal) {
681 					// played by ClientPlayer::Update
682 					return;
683 				} else {
684 					c = audioDevice->RegisterSound("Sounds/Weapons/Switch.opus");
685 				}
686 				if (isLocal)
687 					audioDevice->PlayLocal(c, MakeVector3(.4f, -.3f, .5f), AudioParam());
688 				else
689 					audioDevice->Play(c, p->GetEye() + p->GetFront() * 0.5f - p->GetUp() * .3f +
690 					                       p->GetRight() * .4f,
691 					                  AudioParam());
692 			}
693 		}
694 
PlayerRestocked(spades::client::Player * p)695 		void Client::PlayerRestocked(spades::client::Player *p) {
696 			if (!IsMuted()) {
697 				bool isLocal = p == world->GetLocalPlayer();
698 				Handle<IAudioChunk> c =
699 				  isLocal ? audioDevice->RegisterSound("Sounds/Weapons/RestockLocal.opus")
700 				          : audioDevice->RegisterSound("Sounds/Weapons/Restock.opus");
701 				if (isLocal)
702 					audioDevice->PlayLocal(c, MakeVector3(.4f, -.3f, .5f), AudioParam());
703 				else
704 					audioDevice->Play(c, p->GetEye() + p->GetFront() * 0.5f - p->GetUp() * .3f +
705 					                       p->GetRight() * .4f,
706 					                  AudioParam());
707 			}
708 		}
709 
PlayerThrownGrenade(spades::client::Player * p,Grenade * g)710 		void Client::PlayerThrownGrenade(spades::client::Player *p, Grenade *g) {
711 			SPADES_MARK_FUNCTION();
712 
713 			if (!IsMuted()) {
714 				bool isLocal = p == world->GetLocalPlayer();
715 				Handle<IAudioChunk> c =
716 				  audioDevice->RegisterSound("Sounds/Weapons/Grenade/Throw.opus");
717 
718 				if (g && isLocal) {
719 					net->SendGrenade(g);
720 				}
721 
722 				if (isLocal)
723 					audioDevice->PlayLocal(c, MakeVector3(.4f, 0.1f, .3f), AudioParam());
724 				else
725 					audioDevice->Play(c, p->GetEye() + p->GetFront() * 0.5f - p->GetUp() * .2f +
726 					                       p->GetRight() * .3f,
727 					                  AudioParam());
728 			}
729 		}
730 
PlayerMissedSpade(spades::client::Player * p)731 		void Client::PlayerMissedSpade(spades::client::Player *p) {
732 			SPADES_MARK_FUNCTION();
733 
734 			if (!IsMuted()) {
735 				bool isLocal = p == world->GetLocalPlayer();
736 				Handle<IAudioChunk> c = audioDevice->RegisterSound("Sounds/Weapons/Spade/Miss.opus");
737 				if (isLocal)
738 					audioDevice->PlayLocal(c, MakeVector3(.2f, -.1f, 0.7f), AudioParam());
739 				else
740 					audioDevice->Play(c, p->GetOrigin() + p->GetFront() * 0.8f - p->GetUp() * .2f,
741 					                  AudioParam());
742 			}
743 		}
744 
PlayerHitBlockWithSpade(spades::client::Player * p,Vector3 hitPos,IntVector3 blockPos,IntVector3 normal)745 		void Client::PlayerHitBlockWithSpade(spades::client::Player *p, Vector3 hitPos,
746 		                                     IntVector3 blockPos, IntVector3 normal) {
747 			SPADES_MARK_FUNCTION();
748 
749 			uint32_t col = map->GetColor(blockPos.x, blockPos.y, blockPos.z);
750 			IntVector3 colV = {(uint8_t)col, (uint8_t)(col >> 8), (uint8_t)(col >> 16)};
751 			Vector3 shiftedHitPos = hitPos;
752 			shiftedHitPos.x += normal.x * .05f;
753 			shiftedHitPos.y += normal.y * .05f;
754 			shiftedHitPos.z += normal.z * .05f;
755 
756 			EmitBlockFragments(shiftedHitPos, colV);
757 
758 			if (p == world->GetLocalPlayer()) {
759 				localFireVibrationTime = time;
760 			}
761 
762 			if (!IsMuted()) {
763 				bool isLocal = p == world->GetLocalPlayer();
764 				Handle<IAudioChunk> c =
765 				  audioDevice->RegisterSound("Sounds/Weapons/Spade/HitBlock.opus");
766 				if (isLocal)
767 					audioDevice->PlayLocal(c, MakeVector3(.1f, -.1f, 1.2f), AudioParam());
768 				else
769 					audioDevice->Play(c, p->GetOrigin() + p->GetFront() * 0.5f - p->GetUp() * .2f,
770 					                  AudioParam());
771 			}
772 		}
773 
PlayerKilledPlayer(spades::client::Player * killer,spades::client::Player * victim,KillType kt)774 		void Client::PlayerKilledPlayer(spades::client::Player *killer,
775 		                                spades::client::Player *victim, KillType kt) {
776 			// play hit sound
777 			if (kt == KillTypeWeapon || kt == KillTypeHeadshot) {
778 				// don't play on local: see BullethitPlayer
779 				if (victim != world->GetLocalPlayer()) {
780 					if (!IsMuted()) {
781 						Handle<IAudioChunk> c;
782 						switch (SampleRandomInt(0, 2)) {
783 							case 0:
784 								c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Flesh1.opus");
785 								break;
786 							case 1:
787 								c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Flesh2.opus");
788 								break;
789 							case 2:
790 								c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Flesh3.opus");
791 								break;
792 						}
793 						AudioParam param;
794 						param.volume = 4.f;
795 						audioDevice->Play(c, victim->GetEye(), param);
796 					}
797 				}
798 			}
799 
800 			// The local player is dead; initialize the look-you-are-dead cam
801 			if (victim == world->GetLocalPlayer()) {
802 				followCameraState.enabled = false;
803 
804 				Vector3 v = -victim->GetFront();
805 				followAndFreeCameraState.yaw = atan2(v.y, v.x);
806 				followAndFreeCameraState.pitch = 30.f * M_PI / 180.f;
807 			}
808 
809 			// emit blood (also for local player)
810 			// FIXME: emiting blood for either
811 			// client-side or server-side hit?
812 			switch (kt) {
813 				case KillTypeGrenade:
814 				case KillTypeHeadshot:
815 				case KillTypeMelee:
816 				case KillTypeWeapon: Bleed(victim->GetEye()); break;
817 				default: break;
818 			}
819 
820 			// create ragdoll corpse
821 			if (cg_ragdoll && victim->GetTeamId() < 2) {
822 				Corpse *corp;
823 				corp = new Corpse(renderer, map, victim);
824 				if (victim == world->GetLocalPlayer())
825 					lastMyCorpse = corp;
826 				if (killer != victim && kt != KillTypeGrenade) {
827 					Vector3 dir = victim->GetPosition() - killer->GetPosition();
828 					dir = dir.Normalize();
829 					if (kt == KillTypeMelee) {
830 						dir *= 6.f;
831 					} else {
832 						if (killer->GetWeapon()->GetWeaponType() == SMG_WEAPON) {
833 							dir *= 2.8f;
834 						} else if (killer->GetWeapon()->GetWeaponType() == SHOTGUN_WEAPON) {
835 							dir *= 4.5f;
836 						} else {
837 							dir *= 3.5f;
838 						}
839 					}
840 					corp->AddImpulse(dir);
841 				} else if (kt == KillTypeGrenade) {
842 					corp->AddImpulse(MakeVector3(0, 0, -4.f - SampleRandomFloat() * 4.f));
843 				}
844 				corp->AddImpulse(victim->GetVelocty() * 32.f);
845 				corpses.emplace_back(corp);
846 
847 				if (corpses.size() > corpseHardLimit) {
848 					corpses.pop_front();
849 				} else if (corpses.size() > corpseSoftLimit) {
850 					RemoveInvisibleCorpses();
851 				}
852 			}
853 
854 			// add chat message
855 			std::string s;
856 			s = ChatWindow::TeamColorMessage(killer->GetName(), killer->GetTeamId());
857 
858 			std::string cause;
859 			bool isFriendlyFire = killer->GetTeamId() == victim->GetTeamId();
860 			if (killer == victim)
861 				isFriendlyFire = false;
862 
863 			Weapon *w =
864 			  killer ? killer->GetWeapon() : nullptr; // only used in case of KillTypeWeapon
865 			switch (kt) {
866 				case KillTypeWeapon:
867 					switch (w ? w->GetWeaponType() : RIFLE_WEAPON) {
868 						case RIFLE_WEAPON: cause += _Tr("Client", "Rifle"); break;
869 						case SMG_WEAPON: cause += _Tr("Client", "SMG"); break;
870 						case SHOTGUN_WEAPON: cause += _Tr("Client", "Shotgun"); break;
871 					}
872 					break;
873 				case KillTypeFall:
874 					//! A cause of death shown in the kill feed.
875 					cause += _Tr("Client", "Fall");
876 					break;
877 				case KillTypeMelee:
878 					//! A cause of death shown in the kill feed.
879 					cause += _Tr("Client", "Melee");
880 					break;
881 				case KillTypeGrenade:
882 					cause += _Tr("Client", "Grenade");
883 					break;
884 				case KillTypeHeadshot:
885 					//! A cause of death shown in the kill feed.
886 					cause += _Tr("Client", "Headshot");
887 					break;
888 				case KillTypeTeamChange:
889 					//! A cause of death shown in the kill feed.
890 					cause += _Tr("Client", "Team Change");
891 					break;
892 				case KillTypeClassChange:
893 					//! A cause of death shown in the kill feed.
894 					cause += _Tr("Client", "Weapon Change");
895 					break;
896 				default:
897 					cause += "???";
898 					break;
899 			}
900 
901 			s += " [";
902 			if (isFriendlyFire)
903 				s += ChatWindow::ColoredMessage(cause, MsgColorFriendlyFire);
904 			else if (killer == world->GetLocalPlayer() || victim == world->GetLocalPlayer())
905 				s += ChatWindow::ColoredMessage(cause, MsgColorGray);
906 			else
907 				s += cause;
908 			s += "] ";
909 
910 			if (killer != victim) {
911 				s += ChatWindow::TeamColorMessage(victim->GetName(), victim->GetTeamId());
912 			}
913 
914 			killfeedWindow->AddMessage(s);
915 
916 			// log to netlog
917 			if (killer != victim) {
918 				NetLog("%s (%s) [%s] %s (%s)", killer->GetName().c_str(),
919 				       world->GetTeam(killer->GetTeamId()).name.c_str(), cause.c_str(),
920 				       victim->GetName().c_str(), world->GetTeam(victim->GetTeamId()).name.c_str());
921 			} else {
922 				NetLog("%s (%s) [%s]", killer->GetName().c_str(),
923 				       world->GetTeam(killer->GetTeamId()).name.c_str(), cause.c_str());
924 			}
925 
926 			// show big message if player is involved
927 			if (victim != killer) {
928 				Player *local = world->GetLocalPlayer();
929 				if (killer == local || victim == local) {
930 					std::string msg;
931 					if (killer == local) {
932 						if ((int)cg_centerMessage == 2)
933 							msg = _Tr("Client", "You have killed {0}", victim->GetName());
934 					} else {
935 						msg = _Tr("Client", "You were killed by {0}", killer->GetName());
936 					}
937 					centerMessageView->AddMessage(msg);
938 				}
939 			}
940 		}
941 
BulletHitPlayer(spades::client::Player * hurtPlayer,HitType type,spades::Vector3 hitPos,spades::client::Player * by)942 		void Client::BulletHitPlayer(spades::client::Player *hurtPlayer, HitType type,
943 		                             spades::Vector3 hitPos, spades::client::Player *by) {
944 			SPADES_MARK_FUNCTION();
945 
946 			SPAssert(type != HitTypeBlock);
947 
948 			// don't bleed local player
949 			if (!IsFirstPerson(GetCameraMode()) || &GetCameraTargetPlayer() != hurtPlayer) {
950 				Bleed(hitPos);
951 			}
952 
953 			if (hurtPlayer == world->GetLocalPlayer()) {
954 				// don't player hit sound now;
955 				// local bullet impact sound is
956 				// played by checking the decrease of HP
957 				return;
958 			}
959 
960 			if (!IsMuted()) {
961 				if (type == HitTypeMelee) {
962 					Handle<IAudioChunk> c =
963 					  audioDevice->RegisterSound("Sounds/Weapons/Spade/HitPlayer.opus");
964 					audioDevice->Play(c, hitPos, AudioParam());
965 				} else {
966 					Handle<IAudioChunk> c;
967 					switch (SampleRandomInt(0, 2)) {
968 						case 0:
969 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Flesh1.opus");
970 							break;
971 						case 1:
972 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Flesh2.opus");
973 							break;
974 						case 2:
975 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Flesh3.opus");
976 							break;
977 					}
978 					AudioParam param;
979 					param.volume = 4.f;
980 					audioDevice->Play(c, hitPos, param);
981 				}
982 			}
983 
984 			if (by == world->GetLocalPlayer() && hurtPlayer) {
985 				net->SendHit(hurtPlayer->GetId(), type);
986 
987 				if (type == HitTypeHead) {
988 					Handle<IAudioChunk> c =
989 					  audioDevice->RegisterSound("Sounds/Feedback/HeadshotFeedback.opus");
990 					AudioParam param;
991 					param.volume = cg_hitFeedbackSoundGain;
992 					audioDevice->PlayLocal(c, param);
993 				}
994 
995 				hitFeedbackIconState = 1.f;
996 				if (hurtPlayer->GetTeamId() == world->GetLocalPlayer()->GetTeamId()) {
997 					hitFeedbackFriendly = true;
998 				} else {
999 					hitFeedbackFriendly = false;
1000 				}
1001 			}
1002 		}
1003 
BulletHitBlock(Vector3 hitPos,IntVector3 blockPos,IntVector3 normal)1004 		void Client::BulletHitBlock(Vector3 hitPos, IntVector3 blockPos, IntVector3 normal) {
1005 			SPADES_MARK_FUNCTION();
1006 
1007 			uint32_t col = map->GetColor(blockPos.x, blockPos.y, blockPos.z);
1008 			IntVector3 colV = {(uint8_t)col, (uint8_t)(col >> 8), (uint8_t)(col >> 16)};
1009 			Vector3 shiftedHitPos = hitPos;
1010 			shiftedHitPos.x += normal.x * .05f;
1011 			shiftedHitPos.y += normal.y * .05f;
1012 			shiftedHitPos.z += normal.z * .05f;
1013 
1014 			if (blockPos.z == 63) {
1015 				BulletHitWaterSurface(shiftedHitPos);
1016 				if (!IsMuted()) {
1017 					AudioParam param;
1018 					param.volume = 2.f;
1019 
1020 					Handle<IAudioChunk> c;
1021 
1022 					param.pitch = .9f + SampleRandomFloat() * 0.2f;
1023 					switch (SampleRandomInt(0, 3)) {
1024 						case 0:
1025 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Water1.opus");
1026 							break;
1027 						case 1:
1028 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Water2.opus");
1029 							break;
1030 						case 2:
1031 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Water3.opus");
1032 							break;
1033 						case 3:
1034 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Water4.opus");
1035 							break;
1036 					}
1037 					audioDevice->Play(c, shiftedHitPos, param);
1038 				}
1039 			} else {
1040 				EmitBlockFragments(shiftedHitPos, colV);
1041 
1042 				if (!IsMuted()) {
1043 					AudioParam param;
1044 					param.volume = 2.f;
1045 
1046 					Handle<IAudioChunk> c;
1047                     c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Block.opus");
1048 					audioDevice->Play(c, shiftedHitPos, param);
1049 
1050 					param.pitch = .9f + SampleRandomFloat() * 0.2f;
1051 					param.volume = 2.f;
1052 					switch (SampleRandomInt(0, 3)) {
1053 						case 0:
1054 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Ricochet1.opus");
1055 							break;
1056 						case 1:
1057 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Ricochet2.opus");
1058 							break;
1059 						case 2:
1060 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Ricochet3.opus");
1061 							break;
1062 						case 3:
1063 							c = audioDevice->RegisterSound("Sounds/Weapons/Impacts/Ricochet4.opus");
1064 							break;
1065 					}
1066 					audioDevice->Play(c, shiftedHitPos, param);
1067 				}
1068 			}
1069 		}
1070 
AddBulletTracer(spades::client::Player * player,spades::Vector3 muzzlePos,spades::Vector3 hitPos)1071 		void Client::AddBulletTracer(spades::client::Player *player, spades::Vector3 muzzlePos,
1072 		                             spades::Vector3 hitPos) {
1073 			SPADES_MARK_FUNCTION();
1074 
1075 			// Do not display tracers for bullets fired by the local player
1076 			if (IsFirstPerson(GetCameraMode()) && GetCameraTargetPlayerId() == player->GetId()) {
1077 				return;
1078 			}
1079 
1080 			float vel;
1081 			switch (player->GetWeapon()->GetWeaponType()) {
1082 				case RIFLE_WEAPON: vel = 700.f; break;
1083 				case SMG_WEAPON: vel = 360.f; break;
1084 				case SHOTGUN_WEAPON: vel = 500.f; break;
1085 			}
1086 			AddLocalEntity(new Tracer(this, muzzlePos, hitPos, vel));
1087 			AddLocalEntity(new MapViewTracer(muzzlePos, hitPos, vel));
1088 		}
1089 
BlocksFell(std::vector<IntVector3> blocks)1090 		void Client::BlocksFell(std::vector<IntVector3> blocks) {
1091 			SPADES_MARK_FUNCTION();
1092 
1093 			if (blocks.empty())
1094 				return;
1095 			FallingBlock *b = new FallingBlock(this, blocks);
1096 			AddLocalEntity(b);
1097 
1098 			if (!IsMuted()) {
1099 
1100 				IntVector3 v = blocks[0];
1101 				Vector3 o;
1102 				o.x = v.x;
1103 				o.y = v.y;
1104 				o.z = v.z;
1105 				o += .5f;
1106 
1107 				Handle<IAudioChunk> c = audioDevice->RegisterSound("Sounds/Misc/BlockFall.opus");
1108 				audioDevice->Play(c, o, AudioParam());
1109 			}
1110 		}
1111 
GrenadeBounced(spades::client::Grenade * g)1112 		void Client::GrenadeBounced(spades::client::Grenade *g) {
1113 			SPADES_MARK_FUNCTION();
1114 
1115 			if (g->GetPosition().z < 63.f) {
1116 				if (!IsMuted()) {
1117 					Handle<IAudioChunk> c =
1118 					  audioDevice->RegisterSound("Sounds/Weapons/Grenade/Bounce.opus");
1119 					audioDevice->Play(c, g->GetPosition(), AudioParam());
1120 				}
1121 			}
1122 		}
1123 
GrenadeDroppedIntoWater(spades::client::Grenade * g)1124 		void Client::GrenadeDroppedIntoWater(spades::client::Grenade *g) {
1125 			SPADES_MARK_FUNCTION();
1126 
1127 			if (!IsMuted()) {
1128 				Handle<IAudioChunk> c =
1129 				  audioDevice->RegisterSound("Sounds/Weapons/Grenade/DropWater.opus");
1130 				audioDevice->Play(c, g->GetPosition(), AudioParam());
1131 			}
1132 		}
1133 
GrenadeExploded(spades::client::Grenade * g)1134 		void Client::GrenadeExploded(spades::client::Grenade *g) {
1135 			SPADES_MARK_FUNCTION();
1136 
1137 			bool inWater = g->GetPosition().z > 63.f;
1138 
1139 			if (inWater) {
1140 				if (!IsMuted()) {
1141 					Handle<IAudioChunk> c =
1142 					  audioDevice->RegisterSound("Sounds/Weapons/Grenade/WaterExplode.opus");
1143 					AudioParam param;
1144 					param.volume = 10.f;
1145 					audioDevice->Play(c, g->GetPosition(), param);
1146 
1147 					c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/WaterExplodeFar.opus");
1148 					param.volume = 6.f;
1149 					param.referenceDistance = 10.f;
1150 					audioDevice->Play(c, g->GetPosition(), param);
1151 
1152 					c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/WaterExplodeStereo.opus");
1153 					param.volume = 2.f;
1154 					audioDevice->Play(c, g->GetPosition(), param);
1155 				}
1156 
1157 				GrenadeExplosionUnderwater(g->GetPosition());
1158 			} else {
1159 
1160 				GrenadeExplosion(g->GetPosition());
1161 
1162 				if (!IsMuted()) {
1163 					Handle<IAudioChunk> c, cs;
1164 
1165 					switch (SampleRandomInt(0, 1)) {
1166 						case 0:
1167 							c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/Explode1.opus");
1168 							cs = audioDevice->RegisterSound(
1169 							  "Sounds/Weapons/Grenade/ExplodeStereo1.opus");
1170 							break;
1171 						case 1:
1172 							c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/Explode2.opus");
1173 							cs = audioDevice->RegisterSound(
1174 							  "Sounds/Weapons/Grenade/ExplodeStereo2.opus");
1175 							break;
1176 					}
1177 
1178 					AudioParam param;
1179 					param.volume = 30.f;
1180 					param.referenceDistance = 5.f;
1181 					audioDevice->Play(c, g->GetPosition(), param);
1182 
1183 					param.referenceDistance = 1.f;
1184 					audioDevice->Play(cs, g->GetPosition(), param);
1185 
1186 					c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/ExplodeFar.opus");
1187 					param.volume = 6.f;
1188 					param.referenceDistance = 40.f;
1189 					audioDevice->Play(c, g->GetPosition(), param);
1190 
1191 					c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/ExplodeFarStereo.opus");
1192 					param.referenceDistance = 10.f;
1193 					audioDevice->Play(c, g->GetPosition(), param);
1194 
1195 					// debri sound
1196 					c = audioDevice->RegisterSound("Sounds/Weapons/Grenade/Debris.opus");
1197 					param.volume = 5.f;
1198 					param.referenceDistance = 3.f;
1199 					IntVector3 outPos;
1200 					Vector3 soundPos = g->GetPosition();
1201 					if (world->GetMap()->CastRay(soundPos, MakeVector3(0, 0, 1), 8.f, outPos)) {
1202 						soundPos.z = (float)outPos.z - .2f;
1203 					}
1204 					audioDevice->Play(c, soundPos, param);
1205 				}
1206 			}
1207 		}
1208 
LocalPlayerPulledGrenadePin()1209 		void Client::LocalPlayerPulledGrenadePin() {
1210 			SPADES_MARK_FUNCTION();
1211 
1212 			if (!IsMuted()) {
1213 				Handle<IAudioChunk> c =
1214 				  audioDevice->RegisterSound("Sounds/Weapons/Grenade/Fire.opus");
1215 				audioDevice->PlayLocal(c, MakeVector3(.4f, -.3f, .5f), AudioParam());
1216 			}
1217 		}
1218 
LocalPlayerBlockAction(spades::IntVector3 v,BlockActionType type)1219 		void Client::LocalPlayerBlockAction(spades::IntVector3 v, BlockActionType type) {
1220 			SPADES_MARK_FUNCTION();
1221 			net->SendBlockAction(v, type);
1222 		}
LocalPlayerCreatedLineBlock(spades::IntVector3 v1,spades::IntVector3 v2)1223 		void Client::LocalPlayerCreatedLineBlock(spades::IntVector3 v1, spades::IntVector3 v2) {
1224 			SPADES_MARK_FUNCTION();
1225 			net->SendBlockLine(v1, v2);
1226 		}
1227 
LocalPlayerHurt(HurtType type,bool sourceGiven,spades::Vector3 source)1228 		void Client::LocalPlayerHurt(HurtType type, bool sourceGiven, spades::Vector3 source) {
1229 			SPADES_MARK_FUNCTION();
1230 
1231 			if (sourceGiven) {
1232 				Player *p = world->GetLocalPlayer();
1233 				if (!p)
1234 					return;
1235 				Vector3 rel = source - p->GetEye();
1236 				rel.z = 0.f;
1237 				rel = rel.Normalize();
1238 				hurtRingView->Add(rel);
1239 			}
1240 		}
1241 
LocalPlayerBuildError(BuildFailureReason reason)1242 		void Client::LocalPlayerBuildError(BuildFailureReason reason) {
1243 			SPADES_MARK_FUNCTION();
1244 
1245 			if (!cg_alerts) {
1246 				PlayAlertSound();
1247 				return;
1248 			}
1249 
1250 			switch (reason) {
1251 				case BuildFailureReason::InsufficientBlocks:
1252 					ShowAlert(_Tr("Client", "Insufficient blocks."), AlertType::Error);
1253 					break;
1254 				case BuildFailureReason::InvalidPosition:
1255 					ShowAlert(_Tr("Client", "You cannot place a block there."), AlertType::Error);
1256 					break;
1257 			}
1258 		}
1259 	}
1260 }
1261