/* Copyright (c) 2013 yvt based on code of pysnip (c) Mathias Kaerlev 2011-2012. This file is part of OpenSpades. OpenSpades is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OpenSpades is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenSpades. If not, see . */ #include "Client.h" #include #include #include #include #include "IAudioChunk.h" #include "IAudioDevice.h" #include "ClientUI.h" #include "PaletteView.h" #include "LimboView.h" #include "MapView.h" #include "Corpse.h" #include "ClientPlayer.h" #include "ILocalEntity.h" #include "ChatWindow.h" #include "CenterMessageView.h" #include "Tracer.h" #include "FallingBlock.h" #include "HurtRingView.h" #include "ParticleSpriteEntity.h" #include "SmokeSpriteEntity.h" #include "World.h" #include "Weapon.h" #include "GameMap.h" #include "Grenade.h" #include "NetClient.h" SPADES_SETTING(cg_blood, "1"); SPADES_SETTING(cg_reduceSmoke, "0"); SPADES_SETTING(cg_waterImpact, "1"); SPADES_SETTING(cg_manualFocus, "0"); namespace spades { namespace client { #pragma mark - Local Entities / Effects void Client::RemoveAllCorpses(){ SPADES_MARK_FUNCTION(); corpses.clear(); lastMyCorpse = nullptr; } void Client::RemoveAllLocalEntities(){ SPADES_MARK_FUNCTION(); localEntities.clear(); } void Client::RemoveInvisibleCorpses(){ SPADES_MARK_FUNCTION(); decltype(corpses)::iterator it; std::vector its; int cnt = (int)corpses.size() - corpseSoftLimit; for(it = corpses.begin(); it != corpses.end(); it++){ if(cnt <= 0) break; auto& c = *it; if(!c->IsVisibleFrom(lastSceneDef.viewOrigin)){ if(c.get() == lastMyCorpse) lastMyCorpse = nullptr; its.push_back(it); } cnt--; } for(size_t i = 0; i < its.size(); i++) corpses.erase(its[i]); } Player *Client::HotTrackedPlayer( hitTag_t* hitFlag ){ if(!world) return nullptr; Player *p = world->GetLocalPlayer(); if(!p || !p->IsAlive()) return nullptr; if(ShouldRenderInThirdPersonView()) return nullptr; Vector3 origin = p->GetEye(); Vector3 dir = p->GetFront(); World::WeaponRayCastResult result = world->WeaponRayCast(origin, dir, p); if(result.hit == false || result.player == nullptr) return nullptr; // don't hot track enemies (non-spectator only) if(result.player->GetTeamId() != p->GetTeamId() && p->GetTeamId() < 2) return nullptr; if( hitFlag ) { *hitFlag = result.hitFlag; } return result.player; } bool Client::IsMuted() { // prevent to play loud sound at connection // caused by saved packets return time < worldSetTime + .05f; } void Client::Bleed(spades::Vector3 v){ SPADES_MARK_FUNCTION(); if(!cg_blood) return; // distance cull if((v - lastSceneDef.viewOrigin).GetPoweredLength() > 150.f * 150.f) return; //Handle img = renderer->RegisterImage("Textures/SoftBall.tga"); Handle img = renderer->RegisterImage("Gfx/White.tga"); Vector4 color = {0.5f, 0.02f, 0.04f, 1.f}; for(int i = 0; i < 10; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(v, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * 10.f, 1.f, 0.7f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(0.1f + GetRandom()*GetRandom()*0.2f); ent->SetLifeTime(3.f, 0.f, 1.f); localEntities.emplace_back(ent); } color = MakeVector4(.7f, .35f, .37f, .6f); for(int i = 0; i < 2; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 100.f, SmokeSpriteEntity::Type::Explosion); ent->SetTrajectory(v, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * .7f, .8f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(.5f + GetRandom()*GetRandom()*0.2f, 2.f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(.20f + GetRandom() * .2f, 0.06f, .20f); localEntities.emplace_back(ent); } if(cg_reduceSmoke) return; color.w *= .1f; for(int i = 0; i < 1; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 40.f, SmokeSpriteEntity::Type::Steady); ent->SetTrajectory(v, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * .7f, .8f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(.7f + GetRandom()*GetRandom()*0.2f, 2.f, 0.1f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(.80f + GetRandom() * 0.4f, 0.06f, 1.0f); localEntities.emplace_back(ent); } } void Client::EmitBlockFragments(Vector3 origin, IntVector3 c){ SPADES_MARK_FUNCTION(); // distance cull float distPowered = (origin - lastSceneDef.viewOrigin).GetPoweredLength(); if(distPowered > 150.f * 150.f) return; Handle img = renderer->RegisterImage("Gfx/White.tga"); Vector4 color = {c.x / 255.f, c.y / 255.f, c.z / 255.f, 1.f}; for(int i = 0; i < 7; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * 7.f, 1.f, .9f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(0.2f + GetRandom()*GetRandom()*0.1f); ent->SetLifeTime(2.f, 0.f, 1.f); if(distPowered < 16.f * 16.f) ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak); localEntities.emplace_back(ent); } if(distPowered < 32.f * 32.f){ for(int i = 0; i < 16; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * 12.f, 1.f, .9f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(0.1f + GetRandom()*GetRandom()*0.14f); ent->SetLifeTime(2.f, 0.f, 1.f); if(distPowered < 16.f * 16.f) ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak); localEntities.emplace_back(ent); } } color += (MakeVector4(1, 1, 1, 1) - color) * .2f; color.w *= .2f; for(int i = 0; i < 2; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 100.f); ent->SetTrajectory(origin, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * .7f, 1.f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(.6f + GetRandom()*GetRandom()*0.2f, 0.8f); ent->SetLifeTime(.3f + GetRandom() * .3f, 0.06f, .4f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); localEntities.emplace_back(ent); } } void Client::EmitBlockDestroyFragments(IntVector3 blk, IntVector3 c){ SPADES_MARK_FUNCTION(); Vector3 origin = {blk.x + .5f, blk.y + .5f, blk.z + .5f}; // distance cull if((origin - lastSceneDef.viewOrigin).GetPoweredLength() > 150.f * 150.f) return; Handle img = renderer->RegisterImage("Gfx/White.tga"); Vector4 color = {c.x / 255.f, c.y / 255.f, c.z / 255.f, 1.f}; for(int i = 0; i < 8; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()) * 7.f, 1.f, 1.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(0.3f + GetRandom()*GetRandom()*0.2f); ent->SetLifeTime(2.f, 0.f, 1.f); ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak); localEntities.emplace_back(ent); } } void Client::MuzzleFire(spades::Vector3 origin, spades::Vector3 dir, bool local) { DynamicLightParam l; l.origin = origin; l.radius = 5.f; l.type = DynamicLightTypePoint; l.color = MakeVector3(3.f, 1.6f, 0.5f); flashDlights.push_back(l); Vector4 color; Vector3 velBias = {0, 0, -0.5f}; color = MakeVector4( .8f, .8f, .8f, .3f); // rapid smoke for(int i = 0; i < 2; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 120.f, SmokeSpriteEntity::Type::Explosion); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom())+velBias*.5f) * 0.3f, 1.f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(.4f, 3.f, 0.0000005f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(0.2f + GetRandom()*0.1f, 0.f, .30f); localEntities.emplace_back(ent); } } void Client::GrenadeExplosion(spades::Vector3 origin){ float dist = (origin - lastSceneDef.viewOrigin).GetLength(); if(dist > 170.f) return; grenadeVibration += 2.f / (dist + 5.f); if(grenadeVibration > 1.f) grenadeVibration = 1.f; DynamicLightParam l; l.origin = origin; l.radius = 16.f; l.type = DynamicLightTypePoint; l.color = MakeVector3(3.f, 1.6f, 0.5f); l.useLensFlare = true; flashDlights.push_back(l); Vector3 velBias = {0,0,0}; if(!map->ClipBox(origin.x, origin.y, origin.z)){ if(map->ClipBox(origin.x + 1.f, origin.y, origin.z)){ velBias.x -= 1.f; } if(map->ClipBox(origin.x - 1.f, origin.y, origin.z)){ velBias.x += 1.f; } if(map->ClipBox(origin.x, origin.y + 1.f, origin.z)){ velBias.y -= 1.f; } if(map->ClipBox(origin.x, origin.y - 1.f, origin.z)){ velBias.y += 1.f; } if(map->ClipBox(origin.x, origin.y , origin.z + 1.f)){ velBias.z -= 1.f; } if(map->ClipBox(origin.x, origin.y , origin.z - 1.f)){ velBias.z += 1.f; } } Vector4 color; color = MakeVector4( .6f, .6f, .6f, 1.f); // rapid smoke for(int i = 0; i < 4; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 60.f, SmokeSpriteEntity::Type::Explosion); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom())+velBias*.5f) * 2.f, 1.f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(.6f + GetRandom()*GetRandom()*0.4f, 2.f, .2f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(1.8f + GetRandom()*0.1f, 0.f, .20f); localEntities.emplace_back(ent); } // slow smoke color.w = .25f; for(int i = 0; i < 8; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 20.f); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), (GetRandom()-GetRandom()) * .2f)) * 2.f, 1.f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(1.5f + GetRandom()*GetRandom()*0.8f, 0.2f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); if(cg_reduceSmoke) ent->SetLifeTime(1.f + GetRandom() * 2.f, 0.1f, 8.f); else ent->SetLifeTime(2.f + GetRandom() * 5.f, 0.1f, 8.f); localEntities.emplace_back(ent); } // fragments Handle img = renderer->RegisterImage("Gfx/White.tga"); color = MakeVector4(0.01, 0.03, 0, 1.f); for(int i = 0; i < 42; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); Vector3 dir = MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom()); dir += velBias * .5f; float radius = 0.1f + GetRandom()*GetRandom()*0.2f; ent->SetTrajectory(origin + dir * .2f, dir * 20.f, .1f + radius * 3.f, 1.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(radius); ent->SetLifeTime(3.5f + GetRandom() * 2.f, 0.f, 1.f); ent->SetBlockHitAction(ParticleSpriteEntity::BounceWeak); localEntities.emplace_back(ent); } // fire smoke color= MakeVector4(1.f, .7f, .4f, .2f) * 5.f; for(int i = 0; i < 4; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 120.f, SmokeSpriteEntity::Type::Explosion); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), GetRandom()-GetRandom())+velBias) * 6.f, 1.f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(.3f + GetRandom()*GetRandom()*0.4f, 3.f, .1f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(.18f + GetRandom()*0.03f, 0.f, .10f); //ent->SetAdditive(true); localEntities.emplace_back(ent); } } void Client::GrenadeExplosionUnderwater(spades::Vector3 origin){ float dist = (origin - lastSceneDef.viewOrigin).GetLength(); if(dist > 170.f) return; grenadeVibration += 1.5f / (dist + 5.f); if(grenadeVibration > 1.f) grenadeVibration = 1.f; Vector3 velBias = {0,0,0}; Vector4 color; color = MakeVector4( .95f, .95f, .95f, .6f); // water1 Handle img = renderer->RegisterImage("Textures/WaterExpl.png"); if(cg_reduceSmoke) color.w = .3f; for(int i = 0; i < 7; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), -GetRandom()*7.f)) * 2.5f, .3f, .6f); ent->SetRotation(0.f); ent->SetRadius(1.5f + GetRandom()*GetRandom()*0.4f, 1.3f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(3.f + GetRandom()*0.3f, 0.f, .60f); localEntities.emplace_back(ent); } // water2 img = renderer->RegisterImage("Textures/Fluid.png"); color.w = .9f; if(cg_reduceSmoke) color.w = .4f; for(int i = 0; i < 16; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), -GetRandom()*10.f)) * 3.5f, 1.f, 1.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(0.9f + GetRandom()*GetRandom()*0.4f, 0.7f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(3.f + GetRandom()*0.3f, .7f, .60f); localEntities.emplace_back(ent); } // slow smoke color.w = .4f; if(cg_reduceSmoke) color.w = .2f; for(int i = 0; i < 8; i++){ ParticleSpriteEntity *ent = new SmokeSpriteEntity(this, color, 20.f); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), (GetRandom()-GetRandom()) * .2f)) * 2.f, 1.f, 0.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(1.4f + GetRandom()*GetRandom()*0.8f, 0.2f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime((cg_reduceSmoke ? 3.f : 6.f) + GetRandom() * 5.f, 0.1f, 8.f); localEntities.emplace_back(ent); } // fragments img = renderer->RegisterImage("Gfx/White.tga"); color = MakeVector4(1,1,1, 0.7f); for(int i = 0; i < 42; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); Vector3 dir = MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), -GetRandom() * 3.f); dir += velBias * .5f; float radius = 0.1f + GetRandom()*GetRandom()*0.2f; ent->SetTrajectory(origin + dir * .2f + MakeVector3(0, 0, -1.2f), dir * 13.f, .1f + radius * 3.f, 1.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(radius); ent->SetLifeTime(3.5f + GetRandom() * 2.f, 0.f, 1.f); ent->SetBlockHitAction(ParticleSpriteEntity::Delete); localEntities.emplace_back(ent); } // TODO: wave? } void Client::BulletHitWaterSurface(spades::Vector3 origin){ float dist = (origin - lastSceneDef.viewOrigin).GetLength(); if(dist > 150.f) return; if(!cg_waterImpact) return; Vector4 color; color = MakeVector4( .95f, .95f, .95f, .3f); // water1 Handle img = renderer->RegisterImage("Textures/WaterExpl.png"); if(cg_reduceSmoke) color.w = .2f; for(int i = 0; i < 2; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), -GetRandom()*7.f)) * 1.f, .3f, .6f); ent->SetRotation(0.f); ent->SetRadius(0.6f + GetRandom()*GetRandom()*0.4f, .7f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(3.f + GetRandom()*0.3f, 0.1f, .60f); localEntities.emplace_back(ent); } // water2 img = renderer->RegisterImage("Textures/Fluid.png"); color.w = .9f; if(cg_reduceSmoke) color.w = .4f; for(int i = 0; i < 6; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); ent->SetTrajectory(origin, (MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), -GetRandom()*10.f)) * 2.f, 1.f, 1.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(0.6f + GetRandom()*GetRandom()*0.6f, 0.6f); ent->SetBlockHitAction(ParticleSpriteEntity::Ignore); ent->SetLifeTime(3.f + GetRandom()*0.3f, GetRandom() * 0.3f, .60f); localEntities.emplace_back(ent); } // fragments img = renderer->RegisterImage("Gfx/White.tga"); color = MakeVector4(1,1,1, 0.7f); for(int i = 0; i < 10; i++){ ParticleSpriteEntity *ent = new ParticleSpriteEntity(this, img, color); Vector3 dir = MakeVector3(GetRandom()-GetRandom(), GetRandom()-GetRandom(), -GetRandom() * 3.f); float radius = 0.03f + GetRandom()*GetRandom()*0.05f; ent->SetTrajectory(origin + dir * .2f + MakeVector3(0, 0, -1.2f), dir * 5.f, .1f + radius * 3.f, 1.f); ent->SetRotation(GetRandom() * (float)M_PI * 2.f); ent->SetRadius(radius); ent->SetLifeTime(3.5f + GetRandom() * 2.f, 0.f, 1.f); ent->SetBlockHitAction(ParticleSpriteEntity::Delete); localEntities.emplace_back(ent); } // TODO: wave? } #pragma mark - Camera Control enum { AutoFocusPoints = 4 }; void Client::UpdateAutoFocus(float dt) { if (autoFocusEnabled && world && (int)cg_manualFocus) { // Compute focal length float measureRange = std::tanf(lastSceneDef.fovY * .5f) * .2f; const Vector3 camOrigin = lastSceneDef.viewOrigin; const float lenScale = 1.f / lastSceneDef.viewAxis[2].GetLength(); const Vector3 camDir = lastSceneDef.viewAxis[2].Normalize(); const Vector3 camX = lastSceneDef.viewAxis[0].Normalize() * measureRange; const Vector3 camY = lastSceneDef.viewAxis[1].Normalize() * measureRange; float distances[AutoFocusPoints * AutoFocusPoints]; std::size_t numValidDistances = 0; Vector3 camDir1 = camDir - camX - camY; const Vector3 camDX = camX * (2.f / (AutoFocusPoints - 1)); const Vector3 camDY = camY * (2.f / (AutoFocusPoints - 1)); for (int x = 0; x < AutoFocusPoints; ++x) { Vector3 camDir2 = camDir1; for (int y = 0; y < AutoFocusPoints; ++y) { float dist = RayCastForAutoFocus(camOrigin, camDir2); dist *= lenScale; if (isfinite(dist) && dist > 0.8f) { distances[numValidDistances++] = dist; } camDir2 += camDY; } camDir1 += camDX; } if (numValidDistances > 0) { // Take median std::sort(distances, distances + numValidDistances); float dist = (numValidDistances & 1) ? distances[numValidDistances >> 1] : (distances[numValidDistances >> 1] + distances[(numValidDistances >> 1) - 1]) * 0.5f; targetFocalLength = dist; } } // Change the actual focal length slowly { float dist = 1.f / targetFocalLength; float curDist = 1.f / focalLength; const float maxSpeed = .2f; if (dist > curDist) { curDist = std::min(dist, curDist + maxSpeed * dt); } else { curDist = std::max(dist, curDist - maxSpeed * dt); } focalLength = 1.f / curDist; } } float Client::RayCastForAutoFocus(const Vector3 &origin, const Vector3 &direction) { SPAssert(world); const auto &lastSceneDef = this->lastSceneDef; World::WeaponRayCastResult result = world->WeaponRayCast(origin, direction, nullptr); if (result.hit) { return Vector3::Dot(result.hitPos - origin, lastSceneDef.viewAxis[2]); } return std::nan(nullptr); } } }