2026-02-03 14:55:32 -08:00
|
|
|
#include "audio/footstep_manager.hpp"
|
2026-02-09 00:40:50 -08:00
|
|
|
#include "audio/audio_engine.hpp"
|
2026-02-03 14:55:32 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace audio {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> buildClassicFootstepSet(const std::string& material) {
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
|
for (char c = 'A'; c <= 'L'; ++c) {
|
|
|
|
|
out.push_back("Sound\\Character\\Footsteps\\mFootMediumLarge" + material + std::string(1, c) + ".wav");
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> buildAltFootstepSet(const std::string& folder, const std::string& stem) {
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
|
for (int i = 1; i <= 8; ++i) {
|
|
|
|
|
char index[4];
|
|
|
|
|
std::snprintf(index, sizeof(index), "%02d", i);
|
|
|
|
|
out.push_back("Sound\\Character\\Footsteps\\" + folder + "\\" + stem + "_" + index + ".wav");
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
FootstepManager::FootstepManager() : rng(std::random_device{}()) {}
|
|
|
|
|
|
|
|
|
|
FootstepManager::~FootstepManager() {
|
|
|
|
|
shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FootstepManager::initialize(pipeline::AssetManager* assets) {
|
|
|
|
|
assetManager = assets;
|
|
|
|
|
sampleCount = 0;
|
|
|
|
|
for (auto& surface : surfaces) {
|
|
|
|
|
surface.clips.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!assetManager) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
preloadSurface(FootstepSurface::STONE, buildClassicFootstepSet("Stone"));
|
|
|
|
|
preloadSurface(FootstepSurface::DIRT, buildClassicFootstepSet("Dirt"));
|
|
|
|
|
preloadSurface(FootstepSurface::GRASS, buildClassicFootstepSet("Grass"));
|
|
|
|
|
preloadSurface(FootstepSurface::WOOD, buildClassicFootstepSet("Wood"));
|
|
|
|
|
preloadSurface(FootstepSurface::SNOW, buildClassicFootstepSet("Snow"));
|
|
|
|
|
preloadSurface(FootstepSurface::WATER, buildClassicFootstepSet("Water"));
|
|
|
|
|
|
|
|
|
|
// Alternate naming seen in some builds (especially metals).
|
|
|
|
|
preloadSurface(FootstepSurface::METAL, buildAltFootstepSet("MediumLargeMetalFootsteps", "MediumLargeFootstepMetal"));
|
|
|
|
|
if (surfaces[static_cast<size_t>(FootstepSurface::METAL)].clips.empty()) {
|
|
|
|
|
preloadSurface(FootstepSurface::METAL, buildClassicFootstepSet("Metal"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Footstep manager initialized (", sampleCount, " clips)");
|
|
|
|
|
return sampleCount > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::shutdown() {
|
|
|
|
|
for (auto& surface : surfaces) {
|
|
|
|
|
surface.clips.clear();
|
|
|
|
|
}
|
|
|
|
|
sampleCount = 0;
|
|
|
|
|
assetManager = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::update(float) {
|
2026-02-09 00:40:50 -08:00
|
|
|
// No longer needed - AudioEngine handles cleanup internally
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::playFootstep(FootstepSurface surface, bool sprinting) {
|
|
|
|
|
if (!assetManager || sampleCount == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-09 00:40:50 -08:00
|
|
|
|
|
|
|
|
// Check if AudioEngine is initialized
|
|
|
|
|
if (!AudioEngine::instance().isInitialized()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
playRandomStep(surface, sprinting);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::preloadSurface(FootstepSurface surface, const std::vector<std::string>& candidates) {
|
|
|
|
|
if (!assetManager) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto& list = surfaces[static_cast<size_t>(surface)].clips;
|
|
|
|
|
for (const std::string& path : candidates) {
|
|
|
|
|
if (!assetManager->fileExists(path)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
auto data = assetManager->readFile(path);
|
|
|
|
|
if (data.empty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
list.push_back({path, std::move(data)});
|
|
|
|
|
sampleCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!list.empty()) {
|
|
|
|
|
LOG_INFO("Footsteps ", surfaceName(surface), ": loaded ", list.size(), " clips");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
2026-02-03 17:21:04 -08:00
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
if (lastPlayTime.time_since_epoch().count() != 0) {
|
|
|
|
|
float elapsed = std::chrono::duration<float>(now - lastPlayTime).count();
|
|
|
|
|
float minInterval = sprinting ? 0.09f : 0.14f;
|
|
|
|
|
if (elapsed < minInterval) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
auto& list = surfaces[static_cast<size_t>(surface)].clips;
|
|
|
|
|
if (list.empty()) {
|
|
|
|
|
list = surfaces[static_cast<size_t>(FootstepSurface::STONE)].clips;
|
|
|
|
|
if (list.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// Pick a random clip
|
2026-02-03 14:55:32 -08:00
|
|
|
std::uniform_int_distribution<size_t> clipDist(0, list.size() - 1);
|
|
|
|
|
const Sample& sample = list[clipDist(rng)];
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// Subtle variation for less repetitive cadence
|
2026-02-03 14:55:32 -08:00
|
|
|
std::uniform_real_distribution<float> pitchDist(0.97f, 1.05f);
|
|
|
|
|
std::uniform_real_distribution<float> volumeDist(0.92f, 1.00f);
|
|
|
|
|
float pitch = pitchDist(rng);
|
2026-02-05 17:32:21 -08:00
|
|
|
float volume = volumeDist(rng) * (sprinting ? 1.0f : 0.88f) * volumeScale;
|
2026-02-03 14:55:32 -08:00
|
|
|
if (volume > 1.0f) volume = 1.0f;
|
|
|
|
|
if (volume < 0.1f) volume = 0.1f;
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
// Play using AudioEngine (non-blocking, no process spawn!)
|
|
|
|
|
bool success = AudioEngine::instance().playSound2D(sample.data, volume, pitch);
|
2026-02-03 22:24:17 -08:00
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
if (success) {
|
2026-02-03 17:21:04 -08:00
|
|
|
lastPlayTime = now;
|
2026-02-03 14:55:32 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* FootstepManager::surfaceName(FootstepSurface surface) {
|
|
|
|
|
switch (surface) {
|
|
|
|
|
case FootstepSurface::STONE: return "stone";
|
|
|
|
|
case FootstepSurface::DIRT: return "dirt";
|
|
|
|
|
case FootstepSurface::GRASS: return "grass";
|
|
|
|
|
case FootstepSurface::WOOD: return "wood";
|
|
|
|
|
case FootstepSurface::METAL: return "metal";
|
|
|
|
|
case FootstepSurface::WATER: return "water";
|
|
|
|
|
case FootstepSurface::SNOW: return "snow";
|
|
|
|
|
default: return "unknown";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace audio
|
|
|
|
|
} // namespace wowee
|