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