mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add WoW-style footsteps and improve third-person movement/collision
This commit is contained in:
parent
54dc27c2ec
commit
dfb1f3cfdc
11 changed files with 724 additions and 85 deletions
|
|
@ -96,6 +96,7 @@ set(WOWEE_SOURCES
|
||||||
|
|
||||||
# Audio
|
# Audio
|
||||||
src/audio/music_manager.cpp
|
src/audio/music_manager.cpp
|
||||||
|
src/audio/footstep_manager.cpp
|
||||||
|
|
||||||
# Pipeline (asset loaders)
|
# Pipeline (asset loaders)
|
||||||
src/pipeline/mpq_manager.cpp
|
src/pipeline/mpq_manager.cpp
|
||||||
|
|
@ -174,6 +175,7 @@ set(WOWEE_HEADERS
|
||||||
include/game/inventory.hpp
|
include/game/inventory.hpp
|
||||||
|
|
||||||
include/audio/music_manager.hpp
|
include/audio/music_manager.hpp
|
||||||
|
include/audio/footstep_manager.hpp
|
||||||
|
|
||||||
include/pipeline/mpq_manager.hpp
|
include/pipeline/mpq_manager.hpp
|
||||||
include/pipeline/blp_loader.hpp
|
include/pipeline/blp_loader.hpp
|
||||||
|
|
|
||||||
65
include/audio/footstep_manager.hpp
Normal file
65
include/audio/footstep_manager.hpp
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
||||||
|
namespace audio {
|
||||||
|
|
||||||
|
enum class FootstepSurface : uint8_t {
|
||||||
|
STONE = 0,
|
||||||
|
DIRT,
|
||||||
|
GRASS,
|
||||||
|
WOOD,
|
||||||
|
METAL,
|
||||||
|
WATER,
|
||||||
|
SNOW
|
||||||
|
};
|
||||||
|
|
||||||
|
class FootstepManager {
|
||||||
|
public:
|
||||||
|
FootstepManager();
|
||||||
|
~FootstepManager();
|
||||||
|
|
||||||
|
bool initialize(pipeline::AssetManager* assets);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
void update(float deltaTime);
|
||||||
|
void playFootstep(FootstepSurface surface, bool sprinting);
|
||||||
|
|
||||||
|
bool isInitialized() const { return assetManager != nullptr; }
|
||||||
|
bool hasAnySamples() const { return sampleCount > 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Sample {
|
||||||
|
std::string path;
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SurfaceSamples {
|
||||||
|
std::vector<Sample> clips;
|
||||||
|
};
|
||||||
|
|
||||||
|
void preloadSurface(FootstepSurface surface, const std::vector<std::string>& candidates);
|
||||||
|
void stopCurrentProcess();
|
||||||
|
void reapFinishedProcess();
|
||||||
|
bool playRandomStep(FootstepSurface surface, bool sprinting);
|
||||||
|
static const char* surfaceName(FootstepSurface surface);
|
||||||
|
|
||||||
|
pipeline::AssetManager* assetManager = nullptr;
|
||||||
|
SurfaceSamples surfaces[7];
|
||||||
|
size_t sampleCount = 0;
|
||||||
|
|
||||||
|
std::string tempFilePath = "/tmp/wowee_footstep.wav";
|
||||||
|
pid_t playerPid = -1;
|
||||||
|
|
||||||
|
std::mt19937 rng;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace audio
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -122,12 +122,16 @@ private:
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
bool sitting = false;
|
bool sitting = false;
|
||||||
bool xKeyWasDown = false;
|
bool xKeyWasDown = false;
|
||||||
|
bool rKeyWasDown = false;
|
||||||
|
bool runPace = false;
|
||||||
|
|
||||||
// Movement state tracking (for sending opcodes on state change)
|
// Movement state tracking (for sending opcodes on state change)
|
||||||
bool wasMovingForward = false;
|
bool wasMovingForward = false;
|
||||||
bool wasMovingBackward = false;
|
bool wasMovingBackward = false;
|
||||||
bool wasStrafingLeft = false;
|
bool wasStrafingLeft = false;
|
||||||
bool wasStrafingRight = false;
|
bool wasStrafingRight = false;
|
||||||
|
bool wasTurningLeft = false;
|
||||||
|
bool wasTurningRight = false;
|
||||||
bool wasJumping = false;
|
bool wasJumping = false;
|
||||||
bool wasFalling = false;
|
bool wasFalling = false;
|
||||||
|
|
||||||
|
|
@ -136,10 +140,11 @@ private:
|
||||||
|
|
||||||
// Movement speeds
|
// Movement speeds
|
||||||
bool useWoWSpeed = false;
|
bool useWoWSpeed = false;
|
||||||
static constexpr float WOW_RUN_SPEED = 14.0f; // Normal run (WASD)
|
static constexpr float WOW_RUN_SPEED = 7.0f; // Normal run (WotLK)
|
||||||
static constexpr float WOW_SPRINT_SPEED = 28.0f; // Sprint (hold Shift)
|
static constexpr float WOW_SPRINT_SPEED = 10.5f; // Optional fast mode (not default WoW behavior)
|
||||||
static constexpr float WOW_WALK_SPEED = 5.0f; // Walk (hold Ctrl)
|
static constexpr float WOW_WALK_SPEED = 2.5f; // Walk
|
||||||
static constexpr float WOW_BACK_SPEED = 9.0f; // Backpedal
|
static constexpr float WOW_BACK_SPEED = 4.5f; // Backpedal
|
||||||
|
static constexpr float WOW_TURN_SPEED = 180.0f; // Keyboard turn deg/sec
|
||||||
static constexpr float WOW_GRAVITY = -19.29f;
|
static constexpr float WOW_GRAVITY = -19.29f;
|
||||||
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ public:
|
||||||
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
||||||
void setInstanceVisible(uint32_t instanceId, bool visible);
|
void setInstanceVisible(uint32_t instanceId, bool visible);
|
||||||
void removeInstance(uint32_t instanceId);
|
void removeInstance(uint32_t instanceId);
|
||||||
|
bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const;
|
||||||
|
|
||||||
/** Attach a weapon model to a character instance at the given attachment point. */
|
/** Attach a weapon model to a character instance at the given attachment point. */
|
||||||
bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace core { class Window; }
|
namespace core { class Window; }
|
||||||
namespace game { class World; class ZoneManager; }
|
namespace game { class World; class ZoneManager; }
|
||||||
namespace audio { class MusicManager; }
|
namespace audio { class MusicManager; class FootstepManager; enum class FootstepSurface : uint8_t; }
|
||||||
namespace pipeline { class AssetManager; }
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -142,6 +142,7 @@ private:
|
||||||
std::unique_ptr<M2Renderer> m2Renderer;
|
std::unique_ptr<M2Renderer> m2Renderer;
|
||||||
std::unique_ptr<Minimap> minimap;
|
std::unique_ptr<Minimap> minimap;
|
||||||
std::unique_ptr<audio::MusicManager> musicManager;
|
std::unique_ptr<audio::MusicManager> musicManager;
|
||||||
|
std::unique_ptr<audio::FootstepManager> footstepManager;
|
||||||
std::unique_ptr<game::ZoneManager> zoneManager;
|
std::unique_ptr<game::ZoneManager> zoneManager;
|
||||||
|
|
||||||
pipeline::AssetManager* cachedAssetManager = nullptr;
|
pipeline::AssetManager* cachedAssetManager = nullptr;
|
||||||
|
|
@ -157,6 +158,9 @@ private:
|
||||||
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM };
|
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM };
|
||||||
CharAnimState charAnimState = CharAnimState::IDLE;
|
CharAnimState charAnimState = CharAnimState::IDLE;
|
||||||
void updateCharacterAnimation();
|
void updateCharacterAnimation();
|
||||||
|
bool isFootstepAnimationState() const;
|
||||||
|
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
||||||
|
audio::FootstepSurface resolveFootstepSurface() const;
|
||||||
|
|
||||||
// Emote state
|
// Emote state
|
||||||
bool emoteActive = false;
|
bool emoteActive = false;
|
||||||
|
|
@ -166,6 +170,11 @@ private:
|
||||||
// Target facing
|
// Target facing
|
||||||
const glm::vec3* targetPosition = nullptr;
|
const glm::vec3* targetPosition = nullptr;
|
||||||
|
|
||||||
|
// Footstep event tracking (animation-driven)
|
||||||
|
uint32_t footstepLastAnimationId = 0;
|
||||||
|
float footstepLastNormTime = 0.0f;
|
||||||
|
bool footstepNormInitialized = false;
|
||||||
|
|
||||||
bool terrainEnabled = true;
|
bool terrainEnabled = true;
|
||||||
bool terrainLoaded = false;
|
bool terrainLoaded = false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,12 @@ public:
|
||||||
*/
|
*/
|
||||||
std::optional<float> getHeightAt(float glX, float glY) const;
|
std::optional<float> getHeightAt(float glX, float glY) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dominant terrain texture name at a GL position.
|
||||||
|
* Returns empty if terrain is not loaded at that position.
|
||||||
|
*/
|
||||||
|
std::optional<std::string> getDominantTextureAt(float glX, float glY) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get statistics
|
* Get statistics
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
204
src/audio/footstep_manager.cpp
Normal file
204
src/audio/footstep_manager.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#include "audio/footstep_manager.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
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() {
|
||||||
|
stopCurrentProcess();
|
||||||
|
std::remove(tempFilePath.c_str());
|
||||||
|
for (auto& surface : surfaces) {
|
||||||
|
surface.clips.clear();
|
||||||
|
}
|
||||||
|
sampleCount = 0;
|
||||||
|
assetManager = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FootstepManager::update(float) {
|
||||||
|
reapFinishedProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FootstepManager::playFootstep(FootstepSurface surface, bool sprinting) {
|
||||||
|
if (!assetManager || sampleCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reapFinishedProcess();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FootstepManager::stopCurrentProcess() {
|
||||||
|
if (playerPid > 0) {
|
||||||
|
kill(-playerPid, SIGTERM);
|
||||||
|
kill(playerPid, SIGTERM);
|
||||||
|
int status = 0;
|
||||||
|
waitpid(playerPid, &status, 0);
|
||||||
|
playerPid = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FootstepManager::reapFinishedProcess() {
|
||||||
|
if (playerPid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
pid_t result = waitpid(playerPid, &status, WNOHANG);
|
||||||
|
if (result == playerPid) {
|
||||||
|
playerPid = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep one active step at a time to avoid ffplay process buildup.
|
||||||
|
if (playerPid > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uniform_int_distribution<size_t> clipDist(0, list.size() - 1);
|
||||||
|
const Sample& sample = list[clipDist(rng)];
|
||||||
|
|
||||||
|
std::ofstream out(tempFilePath, std::ios::binary);
|
||||||
|
if (!out) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.write(reinterpret_cast<const char*>(sample.data.data()), static_cast<std::streamsize>(sample.data.size()));
|
||||||
|
out.close();
|
||||||
|
|
||||||
|
// Subtle variation for less repetitive cadence.
|
||||||
|
std::uniform_real_distribution<float> pitchDist(0.97f, 1.05f);
|
||||||
|
std::uniform_real_distribution<float> volumeDist(0.92f, 1.00f);
|
||||||
|
float pitch = pitchDist(rng);
|
||||||
|
float volume = volumeDist(rng) * (sprinting ? 1.0f : 0.88f);
|
||||||
|
if (volume > 1.0f) volume = 1.0f;
|
||||||
|
if (volume < 0.1f) volume = 0.1f;
|
||||||
|
|
||||||
|
std::string filter = "asetrate=44100*" + std::to_string(pitch) +
|
||||||
|
",aresample=44100,volume=" + std::to_string(volume);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setpgid(0, 0);
|
||||||
|
FILE* outFile = freopen("/dev/null", "w", stdout);
|
||||||
|
FILE* errFile = freopen("/dev/null", "w", stderr);
|
||||||
|
(void)outFile;
|
||||||
|
(void)errFile;
|
||||||
|
execlp("ffplay", "ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet",
|
||||||
|
"-af", filter.c_str(), tempFilePath.c_str(), nullptr);
|
||||||
|
_exit(1);
|
||||||
|
} else if (pid > 0) {
|
||||||
|
playerPid = pid;
|
||||||
|
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
|
||||||
|
|
@ -12,6 +12,27 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::optional<float> selectReachableFloor(const std::optional<float>& terrainH,
|
||||||
|
const std::optional<float>& wmoH,
|
||||||
|
float refZ,
|
||||||
|
float maxStepUp) {
|
||||||
|
std::optional<float> best;
|
||||||
|
auto consider = [&](const std::optional<float>& h) {
|
||||||
|
if (!h) return;
|
||||||
|
if (*h > refZ + maxStepUp) return; // Ignore roofs/floors too far above us.
|
||||||
|
if (!best || *h > *best) {
|
||||||
|
best = *h; // Choose highest reachable floor.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
consider(terrainH);
|
||||||
|
consider(wmoH);
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
CameraController::CameraController(Camera* cam) : camera(cam) {
|
CameraController::CameraController(Camera* cam) : camera(cam) {
|
||||||
yaw = defaultYaw;
|
yaw = defaultYaw;
|
||||||
pitch = defaultPitch;
|
pitch = defaultPitch;
|
||||||
|
|
@ -29,12 +50,47 @@ void CameraController::update(float deltaTime) {
|
||||||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||||
|
|
||||||
// Determine current key states
|
// Determine current key states
|
||||||
bool nowForward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
|
bool keyW = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
|
||||||
bool nowBackward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
|
bool keyS = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
|
||||||
bool nowStrafeLeft = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
|
bool keyA = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
|
||||||
bool nowStrafeRight = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
|
bool keyD = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
|
||||||
|
bool keyQ = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_Q);
|
||||||
|
bool keyE = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_E);
|
||||||
|
bool shiftDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
|
||||||
|
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
||||||
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
||||||
|
|
||||||
|
bool mouseAutorun = !uiWantsKeyboard && !sitting && leftMouseDown && rightMouseDown;
|
||||||
|
bool nowForward = keyW || mouseAutorun;
|
||||||
|
bool nowBackward = keyS;
|
||||||
|
bool nowStrafeLeft = false;
|
||||||
|
bool nowStrafeRight = false;
|
||||||
|
bool nowTurnLeft = false;
|
||||||
|
bool nowTurnRight = false;
|
||||||
|
|
||||||
|
// WoW-like third-person keyboard behavior:
|
||||||
|
// - RMB held: A/D strafe
|
||||||
|
// - RMB released: A/D turn character+camera, Q/E strafe
|
||||||
|
if (thirdPerson && !rightMouseDown) {
|
||||||
|
nowTurnLeft = keyA;
|
||||||
|
nowTurnRight = keyD;
|
||||||
|
nowStrafeLeft = keyQ;
|
||||||
|
nowStrafeRight = keyE;
|
||||||
|
} else {
|
||||||
|
nowStrafeLeft = keyA || keyQ;
|
||||||
|
nowStrafeRight = keyD || keyE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard turning updates camera yaw (character follows yaw in renderer)
|
||||||
|
if (nowTurnLeft && !nowTurnRight) {
|
||||||
|
yaw += WOW_TURN_SPEED * deltaTime;
|
||||||
|
} else if (nowTurnRight && !nowTurnLeft) {
|
||||||
|
yaw -= WOW_TURN_SPEED * deltaTime;
|
||||||
|
}
|
||||||
|
if (nowTurnLeft || nowTurnRight) {
|
||||||
|
camera->setRotation(yaw, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
// Select physics constants based on mode
|
// Select physics constants based on mode
|
||||||
float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY;
|
float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY;
|
||||||
float jumpVel = useWoWSpeed ? WOW_JUMP_VELOCITY : JUMP_VELOCITY;
|
float jumpVel = useWoWSpeed ? WOW_JUMP_VELOCITY : JUMP_VELOCITY;
|
||||||
|
|
@ -42,27 +98,34 @@ void CameraController::update(float deltaTime) {
|
||||||
// Calculate movement speed based on direction and modifiers
|
// Calculate movement speed based on direction and modifiers
|
||||||
float speed;
|
float speed;
|
||||||
if (useWoWSpeed) {
|
if (useWoWSpeed) {
|
||||||
// Movement speeds (Shift = sprint, Ctrl = walk)
|
// Movement speeds (WoW-like: Ctrl walk, default run, backpedal slower)
|
||||||
if (nowBackward && !nowForward) {
|
if (nowBackward && !nowForward) {
|
||||||
speed = WOW_BACK_SPEED;
|
speed = WOW_BACK_SPEED;
|
||||||
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
|
} else if (ctrlDown) {
|
||||||
speed = WOW_SPRINT_SPEED; // Shift = sprint (faster)
|
speed = WOW_WALK_SPEED;
|
||||||
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
|
|
||||||
speed = WOW_WALK_SPEED; // Ctrl = walk (slower)
|
|
||||||
} else {
|
} else {
|
||||||
speed = WOW_RUN_SPEED; // Normal run
|
speed = WOW_RUN_SPEED;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Exploration mode (original behavior)
|
// Exploration mode (original behavior)
|
||||||
speed = movementSpeed;
|
speed = movementSpeed;
|
||||||
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
|
if (shiftDown) {
|
||||||
speed *= sprintMultiplier;
|
speed *= sprintMultiplier;
|
||||||
}
|
}
|
||||||
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
|
if (ctrlDown) {
|
||||||
speed *= slowMultiplier;
|
speed *= slowMultiplier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasMoveInput = nowForward || nowBackward || nowStrafeLeft || nowStrafeRight;
|
||||||
|
if (useWoWSpeed) {
|
||||||
|
// "Sprinting" flag drives run animation/stronger footstep set.
|
||||||
|
// In WoW mode this means running pace (not walk/backpedal), not Shift.
|
||||||
|
runPace = hasMoveInput && !ctrlDown && !nowBackward;
|
||||||
|
} else {
|
||||||
|
runPace = hasMoveInput && shiftDown;
|
||||||
|
}
|
||||||
|
|
||||||
// Get camera axes — project forward onto XY plane for walking
|
// Get camera axes — project forward onto XY plane for walking
|
||||||
glm::vec3 forward3D = camera->getForward();
|
glm::vec3 forward3D = camera->getForward();
|
||||||
glm::vec3 forward = glm::normalize(glm::vec3(forward3D.x, forward3D.y, 0.0f));
|
glm::vec3 forward = glm::normalize(glm::vec3(forward3D.x, forward3D.y, 0.0f));
|
||||||
|
|
@ -189,17 +252,15 @@ void CameraController::update(float deltaTime) {
|
||||||
|
|
||||||
// Helper to get ground height at a position
|
// Helper to get ground height at a position
|
||||||
auto getGroundAt = [&](float x, float y) -> std::optional<float> {
|
auto getGroundAt = [&](float x, float y) -> std::optional<float> {
|
||||||
std::optional<float> h;
|
std::optional<float> terrainH;
|
||||||
|
std::optional<float> wmoH;
|
||||||
if (terrainManager) {
|
if (terrainManager) {
|
||||||
h = terrainManager->getHeightAt(x, y);
|
terrainH = terrainManager->getHeightAt(x, y);
|
||||||
}
|
}
|
||||||
if (wmoRenderer) {
|
if (wmoRenderer) {
|
||||||
auto wh = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
||||||
if (wh && (!h || *wh > *h)) {
|
|
||||||
h = wh;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return h;
|
return selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get ground height at target position
|
// Get ground height at target position
|
||||||
|
|
@ -274,35 +335,17 @@ void CameraController::update(float deltaTime) {
|
||||||
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight);
|
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<float> groundH;
|
std::optional<float> groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||||
if (terrainH && wmoH) {
|
|
||||||
groundH = std::max(*terrainH, *wmoH);
|
|
||||||
} else if (terrainH) {
|
|
||||||
groundH = terrainH;
|
|
||||||
} else if (wmoH) {
|
|
||||||
groundH = wmoH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groundH) {
|
if (groundH) {
|
||||||
float groundDiff = *groundH - lastGroundZ;
|
float groundDiff = *groundH - lastGroundZ;
|
||||||
float currentFeetZ = targetPos.z;
|
if (std::abs(groundDiff) < 2.0f) {
|
||||||
|
// Small height difference - smooth it
|
||||||
// Only consider floors that are:
|
lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f);
|
||||||
// 1. Below us (we can fall onto them)
|
} else {
|
||||||
// 2. Slightly above us (we can step up onto them, max 1 unit)
|
// Large height difference - snap (for falling onto ledges)
|
||||||
// Don't teleport to roofs/floors that are way above us
|
lastGroundZ = *groundH;
|
||||||
bool floorIsReachable = (*groundH <= currentFeetZ + 1.0f);
|
|
||||||
|
|
||||||
if (floorIsReachable) {
|
|
||||||
if (std::abs(groundDiff) < 2.0f) {
|
|
||||||
// Small height difference - smooth it
|
|
||||||
lastGroundZ += groundDiff * std::min(1.0f, deltaTime * 15.0f);
|
|
||||||
} else {
|
|
||||||
// Large height difference - snap (for falling onto ledges)
|
|
||||||
lastGroundZ = *groundH;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If floor is way above us (roof), ignore it and keep lastGroundZ
|
|
||||||
|
|
||||||
if (targetPos.z <= lastGroundZ + 0.1f) {
|
if (targetPos.z <= lastGroundZ + 0.1f) {
|
||||||
targetPos.z = lastGroundZ;
|
targetPos.z = lastGroundZ;
|
||||||
|
|
@ -334,26 +377,21 @@ void CameraController::update(float deltaTime) {
|
||||||
float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime);
|
float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime);
|
||||||
currentDistance += (userTargetDistance - currentDistance) * zoomLerp;
|
currentDistance += (userTargetDistance - currentDistance) * zoomLerp;
|
||||||
|
|
||||||
// Desired camera position (before collision)
|
|
||||||
glm::vec3 desiredCam = pivot + camDir * currentDistance;
|
|
||||||
|
|
||||||
// ===== Camera collision (sphere sweep approximation) =====
|
// ===== Camera collision (sphere sweep approximation) =====
|
||||||
// Find max safe distance using raycast + sphere radius
|
// Find max safe distance using raycast + sphere radius
|
||||||
collisionDistance = currentDistance;
|
collisionDistance = currentDistance;
|
||||||
|
|
||||||
// Helper to get floor height
|
// Helper to get floor height
|
||||||
auto getFloorAt = [&](float x, float y, float z) -> std::optional<float> {
|
auto getFloorAt = [&](float x, float y, float z) -> std::optional<float> {
|
||||||
std::optional<float> h;
|
std::optional<float> terrainH;
|
||||||
|
std::optional<float> wmoH;
|
||||||
if (terrainManager) {
|
if (terrainManager) {
|
||||||
h = terrainManager->getHeightAt(x, y);
|
terrainH = terrainManager->getHeightAt(x, y);
|
||||||
}
|
}
|
||||||
if (wmoRenderer) {
|
if (wmoRenderer) {
|
||||||
auto wh = wmoRenderer->getFloorHeight(x, y, z + 5.0f);
|
wmoH = wmoRenderer->getFloorHeight(x, y, z + 5.0f);
|
||||||
if (wh && (!h || *wh > *h)) {
|
|
||||||
h = wh;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return h;
|
return selectReachableFloor(terrainH, wmoH, z, 0.5f);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Raycast against WMO bounding boxes
|
// Raycast against WMO bounding boxes
|
||||||
|
|
@ -409,7 +447,7 @@ void CameraController::update(float deltaTime) {
|
||||||
// After smoothing, ensure camera is above the floor at its final position
|
// After smoothing, ensure camera is above the floor at its final position
|
||||||
// This prevents camera clipping through ground in Stormwind and similar areas
|
// This prevents camera clipping through ground in Stormwind and similar areas
|
||||||
constexpr float MIN_FLOOR_CLEARANCE = 0.20f; // Keep camera at least 20cm above floor
|
constexpr float MIN_FLOOR_CLEARANCE = 0.20f; // Keep camera at least 20cm above floor
|
||||||
auto finalFloorH = getFloorAt(smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z + 5.0f);
|
auto finalFloorH = getFloorAt(smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z);
|
||||||
if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) {
|
if (finalFloorH && smoothedCamPos.z < *finalFloorH + MIN_FLOOR_CLEARANCE) {
|
||||||
smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE;
|
smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE;
|
||||||
}
|
}
|
||||||
|
|
@ -506,14 +544,7 @@ void CameraController::update(float deltaTime) {
|
||||||
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
|
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<float> groundH;
|
std::optional<float> groundH = selectReachableFloor(terrainH, wmoH, newPos.z - eyeHeight, 1.0f);
|
||||||
if (terrainH && wmoH) {
|
|
||||||
groundH = std::max(*terrainH, *wmoH);
|
|
||||||
} else if (terrainH) {
|
|
||||||
groundH = terrainH;
|
|
||||||
} else if (wmoH) {
|
|
||||||
groundH = wmoH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groundH) {
|
if (groundH) {
|
||||||
lastGroundZ = *groundH;
|
lastGroundZ = *groundH;
|
||||||
|
|
@ -565,6 +596,19 @@ void CameraController::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Turning
|
||||||
|
if (nowTurnLeft && !wasTurningLeft) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_TURN_LEFT));
|
||||||
|
}
|
||||||
|
if (nowTurnRight && !wasTurningRight) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_TURN_RIGHT));
|
||||||
|
}
|
||||||
|
if ((!nowTurnLeft && wasTurningLeft) || (!nowTurnRight && wasTurningRight)) {
|
||||||
|
if (!nowTurnLeft && !nowTurnRight) {
|
||||||
|
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_TURN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Jump
|
// Jump
|
||||||
if (nowJump && !wasJumping && grounded) {
|
if (nowJump && !wasJumping && grounded) {
|
||||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_JUMP));
|
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_JUMP));
|
||||||
|
|
@ -591,13 +635,17 @@ void CameraController::update(float deltaTime) {
|
||||||
wasMovingBackward = nowBackward;
|
wasMovingBackward = nowBackward;
|
||||||
wasStrafingLeft = nowStrafeLeft;
|
wasStrafingLeft = nowStrafeLeft;
|
||||||
wasStrafingRight = nowStrafeRight;
|
wasStrafingRight = nowStrafeRight;
|
||||||
|
wasTurningLeft = nowTurnLeft;
|
||||||
|
wasTurningRight = nowTurnRight;
|
||||||
wasJumping = nowJump;
|
wasJumping = nowJump;
|
||||||
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
||||||
|
|
||||||
// Reset camera (R key)
|
// Reset camera/character (R key, edge-triggered)
|
||||||
if (!uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R)) {
|
bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R);
|
||||||
|
if (rDown && !rKeyWasDown) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
rKeyWasDown = rDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||||
|
|
@ -648,7 +696,20 @@ void CameraController::reset() {
|
||||||
yaw = defaultYaw;
|
yaw = defaultYaw;
|
||||||
pitch = defaultPitch;
|
pitch = defaultPitch;
|
||||||
verticalVelocity = 0.0f;
|
verticalVelocity = 0.0f;
|
||||||
grounded = false;
|
grounded = true;
|
||||||
|
swimming = false;
|
||||||
|
sitting = false;
|
||||||
|
|
||||||
|
// Clear edge-state so movement packets can re-start cleanly after respawn.
|
||||||
|
wasMovingForward = false;
|
||||||
|
wasMovingBackward = false;
|
||||||
|
wasStrafingLeft = false;
|
||||||
|
wasStrafingRight = false;
|
||||||
|
wasTurningLeft = false;
|
||||||
|
wasTurningRight = false;
|
||||||
|
wasJumping = false;
|
||||||
|
wasFalling = false;
|
||||||
|
wasSwimming = false;
|
||||||
|
|
||||||
glm::vec3 spawnPos = defaultPosition;
|
glm::vec3 spawnPos = defaultPosition;
|
||||||
|
|
||||||
|
|
@ -665,11 +726,32 @@ void CameraController::reset() {
|
||||||
}
|
}
|
||||||
if (h) {
|
if (h) {
|
||||||
lastGroundZ = *h;
|
lastGroundZ = *h;
|
||||||
spawnPos.z = *h + eyeHeight;
|
spawnPos.z = *h;
|
||||||
}
|
}
|
||||||
|
|
||||||
camera->setPosition(spawnPos);
|
|
||||||
camera->setRotation(yaw, pitch);
|
camera->setRotation(yaw, pitch);
|
||||||
|
glm::vec3 forward3D = camera->getForward();
|
||||||
|
|
||||||
|
if (thirdPerson && followTarget) {
|
||||||
|
// In follow mode, respawn the character (feet position), then place camera behind it.
|
||||||
|
*followTarget = spawnPos;
|
||||||
|
|
||||||
|
currentDistance = userTargetDistance;
|
||||||
|
collisionDistance = currentDistance;
|
||||||
|
|
||||||
|
glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT);
|
||||||
|
glm::vec3 camDir = -forward3D;
|
||||||
|
glm::vec3 camPos = pivot + camDir * currentDistance;
|
||||||
|
smoothedCamPos = camPos;
|
||||||
|
camera->setPosition(camPos);
|
||||||
|
} else {
|
||||||
|
// Free-fly mode keeps camera eye-height above ground.
|
||||||
|
if (h) {
|
||||||
|
spawnPos.z += eyeHeight;
|
||||||
|
}
|
||||||
|
smoothedCamPos = spawnPos;
|
||||||
|
camera->setPosition(spawnPos);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("Camera reset to default position");
|
LOG_INFO("Camera reset to default position");
|
||||||
}
|
}
|
||||||
|
|
@ -701,22 +783,24 @@ bool CameraController::isMoving() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& input = core::Input::getInstance();
|
auto& input = core::Input::getInstance();
|
||||||
|
bool keyW = input.isKeyPressed(SDL_SCANCODE_W);
|
||||||
|
bool keyS = input.isKeyPressed(SDL_SCANCODE_S);
|
||||||
|
bool keyA = input.isKeyPressed(SDL_SCANCODE_A);
|
||||||
|
bool keyD = input.isKeyPressed(SDL_SCANCODE_D);
|
||||||
|
bool keyQ = input.isKeyPressed(SDL_SCANCODE_Q);
|
||||||
|
bool keyE = input.isKeyPressed(SDL_SCANCODE_E);
|
||||||
|
|
||||||
return input.isKeyPressed(SDL_SCANCODE_W) ||
|
// In third-person without RMB, A/D are turn keys (not movement).
|
||||||
input.isKeyPressed(SDL_SCANCODE_S) ||
|
if (thirdPerson && !rightMouseDown) {
|
||||||
input.isKeyPressed(SDL_SCANCODE_A) ||
|
return keyW || keyS || keyQ || keyE;
|
||||||
input.isKeyPressed(SDL_SCANCODE_D);
|
}
|
||||||
|
|
||||||
|
bool mouseAutorun = leftMouseDown && rightMouseDown;
|
||||||
|
return keyW || keyS || keyA || keyD || keyQ || keyE || mouseAutorun;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CameraController::isSprinting() const {
|
bool CameraController::isSprinting() const {
|
||||||
if (!enabled || !camera) {
|
return enabled && camera && runPace;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (ImGui::GetIO().WantCaptureKeyboard) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto& input = core::Input::getInstance();
|
|
||||||
return isMoving() && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -1141,6 +1141,30 @@ void CharacterRenderer::removeInstance(uint32_t instanceId) {
|
||||||
instances.erase(instanceId);
|
instances.erase(instanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CharacterRenderer::getAnimationState(uint32_t instanceId, uint32_t& animationId,
|
||||||
|
float& animationTimeMs, float& animationDurationMs) const {
|
||||||
|
auto it = instances.find(instanceId);
|
||||||
|
if (it == instances.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterInstance& instance = it->second;
|
||||||
|
auto modelIt = models.find(instance.modelId);
|
||||||
|
if (modelIt == models.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& sequences = modelIt->second.data.sequences;
|
||||||
|
if (instance.currentSequenceIndex < 0 || instance.currentSequenceIndex >= static_cast<int>(sequences.size())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = instance.currentAnimationId;
|
||||||
|
animationTimeMs = instance.animationTime;
|
||||||
|
animationDurationMs = static_cast<float>(sequences[instance.currentSequenceIndex].duration);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
||||||
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
||||||
const std::string& texturePath) {
|
const std::string& texturePath) {
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,14 @@
|
||||||
#include "game/world.hpp"
|
#include "game/world.hpp"
|
||||||
#include "game/zone_manager.hpp"
|
#include "game/zone_manager.hpp"
|
||||||
#include "audio/music_manager.hpp"
|
#include "audio/music_manager.hpp"
|
||||||
|
#include "audio/footstep_manager.hpp"
|
||||||
#include <GL/glew.h>
|
#include <GL/glew.h>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
#include <glm/gtx/euler_angles.hpp>
|
#include <glm/gtx/euler_angles.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
|
|
@ -185,6 +188,7 @@ bool Renderer::initialize(core::Window* win) {
|
||||||
|
|
||||||
// Create music manager (initialized later with asset manager)
|
// Create music manager (initialized later with asset manager)
|
||||||
musicManager = std::make_unique<audio::MusicManager>();
|
musicManager = std::make_unique<audio::MusicManager>();
|
||||||
|
footstepManager = std::make_unique<audio::FootstepManager>();
|
||||||
|
|
||||||
LOG_INFO("Renderer initialized");
|
LOG_INFO("Renderer initialized");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -257,6 +261,10 @@ void Renderer::shutdown() {
|
||||||
musicManager->shutdown();
|
musicManager->shutdown();
|
||||||
musicManager.reset();
|
musicManager.reset();
|
||||||
}
|
}
|
||||||
|
if (footstepManager) {
|
||||||
|
footstepManager->shutdown();
|
||||||
|
footstepManager.reset();
|
||||||
|
}
|
||||||
|
|
||||||
zoneManager.reset();
|
zoneManager.reset();
|
||||||
|
|
||||||
|
|
@ -492,6 +500,87 @@ bool Renderer::isMoving() const {
|
||||||
return cameraController && cameraController->isMoving();
|
return cameraController && cameraController->isMoving();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Renderer::isFootstepAnimationState() const {
|
||||||
|
return charAnimState == CharAnimState::WALK || charAnimState == CharAnimState::RUN;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Renderer::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) {
|
||||||
|
if (animationDurationMs <= 1.0f) {
|
||||||
|
footstepNormInitialized = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float norm = std::fmod(animationTimeMs, animationDurationMs) / animationDurationMs;
|
||||||
|
if (norm < 0.0f) norm += 1.0f;
|
||||||
|
|
||||||
|
if (animationId != footstepLastAnimationId) {
|
||||||
|
footstepLastAnimationId = animationId;
|
||||||
|
footstepLastNormTime = norm;
|
||||||
|
footstepNormInitialized = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!footstepNormInitialized) {
|
||||||
|
footstepNormInitialized = true;
|
||||||
|
footstepLastNormTime = norm;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto crossed = [&](float eventNorm) {
|
||||||
|
if (footstepLastNormTime <= norm) {
|
||||||
|
return footstepLastNormTime < eventNorm && eventNorm <= norm;
|
||||||
|
}
|
||||||
|
return footstepLastNormTime < eventNorm || eventNorm <= norm;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool trigger = crossed(0.22f) || crossed(0.72f);
|
||||||
|
footstepLastNormTime = norm;
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio::FootstepSurface Renderer::resolveFootstepSurface() const {
|
||||||
|
if (!cameraController || !cameraController->isThirdPerson()) {
|
||||||
|
return audio::FootstepSurface::STONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const glm::vec3& p = characterPosition;
|
||||||
|
|
||||||
|
if (cameraController->isSwimming()) {
|
||||||
|
return audio::FootstepSurface::WATER;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waterRenderer) {
|
||||||
|
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
|
||||||
|
if (waterH && p.z < (*waterH + 0.25f)) {
|
||||||
|
return audio::FootstepSurface::WATER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wmoRenderer) {
|
||||||
|
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
|
||||||
|
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
|
||||||
|
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
|
||||||
|
return audio::FootstepSurface::STONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terrainManager) {
|
||||||
|
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
|
||||||
|
if (texture) {
|
||||||
|
std::string t = *texture;
|
||||||
|
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) return audio::FootstepSurface::SNOW;
|
||||||
|
if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) return audio::FootstepSurface::GRASS;
|
||||||
|
if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) return audio::FootstepSurface::DIRT;
|
||||||
|
if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) return audio::FootstepSurface::WOOD;
|
||||||
|
if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) return audio::FootstepSurface::METAL;
|
||||||
|
if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) return audio::FootstepSurface::STONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio::FootstepSurface::STONE;
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::update(float deltaTime) {
|
void Renderer::update(float deltaTime) {
|
||||||
if (cameraController) {
|
if (cameraController) {
|
||||||
cameraController->update(deltaTime);
|
cameraController->update(deltaTime);
|
||||||
|
|
@ -569,6 +658,25 @@ void Renderer::update(float deltaTime) {
|
||||||
characterRenderer->update(deltaTime);
|
characterRenderer->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Footsteps: animation-event driven + surface query at event time.
|
||||||
|
if (footstepManager) {
|
||||||
|
footstepManager->update(deltaTime);
|
||||||
|
if (characterRenderer && characterInstanceId > 0 &&
|
||||||
|
cameraController && cameraController->isThirdPerson() &&
|
||||||
|
isFootstepAnimationState() && cameraController->isGrounded() &&
|
||||||
|
!cameraController->isSwimming()) {
|
||||||
|
uint32_t animId = 0;
|
||||||
|
float animTimeMs = 0.0f;
|
||||||
|
float animDurationMs = 0.0f;
|
||||||
|
if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) &&
|
||||||
|
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
|
||||||
|
footstepManager->playFootstep(resolveFootstepSurface(), cameraController->isSprinting());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
footstepNormInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update M2 doodad animations
|
// Update M2 doodad animations
|
||||||
if (m2Renderer) {
|
if (m2Renderer) {
|
||||||
m2Renderer->update(deltaTime);
|
m2Renderer->update(deltaTime);
|
||||||
|
|
@ -820,6 +928,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
||||||
// Initialize music manager with asset manager
|
// Initialize music manager with asset manager
|
||||||
if (musicManager && assetManager && !cachedAssetManager) {
|
if (musicManager && assetManager && !cachedAssetManager) {
|
||||||
musicManager->initialize(assetManager);
|
musicManager->initialize(assetManager);
|
||||||
|
if (footstepManager) {
|
||||||
|
footstepManager->initialize(assetManager);
|
||||||
|
}
|
||||||
cachedAssetManager = assetManager;
|
cachedAssetManager = assetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -883,6 +994,11 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
|
||||||
musicManager->initialize(cachedAssetManager);
|
musicManager->initialize(cachedAssetManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (footstepManager && cachedAssetManager) {
|
||||||
|
if (!footstepManager->isInitialized()) {
|
||||||
|
footstepManager->initialize(cachedAssetManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wire WMO, M2, and water renderer to camera controller
|
// Wire WMO, M2, and water renderer to camera controller
|
||||||
if (cameraController && wmoRenderer) {
|
if (cameraController && wmoRenderer) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,66 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool decodeLayerAlpha(const pipeline::MapChunk& chunk, size_t layerIdx, std::vector<uint8_t>& outAlpha) {
|
||||||
|
if (layerIdx >= chunk.layers.size()) return false;
|
||||||
|
const auto& layer = chunk.layers[layerIdx];
|
||||||
|
if (!layer.useAlpha() || layer.offsetMCAL >= chunk.alphaMap.size()) return false;
|
||||||
|
|
||||||
|
size_t offset = layer.offsetMCAL;
|
||||||
|
size_t layerSize = chunk.alphaMap.size() - offset;
|
||||||
|
for (size_t j = layerIdx + 1; j < chunk.layers.size(); j++) {
|
||||||
|
if (chunk.layers[j].useAlpha()) {
|
||||||
|
layerSize = chunk.layers[j].offsetMCAL - offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outAlpha.assign(4096, 255);
|
||||||
|
|
||||||
|
if (layer.compressedAlpha()) {
|
||||||
|
size_t readPos = offset;
|
||||||
|
size_t writePos = 0;
|
||||||
|
while (writePos < 4096 && readPos < chunk.alphaMap.size()) {
|
||||||
|
uint8_t cmd = chunk.alphaMap[readPos++];
|
||||||
|
bool fill = (cmd & 0x80) != 0;
|
||||||
|
int count = (cmd & 0x7F) + 1;
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
if (readPos >= chunk.alphaMap.size()) break;
|
||||||
|
uint8_t val = chunk.alphaMap[readPos++];
|
||||||
|
for (int i = 0; i < count && writePos < 4096; i++) {
|
||||||
|
outAlpha[writePos++] = val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < count && writePos < 4096 && readPos < chunk.alphaMap.size(); i++) {
|
||||||
|
outAlpha[writePos++] = chunk.alphaMap[readPos++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layerSize >= 4096) {
|
||||||
|
std::copy(chunk.alphaMap.begin() + offset, chunk.alphaMap.begin() + offset + 4096, outAlpha.begin());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layerSize >= 2048) {
|
||||||
|
for (size_t i = 0; i < 2048; i++) {
|
||||||
|
uint8_t v = chunk.alphaMap[offset + i];
|
||||||
|
outAlpha[i * 2] = (v & 0x0F) * 17;
|
||||||
|
outAlpha[i * 2 + 1] = (v >> 4) * 17;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TerrainManager::TerrainManager() {
|
TerrainManager::TerrainManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -807,6 +867,69 @@ std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float glY) const {
|
||||||
|
const float unitSize = CHUNK_SIZE / 8.0f;
|
||||||
|
std::vector<uint8_t> alphaScratch;
|
||||||
|
|
||||||
|
for (const auto& [coord, tile] : loadedTiles) {
|
||||||
|
(void)coord;
|
||||||
|
if (!tile || !tile->loaded) continue;
|
||||||
|
|
||||||
|
for (int cy = 0; cy < 16; cy++) {
|
||||||
|
for (int cx = 0; cx < 16; cx++) {
|
||||||
|
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
||||||
|
if (!chunk.hasHeightMap() || chunk.layers.empty()) continue;
|
||||||
|
|
||||||
|
float chunkMaxX = chunk.position[0];
|
||||||
|
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
||||||
|
float chunkMaxY = chunk.position[1];
|
||||||
|
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
||||||
|
if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float fracY = (chunk.position[0] - glX) / unitSize;
|
||||||
|
float fracX = (chunk.position[1] - glY) / unitSize;
|
||||||
|
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
||||||
|
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
||||||
|
|
||||||
|
int alphaX = glm::clamp(static_cast<int>((fracX / 8.0f) * 63.0f), 0, 63);
|
||||||
|
int alphaY = glm::clamp(static_cast<int>((fracY / 8.0f) * 63.0f), 0, 63);
|
||||||
|
int alphaIndex = alphaY * 64 + alphaX;
|
||||||
|
|
||||||
|
std::vector<int> weights(chunk.layers.size(), 0);
|
||||||
|
int accum = 0;
|
||||||
|
for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) {
|
||||||
|
int alpha = 0;
|
||||||
|
if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast<int>(alphaScratch.size())) {
|
||||||
|
alpha = alphaScratch[alphaIndex];
|
||||||
|
}
|
||||||
|
weights[layerIdx] = alpha;
|
||||||
|
accum += alpha;
|
||||||
|
}
|
||||||
|
weights[0] = glm::clamp(255 - accum, 0, 255);
|
||||||
|
|
||||||
|
size_t bestLayer = 0;
|
||||||
|
int bestWeight = weights[0];
|
||||||
|
for (size_t i = 1; i < weights.size(); i++) {
|
||||||
|
if (weights[i] > bestWeight) {
|
||||||
|
bestWeight = weights[i];
|
||||||
|
bestLayer = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t texId = chunk.layers[bestLayer].textureId;
|
||||||
|
if (texId < tile->terrain.textures.size()) {
|
||||||
|
return tile->terrain.textures[texId];
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
void TerrainManager::streamTiles() {
|
void TerrainManager::streamTiles() {
|
||||||
// Enqueue tiles in radius around current tile for async loading
|
// Enqueue tiles in radius around current tile for async loading
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue