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
|
||||
src/audio/music_manager.cpp
|
||||
src/audio/footstep_manager.cpp
|
||||
|
||||
# Pipeline (asset loaders)
|
||||
src/pipeline/mpq_manager.cpp
|
||||
|
|
@ -174,6 +175,7 @@ set(WOWEE_HEADERS
|
|||
include/game/inventory.hpp
|
||||
|
||||
include/audio/music_manager.hpp
|
||||
include/audio/footstep_manager.hpp
|
||||
|
||||
include/pipeline/mpq_manager.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 sitting = false;
|
||||
bool xKeyWasDown = false;
|
||||
bool rKeyWasDown = false;
|
||||
bool runPace = false;
|
||||
|
||||
// Movement state tracking (for sending opcodes on state change)
|
||||
bool wasMovingForward = false;
|
||||
bool wasMovingBackward = false;
|
||||
bool wasStrafingLeft = false;
|
||||
bool wasStrafingRight = false;
|
||||
bool wasTurningLeft = false;
|
||||
bool wasTurningRight = false;
|
||||
bool wasJumping = false;
|
||||
bool wasFalling = false;
|
||||
|
||||
|
|
@ -136,10 +140,11 @@ private:
|
|||
|
||||
// Movement speeds
|
||||
bool useWoWSpeed = false;
|
||||
static constexpr float WOW_RUN_SPEED = 14.0f; // Normal run (WASD)
|
||||
static constexpr float WOW_SPRINT_SPEED = 28.0f; // Sprint (hold Shift)
|
||||
static constexpr float WOW_WALK_SPEED = 5.0f; // Walk (hold Ctrl)
|
||||
static constexpr float WOW_BACK_SPEED = 9.0f; // Backpedal
|
||||
static constexpr float WOW_RUN_SPEED = 7.0f; // Normal run (WotLK)
|
||||
static constexpr float WOW_SPRINT_SPEED = 10.5f; // Optional fast mode (not default WoW behavior)
|
||||
static constexpr float WOW_WALK_SPEED = 2.5f; // Walk
|
||||
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_JUMP_VELOCITY = 7.96f;
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ public:
|
|||
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
||||
void setInstanceVisible(uint32_t instanceId, bool visible);
|
||||
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. */
|
||||
bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
namespace wowee {
|
||||
namespace core { class Window; }
|
||||
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 rendering {
|
||||
|
|
@ -142,6 +142,7 @@ private:
|
|||
std::unique_ptr<M2Renderer> m2Renderer;
|
||||
std::unique_ptr<Minimap> minimap;
|
||||
std::unique_ptr<audio::MusicManager> musicManager;
|
||||
std::unique_ptr<audio::FootstepManager> footstepManager;
|
||||
std::unique_ptr<game::ZoneManager> zoneManager;
|
||||
|
||||
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 };
|
||||
CharAnimState charAnimState = CharAnimState::IDLE;
|
||||
void updateCharacterAnimation();
|
||||
bool isFootstepAnimationState() const;
|
||||
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
||||
audio::FootstepSurface resolveFootstepSurface() const;
|
||||
|
||||
// Emote state
|
||||
bool emoteActive = false;
|
||||
|
|
@ -166,6 +170,11 @@ private:
|
|||
// Target facing
|
||||
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 terrainLoaded = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -172,6 +172,12 @@ public:
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
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 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) {
|
||||
yaw = defaultYaw;
|
||||
pitch = defaultPitch;
|
||||
|
|
@ -29,12 +50,47 @@ void CameraController::update(float deltaTime) {
|
|||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||
|
||||
// Determine current key states
|
||||
bool nowForward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
|
||||
bool nowBackward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
|
||||
bool nowStrafeLeft = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
|
||||
bool nowStrafeRight = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
|
||||
bool keyW = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
|
||||
bool keyS = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
|
||||
bool keyA = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
|
||||
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 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
|
||||
float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY;
|
||||
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
|
||||
float speed;
|
||||
if (useWoWSpeed) {
|
||||
// Movement speeds (Shift = sprint, Ctrl = walk)
|
||||
// Movement speeds (WoW-like: Ctrl walk, default run, backpedal slower)
|
||||
if (nowBackward && !nowForward) {
|
||||
speed = WOW_BACK_SPEED;
|
||||
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
|
||||
speed = WOW_SPRINT_SPEED; // Shift = sprint (faster)
|
||||
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
|
||||
speed = WOW_WALK_SPEED; // Ctrl = walk (slower)
|
||||
} else if (ctrlDown) {
|
||||
speed = WOW_WALK_SPEED;
|
||||
} else {
|
||||
speed = WOW_RUN_SPEED; // Normal run
|
||||
speed = WOW_RUN_SPEED;
|
||||
}
|
||||
} else {
|
||||
// Exploration mode (original behavior)
|
||||
speed = movementSpeed;
|
||||
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
|
||||
if (shiftDown) {
|
||||
speed *= sprintMultiplier;
|
||||
}
|
||||
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
|
||||
if (ctrlDown) {
|
||||
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
|
||||
glm::vec3 forward3D = camera->getForward();
|
||||
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
|
||||
auto getGroundAt = [&](float x, float y) -> std::optional<float> {
|
||||
std::optional<float> h;
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
if (terrainManager) {
|
||||
h = terrainManager->getHeightAt(x, y);
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
auto wh = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
||||
if (wh && (!h || *wh > *h)) {
|
||||
h = wh;
|
||||
}
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, targetPos.z + 5.0f);
|
||||
}
|
||||
return h;
|
||||
return selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
std::optional<float> groundH;
|
||||
if (terrainH && wmoH) {
|
||||
groundH = std::max(*terrainH, *wmoH);
|
||||
} else if (terrainH) {
|
||||
groundH = terrainH;
|
||||
} else if (wmoH) {
|
||||
groundH = wmoH;
|
||||
}
|
||||
std::optional<float> groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, 1.0f);
|
||||
|
||||
if (groundH) {
|
||||
float groundDiff = *groundH - lastGroundZ;
|
||||
float currentFeetZ = targetPos.z;
|
||||
|
||||
// Only consider floors that are:
|
||||
// 1. Below us (we can fall onto them)
|
||||
// 2. Slightly above us (we can step up onto them, max 1 unit)
|
||||
// Don't teleport to roofs/floors that are way above us
|
||||
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 (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) {
|
||||
targetPos.z = lastGroundZ;
|
||||
|
|
@ -334,26 +377,21 @@ void CameraController::update(float deltaTime) {
|
|||
float zoomLerp = 1.0f - std::exp(-ZOOM_SMOOTH_SPEED * deltaTime);
|
||||
currentDistance += (userTargetDistance - currentDistance) * zoomLerp;
|
||||
|
||||
// Desired camera position (before collision)
|
||||
glm::vec3 desiredCam = pivot + camDir * currentDistance;
|
||||
|
||||
// ===== Camera collision (sphere sweep approximation) =====
|
||||
// Find max safe distance using raycast + sphere radius
|
||||
collisionDistance = currentDistance;
|
||||
|
||||
// Helper to get floor height
|
||||
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) {
|
||||
h = terrainManager->getHeightAt(x, y);
|
||||
terrainH = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
auto wh = wmoRenderer->getFloorHeight(x, y, z + 5.0f);
|
||||
if (wh && (!h || *wh > *h)) {
|
||||
h = wh;
|
||||
}
|
||||
wmoH = wmoRenderer->getFloorHeight(x, y, z + 5.0f);
|
||||
}
|
||||
return h;
|
||||
return selectReachableFloor(terrainH, wmoH, z, 0.5f);
|
||||
};
|
||||
|
||||
// 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
|
||||
// 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
|
||||
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) {
|
||||
smoothedCamPos.z = *finalFloorH + MIN_FLOOR_CLEARANCE;
|
||||
}
|
||||
|
|
@ -506,14 +544,7 @@ void CameraController::update(float deltaTime) {
|
|||
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
|
||||
}
|
||||
|
||||
std::optional<float> groundH;
|
||||
if (terrainH && wmoH) {
|
||||
groundH = std::max(*terrainH, *wmoH);
|
||||
} else if (terrainH) {
|
||||
groundH = terrainH;
|
||||
} else if (wmoH) {
|
||||
groundH = wmoH;
|
||||
}
|
||||
std::optional<float> groundH = selectReachableFloor(terrainH, wmoH, newPos.z - eyeHeight, 1.0f);
|
||||
|
||||
if (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
|
||||
if (nowJump && !wasJumping && grounded) {
|
||||
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_JUMP));
|
||||
|
|
@ -591,13 +635,17 @@ void CameraController::update(float deltaTime) {
|
|||
wasMovingBackward = nowBackward;
|
||||
wasStrafingLeft = nowStrafeLeft;
|
||||
wasStrafingRight = nowStrafeRight;
|
||||
wasTurningLeft = nowTurnLeft;
|
||||
wasTurningRight = nowTurnRight;
|
||||
wasJumping = nowJump;
|
||||
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
||||
|
||||
// Reset camera (R key)
|
||||
if (!uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R)) {
|
||||
// Reset camera/character (R key, edge-triggered)
|
||||
bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R);
|
||||
if (rDown && !rKeyWasDown) {
|
||||
reset();
|
||||
}
|
||||
rKeyWasDown = rDown;
|
||||
}
|
||||
|
||||
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||
|
|
@ -648,7 +696,20 @@ void CameraController::reset() {
|
|||
yaw = defaultYaw;
|
||||
pitch = defaultPitch;
|
||||
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;
|
||||
|
||||
|
|
@ -665,11 +726,32 @@ void CameraController::reset() {
|
|||
}
|
||||
if (h) {
|
||||
lastGroundZ = *h;
|
||||
spawnPos.z = *h + eyeHeight;
|
||||
spawnPos.z = *h;
|
||||
}
|
||||
|
||||
camera->setPosition(spawnPos);
|
||||
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");
|
||||
}
|
||||
|
|
@ -701,22 +783,24 @@ bool CameraController::isMoving() const {
|
|||
}
|
||||
|
||||
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) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_S) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_A) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_D);
|
||||
// In third-person without RMB, A/D are turn keys (not movement).
|
||||
if (thirdPerson && !rightMouseDown) {
|
||||
return keyW || keyS || keyQ || keyE;
|
||||
}
|
||||
|
||||
bool mouseAutorun = leftMouseDown && rightMouseDown;
|
||||
return keyW || keyS || keyA || keyD || keyQ || keyE || mouseAutorun;
|
||||
}
|
||||
|
||||
bool CameraController::isSprinting() const {
|
||||
if (!enabled || !camera) {
|
||||
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));
|
||||
return enabled && camera && runPace;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -1141,6 +1141,30 @@ void CharacterRenderer::removeInstance(uint32_t 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,
|
||||
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
||||
const std::string& texturePath) {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,14 @@
|
|||
#include "game/world.hpp"
|
||||
#include "game/zone_manager.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include "audio/footstep_manager.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/euler_angles.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
|
|
@ -185,6 +188,7 @@ bool Renderer::initialize(core::Window* win) {
|
|||
|
||||
// Create music manager (initialized later with asset manager)
|
||||
musicManager = std::make_unique<audio::MusicManager>();
|
||||
footstepManager = std::make_unique<audio::FootstepManager>();
|
||||
|
||||
LOG_INFO("Renderer initialized");
|
||||
return true;
|
||||
|
|
@ -257,6 +261,10 @@ void Renderer::shutdown() {
|
|||
musicManager->shutdown();
|
||||
musicManager.reset();
|
||||
}
|
||||
if (footstepManager) {
|
||||
footstepManager->shutdown();
|
||||
footstepManager.reset();
|
||||
}
|
||||
|
||||
zoneManager.reset();
|
||||
|
||||
|
|
@ -492,6 +500,87 @@ bool Renderer::isMoving() const {
|
|||
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) {
|
||||
if (cameraController) {
|
||||
cameraController->update(deltaTime);
|
||||
|
|
@ -569,6 +658,25 @@ void Renderer::update(float 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
|
||||
if (m2Renderer) {
|
||||
m2Renderer->update(deltaTime);
|
||||
|
|
@ -820,6 +928,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
|
|||
// Initialize music manager with asset manager
|
||||
if (musicManager && assetManager && !cachedAssetManager) {
|
||||
musicManager->initialize(assetManager);
|
||||
if (footstepManager) {
|
||||
footstepManager->initialize(assetManager);
|
||||
}
|
||||
cachedAssetManager = assetManager;
|
||||
}
|
||||
|
||||
|
|
@ -883,6 +994,11 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
|
|||
musicManager->initialize(cachedAssetManager);
|
||||
}
|
||||
}
|
||||
if (footstepManager && cachedAssetManager) {
|
||||
if (!footstepManager->isInitialized()) {
|
||||
footstepManager->initialize(cachedAssetManager);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire WMO, M2, and water renderer to camera controller
|
||||
if (cameraController && wmoRenderer) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,66 @@
|
|||
namespace wowee {
|
||||
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() {
|
||||
}
|
||||
|
||||
|
|
@ -807,6 +867,69 @@ std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
|||
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() {
|
||||
// Enqueue tiles in radius around current tile for async loading
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue