mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 08:30:13 +00:00
Restructure inventory UI, add vendor selling, camera intro on all spawns, and quest log
Split inventory into bags-only (B key) and character screen (C key). Vendor window auto-opens bags with sell prices on hover and right-click to sell. Add camera intro pan on all login/spawn/teleport/hearthstone events and idle orbit after 2 minutes. Add quest log UI, SMSG_MONSTER_MOVE handling, deferred creature spawn queue, and creature fade-in/movement interpolation for online mode.
This commit is contained in:
parent
bb4c2c25f7
commit
71c3d2ea77
21 changed files with 1092 additions and 149 deletions
|
|
@ -60,6 +60,7 @@ void CameraController::startIntroPan(float durationSec, float orbitDegrees) {
|
|||
if (!camera) return;
|
||||
introActive = true;
|
||||
introTimer = 0.0f;
|
||||
idleTimer_ = 0.0f;
|
||||
introDuration = std::max(0.5f, durationSec);
|
||||
introStartYaw = facingYaw + orbitDegrees;
|
||||
introEndYaw = facingYaw;
|
||||
|
|
@ -96,9 +97,24 @@ void CameraController::update(float deltaTime) {
|
|||
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
||||
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
|
||||
|
||||
// Idle camera: any input resets the timer; timeout triggers a slow orbit pan
|
||||
bool anyInput = leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump;
|
||||
if (anyInput) {
|
||||
idleTimer_ = 0.0f;
|
||||
} else if (!introActive) {
|
||||
idleTimer_ += deltaTime;
|
||||
if (idleTimer_ >= IDLE_TIMEOUT) {
|
||||
idleTimer_ = 0.0f;
|
||||
startIntroPan(6.0f, 360.0f); // Slow full orbit
|
||||
idleOrbit_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (introActive) {
|
||||
if (leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump) {
|
||||
if (anyInput) {
|
||||
introActive = false;
|
||||
idleOrbit_ = false;
|
||||
idleTimer_ = 0.0f;
|
||||
} else {
|
||||
introTimer += deltaTime;
|
||||
float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f;
|
||||
|
|
@ -109,7 +125,13 @@ void CameraController::update(float deltaTime) {
|
|||
camera->setRotation(yaw, pitch);
|
||||
facingYaw = yaw;
|
||||
if (t >= 1.0f) {
|
||||
introActive = false;
|
||||
if (idleOrbit_) {
|
||||
// Loop: restart the slow orbit continuously
|
||||
startIntroPan(6.0f, 360.0f);
|
||||
idleOrbit_ = true;
|
||||
} else {
|
||||
introActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Suppress player movement/input during intro.
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ bool CharacterRenderer::initialize() {
|
|||
uniform mat4 uLightSpaceMatrix;
|
||||
uniform int uShadowEnabled;
|
||||
uniform float uShadowStrength;
|
||||
uniform float uOpacity;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
|
|
@ -154,8 +155,8 @@ bool CharacterRenderer::initialize() {
|
|||
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
|
||||
result = mix(uFogColor, result, fogFactor);
|
||||
|
||||
// Force alpha=1 for opaque character rendering (baked NPC textures may have alpha=0)
|
||||
FragColor = vec4(result, 1.0);
|
||||
// Apply opacity (for fade-in effects)
|
||||
FragColor = vec4(result, uOpacity);
|
||||
}
|
||||
)";
|
||||
|
||||
|
|
@ -906,6 +907,35 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId,
|
|||
}
|
||||
|
||||
void CharacterRenderer::update(float deltaTime) {
|
||||
// Update fade-in opacity
|
||||
for (auto& [id, inst] : instances) {
|
||||
if (inst.fadeInDuration > 0.0f && inst.opacity < 1.0f) {
|
||||
inst.fadeInTime += deltaTime;
|
||||
inst.opacity = std::min(1.0f, inst.fadeInTime / inst.fadeInDuration);
|
||||
if (inst.opacity >= 1.0f) {
|
||||
inst.fadeInDuration = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interpolate creature movement
|
||||
for (auto& [id, inst] : instances) {
|
||||
if (inst.isMoving) {
|
||||
inst.moveElapsed += deltaTime;
|
||||
float t = inst.moveElapsed / inst.moveDuration;
|
||||
if (t >= 1.0f) {
|
||||
inst.position = inst.moveEnd;
|
||||
inst.isMoving = false;
|
||||
// Return to idle when movement completes
|
||||
if (inst.currentAnimationId == 4) {
|
||||
playAnimation(id, 0, true);
|
||||
}
|
||||
} else {
|
||||
inst.position = glm::mix(inst.moveStart, inst.moveEnd, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& pair : instances) {
|
||||
updateAnimation(pair.second, deltaTime);
|
||||
}
|
||||
|
|
@ -1123,6 +1153,8 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE); // M2 models have mixed winding; render both sides
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
characterShader->use();
|
||||
characterShader->setUniform("uView", view);
|
||||
|
|
@ -1155,11 +1187,15 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
|
||||
const auto& gpuModel = models[instance.modelId];
|
||||
|
||||
// Skip fully transparent instances
|
||||
if (instance.opacity <= 0.0f) continue;
|
||||
|
||||
// Set model matrix (use override for weapon instances)
|
||||
glm::mat4 modelMat = instance.hasOverrideModelMatrix
|
||||
? instance.overrideModelMatrix
|
||||
: getModelMatrix(instance);
|
||||
characterShader->setUniform("uModel", modelMat);
|
||||
characterShader->setUniform("uOpacity", instance.opacity);
|
||||
|
||||
// Set bone matrices (upload all at once for performance)
|
||||
int numBones = std::min(static_cast<int>(instance.boneMatrices.size()), MAX_BONES);
|
||||
|
|
@ -1273,6 +1309,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
}
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_CULL_FACE); // Restore culling for other renderers
|
||||
}
|
||||
|
||||
|
|
@ -1379,6 +1416,55 @@ void CharacterRenderer::setInstanceRotation(uint32_t instanceId, const glm::vec3
|
|||
}
|
||||
}
|
||||
|
||||
void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& destination, float durationSeconds) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it == instances.end()) return;
|
||||
|
||||
auto& inst = it->second;
|
||||
if (durationSeconds <= 0.0f) {
|
||||
// Instant move (stop)
|
||||
inst.position = destination;
|
||||
inst.isMoving = false;
|
||||
// Return to idle animation if currently walking
|
||||
if (inst.currentAnimationId == 4) {
|
||||
playAnimation(instanceId, 0, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
inst.moveStart = inst.position;
|
||||
inst.moveEnd = destination;
|
||||
inst.moveDuration = durationSeconds;
|
||||
inst.moveElapsed = 0.0f;
|
||||
inst.isMoving = true;
|
||||
|
||||
// Face toward destination (yaw around Z axis since Z is up)
|
||||
glm::vec3 dir = destination - inst.position;
|
||||
if (glm::length(glm::vec2(dir.x, dir.y)) > 0.001f) {
|
||||
float angle = std::atan2(dir.y, dir.x);
|
||||
inst.rotation.z = angle;
|
||||
}
|
||||
|
||||
// Play walk animation (ID 4) while moving
|
||||
if (inst.currentAnimationId == 0) {
|
||||
playAnimation(instanceId, 4, true);
|
||||
}
|
||||
}
|
||||
|
||||
const pipeline::M2Model* CharacterRenderer::getModelData(uint32_t modelId) const {
|
||||
auto it = models.find(modelId);
|
||||
if (it == models.end()) return nullptr;
|
||||
return &it->second.data;
|
||||
}
|
||||
|
||||
void CharacterRenderer::startFadeIn(uint32_t instanceId, float durationSeconds) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it == instances.end()) return;
|
||||
it->second.opacity = 0.0f;
|
||||
it->second.fadeInTime = 0.0f;
|
||||
it->second.fadeInDuration = durationSeconds;
|
||||
}
|
||||
|
||||
void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it != instances.end()) {
|
||||
|
|
|
|||
|
|
@ -391,7 +391,8 @@ uint32_t Renderer::resolveMeleeAnimId() {
|
|||
return 0.0f;
|
||||
};
|
||||
|
||||
const uint32_t attackCandidates[] = {16, 17, 18, 19, 20, 21};
|
||||
// Prefer weapon attacks (1H=17, 2H=18) over unarmed (16); 19-21 are other variants
|
||||
const uint32_t attackCandidates[] = {17, 18, 16, 19, 20, 21};
|
||||
for (uint32_t id : attackCandidates) {
|
||||
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
||||
meleeAnimId = id;
|
||||
|
|
@ -1032,6 +1033,113 @@ void Renderer::update(float deltaTime) {
|
|||
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Selection Circle
|
||||
// ============================================================
|
||||
|
||||
void Renderer::initSelectionCircle() {
|
||||
if (selCircleVAO) return;
|
||||
|
||||
// Minimal shader: position + uniform MVP + color
|
||||
const char* vsSrc = R"(
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 aPos;
|
||||
uniform mat4 uMVP;
|
||||
void main() {
|
||||
gl_Position = uMVP * vec4(aPos, 1.0);
|
||||
}
|
||||
)";
|
||||
const char* fsSrc = R"(
|
||||
#version 330 core
|
||||
uniform vec3 uColor;
|
||||
out vec4 FragColor;
|
||||
void main() {
|
||||
FragColor = vec4(uColor, 0.6);
|
||||
}
|
||||
)";
|
||||
|
||||
auto compile = [](GLenum type, const char* src) -> GLuint {
|
||||
GLuint s = glCreateShader(type);
|
||||
glShaderSource(s, 1, &src, nullptr);
|
||||
glCompileShader(s);
|
||||
return s;
|
||||
};
|
||||
|
||||
GLuint vs = compile(GL_VERTEX_SHADER, vsSrc);
|
||||
GLuint fs = compile(GL_FRAGMENT_SHADER, fsSrc);
|
||||
selCircleShader = glCreateProgram();
|
||||
glAttachShader(selCircleShader, vs);
|
||||
glAttachShader(selCircleShader, fs);
|
||||
glLinkProgram(selCircleShader);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
|
||||
// Build ring vertices (two concentric circles forming a strip)
|
||||
constexpr int SEGMENTS = 48;
|
||||
constexpr float INNER = 0.85f;
|
||||
constexpr float OUTER = 1.0f;
|
||||
std::vector<float> verts;
|
||||
for (int i = 0; i <= SEGMENTS; i++) {
|
||||
float angle = 2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(SEGMENTS);
|
||||
float c = std::cos(angle), s = std::sin(angle);
|
||||
// Outer vertex
|
||||
verts.push_back(c * OUTER);
|
||||
verts.push_back(s * OUTER);
|
||||
verts.push_back(0.0f);
|
||||
// Inner vertex
|
||||
verts.push_back(c * INNER);
|
||||
verts.push_back(s * INNER);
|
||||
verts.push_back(0.0f);
|
||||
}
|
||||
selCircleVertCount = static_cast<int>((SEGMENTS + 1) * 2);
|
||||
|
||||
glGenVertexArrays(1, &selCircleVAO);
|
||||
glGenBuffers(1, &selCircleVBO);
|
||||
glBindVertexArray(selCircleVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, selCircleVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) {
|
||||
selCirclePos = pos;
|
||||
selCircleRadius = radius;
|
||||
selCircleColor = color;
|
||||
selCircleVisible = true;
|
||||
}
|
||||
|
||||
void Renderer::clearSelectionCircle() {
|
||||
selCircleVisible = false;
|
||||
}
|
||||
|
||||
void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection) {
|
||||
if (!selCircleVisible) return;
|
||||
initSelectionCircle();
|
||||
|
||||
glm::mat4 model = glm::translate(glm::mat4(1.0f), selCirclePos);
|
||||
model = glm::scale(model, glm::vec3(selCircleRadius));
|
||||
|
||||
glm::mat4 mvp = projection * view * model;
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
glUseProgram(selCircleShader);
|
||||
glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]);
|
||||
glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]);
|
||||
|
||||
glBindVertexArray(selCircleVAO);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, selCircleVertCount);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
void Renderer::renderWorld(game::World* world) {
|
||||
auto renderStart = std::chrono::steady_clock::now();
|
||||
lastTerrainRenderMs = 0.0;
|
||||
|
|
@ -1157,6 +1265,9 @@ void Renderer::renderWorld(game::World* world) {
|
|||
characterRenderer->render(*camera, view, projection);
|
||||
}
|
||||
|
||||
// Render selection circle under targeted creature
|
||||
renderSelectionCircle(view, projection);
|
||||
|
||||
// Render WMO buildings (after characters, before UI)
|
||||
if (wmoRenderer && camera) {
|
||||
auto wmoStart = std::chrono::steady_clock::now();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue