diff --git a/Sources/Client/BloodMarks.cpp b/Sources/Client/BloodMarks.cpp new file mode 100644 index 000000000..c2c5213e8 --- /dev/null +++ b/Sources/Client/BloodMarks.cpp @@ -0,0 +1,649 @@ +/* + Copyright (c) 2021 yvt + + 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "BloodMarks.h" + +#include "Client.h" +#include "GameMap.h" +#include "IRenderer.h" +#include "World.h" + +using std::array; +using std::get; +using std::size_t; +using std::tuple; +using std::uint8_t; +using std::vector; +using stmp::optional; + +SPADES_SETTING(cg_blood); + +namespace spades { + namespace client { + namespace { + constexpr size_t kNumFloorMarkVariations = 8; + constexpr size_t kNumWallMarkVariations = 4; + constexpr size_t kNumWallMarkAnimationFrames = 16; + constexpr int kMarkResolution = 64; + + constexpr size_t kNumMarkSoftLimit = 64; + constexpr size_t kNumMarkHardLimit = 32; // + `kNumMarkSoftLimit` + constexpr size_t kLocalPlayerMarkAllocation = 48; + constexpr size_t kNumEmergencySlots = 16; + + static_assert(kLocalPlayerMarkAllocation < kNumMarkSoftLimit, + "kLocalPlayerMarkAllocation < kNumMarkSoftLimit"); + + struct Assets { + static const Assets &Get(); + Assets(); + + vector> models; + size_t lowSpeedFloorMarkStart; + size_t highSpeedFloorMarkStart; + size_t wallMarkStart; + + array orientations; + + size_t SampleModelIndexForLowSpeedFloorMark() const { + return this->lowSpeedFloorMarkStart + + SampleRandomInt(0, kNumFloorMarkVariations - 1); + } + size_t SampleModelIndexForHighSpeedFloorMark() const { + return this->highSpeedFloorMarkStart + + SampleRandomInt(0, kNumFloorMarkVariations - 1); + } + + size_t SampleModelIndexStartForWallMark() const { + return this->wallMarkStart + + SampleRandomInt(0, kNumWallMarkVariations - 1) * + kNumWallMarkAnimationFrames; + } + }; + + struct Mark { + Vector3 position; + /** An index into `Assets::orientations` */ + size_t orientationIndex; + /** An index into `Assets::models` */ + size_t modelIndexStart; + size_t modelIndexLenM1; + bool byLocalPlayer; + array anchors; + float fade = 0.0f; + float time = 0.0f; + float score; + }; + + bool IsBloodMarksEnabled() { return cg_blood.operator int() >= 2; } + + /** Project a particle affected by gravity and return the position where the collision + * with the terrain occurs */ + GameMap::RayCastResult ProjectArc(Vector3 position, Vector3 velocity, float maxTime, + GameMap &map) { + GameMap::RayCastResult result; + result.hit = false; + + Vector3 startPosition = position; + + constexpr float kTimeStep = 0.2f; + for (float time = 0.0f; time < maxTime; time += kTimeStep) { + float const newTime = time + kTimeStep; + Vector3 newPosition = startPosition + velocity * time; + newPosition.z += newTime * newTime * (32.0f / 2.0f); + + int const maxSteps = + (newPosition.Floor() - position.Floor()).GetManhattanLength() + 1; + Vector3 const direction = newPosition - position; + result = map.CastRay2(position, newPosition - position, maxSteps); + if (result.hit) { + // Filter out-of-range hit + float hitDistance = Vector3::Dot(result.hitPos - position, direction); + float rayLength = Vector3::Dot(direction, direction); + if (hitDistance > rayLength) { + result.hit = false; + } + } + if (result.hit) { + break; + } + + position = newPosition; + } + + return result; + } + } // namespace + + struct BloodMarksImpl { + Client &client; + const Assets &assets; + vector> models; + vector> marks; + vector tmpIndices; + size_t markNextIndex = 0; + + BloodMarksImpl(Client &client) : client{client}, assets{Assets::Get()} {} + }; + + const Assets &Assets::Get() { + // Create it on first use, not during the program startup + static Assets global; + return global; + } + + Assets::Assets() { + struct Simulator { + array, kMarkResolution> heightMap; + + void ClearAndRespatter(bool highSpeed) { + heightMap = decltype(heightMap){}; + + if (highSpeed) { + for (size_t k = 0; k < 20; ++k) { + // Approximated gaussian distribution + float x = + SampleRandomFloat() + SampleRandomFloat() + SampleRandomFloat(); + float y = + SampleRandomFloat() + SampleRandomFloat() + SampleRandomFloat(); + int quantity = SampleRandomInt(1, 4); + x = (x / 3.0f) * float(kMarkResolution - 2) + 1.0f; + y = (y / 3.0f) * float(kMarkResolution - 2) + 1.0f; + + x += (float(kMarkResolution) * 0.5f - x) * (float(quantity) / 20.0f); + y += (float(kMarkResolution) * 0.5f - y) * (float(quantity) / 20.0f); + + size_t ix = size_t(x) % kMarkResolution, + iy = size_t(y) % kMarkResolution; + heightMap.at(iy).at(ix) += quantity; + } + } else { + for (size_t k = 0; k < 6; ++k) { + // Approximated gaussian distribution + float x = SampleRandomFloat() + SampleRandomFloat() + + SampleRandomFloat() + SampleRandomFloat() + + SampleRandomFloat(); + float y = SampleRandomFloat() + SampleRandomFloat() + + SampleRandomFloat() + SampleRandomFloat() + + SampleRandomFloat(); + int quantity = SampleRandomInt(1, 12); + x = (x / 5.0f) * float(kMarkResolution - 2) + 1.0f; + y = (y / 5.0f) * float(kMarkResolution - 2) + 1.0f; + + x += (float(kMarkResolution) * 0.5f - x) * (float(quantity) / 15.0f); + y += (float(kMarkResolution) * 0.5f - y) * (float(quantity) / 15.0f); + + size_t ix = size_t(x) % kMarkResolution, + iy = size_t(y) % kMarkResolution; + heightMap.at(iy).at(ix) += quantity; + } + } + } + + void Update(bool inclined) { + auto const oldHeightMap = this->heightMap; + for (size_t y = 1; y < kMarkResolution - 1; ++y) { + for (size_t x = 1; x < kMarkResolution - 1; ++x) { + if (oldHeightMap.at(y).at(x) > 1) { + heightMap.at(y).at(x) -= 1; + if (inclined && SampleRandomInt(0, 2) > 0) { + heightMap.at(y + 1).at(x) += 1; + } else { + // Purposefully utilizes the wrap-around behavior of + // unsigned types + size_t dir = SampleRandomInt(0, 3); + size_t dx = size_t(dir & 2) - size_t(1), dy = 0; + if (dir & 1) { + dy = dx; + dx = 0; + } + heightMap.at(y + dx).at(x + dy) += 1; + } + } + } + } + } + + Handle ToModel() { + auto model = Handle::New(kMarkResolution, 1, kMarkResolution); + for (size_t y = 0; y < kMarkResolution; ++y) { + for (size_t x = 0; x < kMarkResolution; ++x) { + if (heightMap.at(y).at(x) > 0) { + model->SetSolid(x, 0, y, 0x000000); + } + } + } + model->SetOrigin(Vector3{float(kMarkResolution) * -0.5f, 0.0f, + float(kMarkResolution) * -0.5f}); + return model; + } + }; + + Simulator sim; + + lowSpeedFloorMarkStart = models.size(); + for (size_t i = 0; i < kNumFloorMarkVariations; ++i) { + sim.ClearAndRespatter(false); + for (int k = 0; k < 20; ++k) { + sim.Update(false); + } + this->models.push_back(sim.ToModel()); + } + + highSpeedFloorMarkStart = models.size(); + for (size_t i = 0; i < kNumFloorMarkVariations; ++i) { + sim.ClearAndRespatter(true); + for (int k = 0; k < 20; ++k) { + sim.Update(false); + } + this->models.push_back(sim.ToModel()); + } + + wallMarkStart = models.size(); + for (size_t i = 0; i < kNumWallMarkVariations; ++i) { + sim.ClearAndRespatter(true); + for (size_t k = 0; k < kNumWallMarkAnimationFrames; ++k) { + sim.Update(true); + this->models.push_back(sim.ToModel()); + } + } + + // Wall + orientations.at(0) = Matrix4::Translate(0.0f, -0.2f, 0.0f); // make it just visible + + // Floor and ceiling + orientations.at(4) = Matrix4::Rotate(Vector3{1.0f, 0.0f, 0.0f}, float(M_PI) * -0.5f) * + Matrix4::Translate(0.0f, -0.2f, 0.0f); + orientations.at(8) = Matrix4::Rotate(Vector3{1.0f, 0.0f, 0.0f}, float(M_PI) * 0.5f) * + Matrix4::Translate(0.0f, -0.2f, 0.0f); + + for (size_t i = 0; i < 12; i += 4) { + for (size_t k = 1; k < 4; ++k) { + orientations.at(i + k) = + Matrix4::Rotate(Vector3{0.0f, 0.0f, 1.0f}, float(M_PI) * 0.5f * float(k)) * + orientations.at(i); + } + } + + for (size_t i = 0; i < 12; ++i) { + orientations.at(i) = + Matrix4::Scale(1.0f / float(kMarkResolution)) * orientations.at(i); + } + } + + BloodMarks::BloodMarks(Client &client) : impl{new BloodMarksImpl{client}} { + // Materialize the models + BloodMarksImpl &impl = *this->impl; + const Assets &assets = impl.assets; + std::transform(assets.models.begin(), assets.models.end(), + std::back_inserter(impl.models), [&](Handle const &model) { + return client.GetRenderer().CreateModel(*model); + }); + + // Reserve slots + for (size_t i = 0; i < kNumMarkSoftLimit + kNumMarkHardLimit + kNumEmergencySlots; + ++i) { + impl.marks.emplace_back(); + } + } + + BloodMarks::~BloodMarks() {} + + void BloodMarks::Clear() { + for (auto &e : this->impl->marks) { + e.reset(); + } + } + + void BloodMarks::Spatter(const Vector3 &position, const Vector3 &velocity, + bool byLocalPlayer) { + BloodMarksImpl &impl = *this->impl; + Client &client = impl.client; + if (!client.GetWorld() || !client.GetWorld()->GetMap() || !IsBloodMarksEnabled()) { + return; + } + Handle map = client.GetWorld()->GetMap(); + + bool isHighSpeed = velocity.GetLength() > 3.0f; + + array energyDistribution; + for (float &e : energyDistribution) { + e = SampleRandomFloat() + 0.3f; + } + + { + float const scale = + float(energyDistribution.size()) / + std::accumulate(energyDistribution.begin(), energyDistribution.end(), 0.0f); + for (float &e : energyDistribution) { + e *= scale; + } + } + + for (const float &e : energyDistribution) { + for (int attempt = 0; attempt < 3; ++attempt) { + // Draw a trajectory + Vector3 particleVelocity = velocity; + particleVelocity *= e; + particleVelocity += Vector3{SampleRandomFloat() - SampleRandomFloat(), + SampleRandomFloat() - SampleRandomFloat(), + SampleRandomFloat() - SampleRandomFloat()} * + (velocity.GetLength() * 0.2f); + GameMap::RayCastResult const hit = + ProjectArc(position, particleVelocity, 1.4f, *map); + if (!hit.hit) { + continue; + } + + if (hit.hitBlock.z >= 62) { + // Water + break; + } + + // Find the anchor voxels + array anchors; + anchors.at(0) = (hit.hitPos - 0.5f).Floor(); + if (hit.normal.x != 0) { + anchors.at(0).x = hit.hitBlock.x; + } + if (hit.normal.y != 0) { + anchors.at(0).y = hit.hitBlock.y; + } + if (hit.normal.z != 0) { + anchors.at(0).z = hit.hitBlock.z; + } + for (size_t i = 1; i < 4; ++i) { + size_t bits = i; + if (hit.normal.x != 0) { + bits = (bits << 1); + } else if (hit.normal.y != 0) { + bits = ((bits | (bits << 1)) & 0b101); + } + anchors.at(i) = anchors.at(0); + anchors.at(i).x += int(bits) & 1; + anchors.at(i).y += int(bits >> 1) & 1; + anchors.at(i).z += int(bits >> 2) & 1; + } + + // Are the anchor voxels present + bool const anchorPresent = + std::all_of(anchors.begin(), anchors.end(), [&](const IntVector3 &p) { + return map->IsSolidWrapped(p.x, p.y, p.z); + }); + if (!anchorPresent) { + continue; + } + + // Place the new mark + bool found = false; + for (size_t i = 0; i < impl.marks.size(); ++i) { + if (!impl.marks.at(impl.markNextIndex)) { + found = true; + break; + } + if ((++impl.markNextIndex) == impl.marks.size()) { + impl.markNextIndex = 0; + } + } + + if (!found) { + // Out of slots + return; + } + + // Fill the slot + impl.marks.at(impl.markNextIndex).reset(Mark{}); // FIXME: #972 + Mark &mark = impl.marks.at(impl.markNextIndex).value(); + + mark.position = hit.hitPos; + mark.anchors = anchors; + mark.byLocalPlayer = byLocalPlayer; + + if (hit.normal.z != 0) { + + if (hit.normal.z > 0) { + mark.orientationIndex = SampleRandomInt(8, 11); + } else { + mark.orientationIndex = SampleRandomInt(4, 7); + } + mark.modelIndexStart = + isHighSpeed ? impl.assets.SampleModelIndexForHighSpeedFloorMark() + : impl.assets.SampleModelIndexForLowSpeedFloorMark(); + mark.modelIndexLenM1 = 0; + } else { + if (hit.normal.y > 0) { + mark.orientationIndex = 0; + } else if (hit.normal.y < 0) { + mark.orientationIndex = 2; + } else if (hit.normal.x > 0) { + mark.orientationIndex = 3; + } else { + mark.orientationIndex = 1; + } + mark.modelIndexStart = impl.assets.SampleModelIndexStartForWallMark(); + mark.modelIndexLenM1 = kNumWallMarkAnimationFrames - 1; + } + + break; + } + } + } + + void BloodMarks::Update(float dt) { + BloodMarksImpl &impl = *this->impl; + Client &client = impl.client; + if (!client.GetWorld() || !client.GetWorld()->GetMap() || !IsBloodMarksEnabled()) { + this->Clear(); + return; + } + + Handle const map = client.GetWorld()->GetMap(); + + vector> &marks = impl.marks; + + vector &tmpIndices = impl.tmpIndices; + tmpIndices.resize(marks.size()); + + // Evaluate each mark's importance for eviction + SceneDefinition const sceneDef = client.GetLastSceneDef(); + for (auto &slot : marks) { + if (slot) { + Mark &mark = slot.value(); + + float distance = (mark.position - sceneDef.viewOrigin).GetLength(); + if (distance > 150.0f) { + // Distance culled + distance += (distance - 150.0f) * 10.0f; + } + + // Looking away? + float dot = Vector3::Dot((mark.position - sceneDef.viewOrigin).Normalize(), + sceneDef.viewAxis[2]); + dot = std::max(0.5f - dot, 0.0f) * 20.0f; + + mark.score = distance + dot + mark.fade * 50.0f + mark.time * 2.0f; + } + } + + // Hard eviction + size_t numMarks = std::count_if( + marks.begin(), marks.end(), [](const stmp::optional &slot) { return !!slot; }); + if (numMarks > kNumMarkSoftLimit + kNumMarkHardLimit) { + { + size_t outIndex = 0; + for (size_t i = 0; i < marks.size(); ++i) { + if (marks.at(i)) { + tmpIndices.at(outIndex++) = i; + } + } + assert(outIndex == numMarks); + } + + // Choose the `numEvicted` elements with the largest `score`s + // ("largest" hence the reversed comparer) + size_t numEvicted = numMarks - (kNumMarkSoftLimit + kNumMarkHardLimit); + std::partial_sort(tmpIndices.begin(), tmpIndices.begin() + numEvicted, + tmpIndices.begin() + numMarks, + [&](const size_t &a, const size_t &b) { + return marks.at(a).get().score > marks.at(b).get().score; + }); + + for (size_t i = 0; i < numEvicted; ++i) { + marks.at(tmpIndices.at(i)).reset(); + } + + numMarks = kNumMarkSoftLimit + kNumMarkHardLimit; + } + + // Soft eviction + size_t const numByLocalPlayerMarks = + std::count_if(marks.begin(), marks.end(), [](const stmp::optional &slot) { + return slot && slot.get().byLocalPlayer; + }); + + if (numByLocalPlayerMarks > kLocalPlayerMarkAllocation) { + // A certain number of slots are reserved for by-local-player marks. If there are + // more than that number of by-local-player marks, they are counted as non-by-local- + // player marks. + { + size_t outIndex = 0; + for (size_t i = 0; i < marks.size(); ++i) { + if (marks.at(i) && marks.at(i).get().byLocalPlayer) { + tmpIndices.at(outIndex++) = i; + } + } + assert(outIndex == numByLocalPlayerMarks); + } + + // Choose the `numExceeded` elements with the largest `score`s + // ("largest" hence the reversed comparer) + size_t const numExceeded = numByLocalPlayerMarks - kLocalPlayerMarkAllocation; + std::partial_sort(tmpIndices.begin(), tmpIndices.begin() + numExceeded, + tmpIndices.begin() + numByLocalPlayerMarks, + [&](const size_t &a, const size_t &b) { + return marks.at(a).get().score > marks.at(b).get().score; + }); + + // Assign negative infinity to the `kLocalPlayerMarkAllocation` elements with + // the least `score`s + for (size_t i = numExceeded; i < numByLocalPlayerMarks; ++i) { + marks.at(tmpIndices.at(i)).get().score = -INFINITY; + } + } + + if (numMarks > kNumMarkSoftLimit) { + { + size_t outIndex = 0; + for (size_t i = 0; i < marks.size(); ++i) { + if (marks.at(i)) { + tmpIndices.at(outIndex++) = i; + } + } + assert(outIndex == numMarks); + } + + // Choose the `numEvicted` elements with the largest `score`s + // ("largest" hence the reversed comparer) + size_t const numEvicted = numMarks - kNumMarkSoftLimit; + std::partial_sort(tmpIndices.begin(), tmpIndices.begin() + numEvicted, + tmpIndices.begin() + numMarks, + [&](const size_t &a, const size_t &b) { + return marks.at(a).get().score > marks.at(b).get().score; + }); + + for (size_t i = 0; i < numEvicted; ++i) { + Mark &mark = marks.at(tmpIndices.at(i)).value(); + mark.fade += dt; + if (mark.fade >= 1.0f) { + marks.at(tmpIndices.at(i)).reset(); + } + } + } + + // Miscellaneous + for (auto &slot : marks) { + if (slot) { + Mark &mark = slot.value(); + mark.time += dt; + + // Check the anchor voxels' existence + const auto &anchors = mark.anchors; + bool const anchorPresent = + std::all_of(anchors.begin(), anchors.end(), [&](const IntVector3 &p) { + return map->IsSolidWrapped(p.x, p.y, p.z); + }); + if (!anchorPresent) { + slot.reset(); + } + } + } + } + + void BloodMarks::Draw() { + if (!IsBloodMarksEnabled()) { + return; + } + + BloodMarksImpl &impl = *this->impl; + Client &client = impl.client; + IRenderer &r = client.GetRenderer(); + + ModelRenderParam modelParam; + modelParam.customColor = Vector3{0.7f, 0.07f, 0.01f}; // hemoglobin + + for (auto &slot : impl.marks) { + if (slot) { + Mark &mark = slot.value(); + + // Decide the model for this mark + float frame = + std::min(std::max(mark.time * 5.0f, 0.0f), float(mark.modelIndexLenM1)); + size_t modelIndex = mark.modelIndexStart + size_t(frame); + IModel &model = *impl.models.at(modelIndex); + + // Render the model + modelParam.matrix = impl.assets.orientations.at(mark.orientationIndex); + + if (mark.fade > 0.0f) { + // Sink into the terrain + // TODO: ... which turned out to be not a great idea + modelParam.matrix *= Matrix4::Translate(0.0f, mark.fade * -0.3f, 0.0f); + } + + modelParam.matrix = Matrix4::Translate(mark.position) * modelParam.matrix; + + r.RenderModel(model, modelParam); + } + } + } + } // namespace client +} // namespace spades \ No newline at end of file diff --git a/Sources/Client/BloodMarks.h b/Sources/Client/BloodMarks.h new file mode 100644 index 000000000..b96ebd8f3 --- /dev/null +++ b/Sources/Client/BloodMarks.h @@ -0,0 +1,52 @@ +/* + Copyright (c) 2021 yvt + + 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 . + + */ + +#pragma once + +#include + +#include + +namespace spades { + namespace client { + struct BloodMarksImpl; + class Client; + + class BloodMarks { + std::unique_ptr impl; + + public: + BloodMarks(Client &); + ~BloodMarks(); + + /** Remove all local entities. */ + void Clear(); + + /** Oh no, someone got hurt by a high speed projectile! */ + void Spatter(const Vector3 &position, const Vector3 &velocity, bool byLocalPlayer); + + /** Update the entities' states. */ + void Update(float dt); + + /** Issue drawing commands. */ + void Draw(); + }; + } // namespace client +} // namespace spades diff --git a/Sources/Client/Client.cpp b/Sources/Client/Client.cpp index 72f1b2bc0..739f823f9 100644 --- a/Sources/Client/Client.cpp +++ b/Sources/Client/Client.cpp @@ -44,6 +44,7 @@ #include "ScoreboardView.h" #include "TCProgressView.h" +#include "BloodMarks.h" #include "Corpse.h" #include "ILocalEntity.h" #include "SmokeSpriteEntity.h" @@ -129,6 +130,7 @@ namespace spades { scriptedUI = Handle::New(renderer.GetPointerOrNull(), audioDev.GetPointerOrNull(), fontManager.GetPointerOrNull(), this); + bloodMarks = stmp::make_unique(*this); renderer->SetGameMap(nullptr); } diff --git a/Sources/Client/Client.h b/Sources/Client/Client.h index ba3760e0a..6a61c837d 100644 --- a/Sources/Client/Client.h +++ b/Sources/Client/Client.h @@ -65,6 +65,7 @@ namespace spades { class PaletteView; class TCProgressView; class ClientPlayer; + class BloodMarks; class ClientUI; @@ -330,6 +331,8 @@ namespace spades { void RemoveAllLocalEntities(); void RemoveCorpseForPlayer(int playerId); + std::unique_ptr bloodMarks; + int nextScreenShotIndex; int nextMapShotIndex; diff --git a/Sources/Client/Client_LocalEnts.cpp b/Sources/Client/Client_LocalEnts.cpp index 8b27d2e53..296b5c04d 100644 --- a/Sources/Client/Client_LocalEnts.cpp +++ b/Sources/Client/Client_LocalEnts.cpp @@ -32,6 +32,7 @@ #include "IAudioChunk.h" #include "IAudioDevice.h" +#include "BloodMarks.h" #include "CenterMessageView.h" #include "ChatWindow.h" #include "ClientPlayer.h" @@ -75,6 +76,7 @@ namespace spades { SPADES_MARK_FUNCTION(); localEntities.clear(); + bloodMarks->Clear(); } void Client::RemoveInvisibleCorpses() { diff --git a/Sources/Client/Client_Scene.cpp b/Sources/Client/Client_Scene.cpp index a9397b5af..f77ea79f8 100644 --- a/Sources/Client/Client_Scene.cpp +++ b/Sources/Client/Client_Scene.cpp @@ -25,6 +25,7 @@ #include #include +#include "BloodMarks.h" #include "CTFGameMode.h" #include "Corpse.h" #include "GameMap.h" @@ -635,6 +636,8 @@ namespace spades { } } + this->bloodMarks->Draw(); + // Draw block cursor if (p) { if (p->IsReadyToUseTool() && p->GetTool() == Player::ToolBlock && diff --git a/Sources/Client/Client_Update.cpp b/Sources/Client/Client_Update.cpp index ca07115a3..62b67275d 100644 --- a/Sources/Client/Client_Update.cpp +++ b/Sources/Client/Client_Update.cpp @@ -28,6 +28,7 @@ #include "IAudioChunk.h" #include "IAudioDevice.h" +#include "BloodMarks.h" #include "CenterMessageView.h" #include "ChatWindow.h" #include "ClientPlayer.h" @@ -238,6 +239,8 @@ namespace spades { } } + this->bloodMarks->Update(dt); + corpseDispatch.Join(); if (grenadeVibration > 0.f) { @@ -960,6 +963,37 @@ namespace spades { SPAssert(type != HitTypeBlock); + // spatter blood + { + bool const byLocalPlayer = &by == world->GetLocalPlayer(); + + float const distance = (by.GetEye() - hitPos).GetLength(); + Vector3 const direction = (by.GetEye() - hitPos).Normalize(); + + float frontSpeed = 8.0f; + float backSpeed = 0.0f; + + if (type == HitTypeMelee) { + // Blunt + frontSpeed = 1.5f; + } else if (by.GetWeaponType() == RIFLE_WEAPON) { + // Penetrating + frontSpeed = 1.0f; + backSpeed = 21.0f; + } else if (by.GetWeaponType() == SMG_WEAPON && distance < 20.0f * SampleRandomFloat()) { + // Penetrating + frontSpeed = 1.0f; + backSpeed = 12.0f; + } + + if (frontSpeed > 0.0f) { + bloodMarks->Spatter(hitPos, direction * frontSpeed, byLocalPlayer); + } + if (backSpeed > 0.0f) { + bloodMarks->Spatter(hitPos, direction * -backSpeed, byLocalPlayer); + } + } + // don't bleed local player if (!IsFirstPerson(GetCameraMode()) || &GetCameraTargetPlayer() != &hurtPlayer) { Bleed(hitPos);