#include "rendering/camera_controller.hpp" #include "rendering/terrain_manager.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" #include "rendering/water_renderer.hpp" #include "game/opcodes.hpp" #include "core/logger.hpp" #include #include namespace wowee { namespace rendering { CameraController::CameraController(Camera* cam) : camera(cam) { yaw = defaultYaw; pitch = defaultPitch; reset(); } void CameraController::update(float deltaTime) { if (!enabled || !camera) { return; } auto& input = core::Input::getInstance(); // Don't process keyboard input when UI (e.g. chat box) has focus 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 nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE); // Select physics constants based on mode float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY; float jumpVel = useWoWSpeed ? WOW_JUMP_VELOCITY : JUMP_VELOCITY; // Calculate movement speed based on direction and modifiers float speed; if (useWoWSpeed) { // WoW-correct speeds if (nowBackward && !nowForward) { speed = WOW_BACK_SPEED; } else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) { speed = WOW_WALK_SPEED; // Shift = walk in WoW mode } else { speed = WOW_RUN_SPEED; } } else { // Exploration mode (original behavior) speed = movementSpeed; if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) { speed *= sprintMultiplier; } if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) { speed *= slowMultiplier; } } // 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)); glm::vec3 right = camera->getRight(); right.z = 0.0f; if (glm::length(right) > 0.001f) { right = glm::normalize(right); } // Toggle sit with X key (edge-triggered) — only when UI doesn't want keyboard bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X); if (xDown && !xKeyWasDown) { sitting = !sitting; } xKeyWasDown = xDown; // Calculate horizontal movement vector glm::vec3 movement(0.0f); if (nowForward) movement += forward; if (nowBackward) movement -= forward; if (nowStrafeLeft) movement -= right; if (nowStrafeRight) movement += right; // Stand up if any movement key is pressed while sitting if (!uiWantsKeyboard && sitting && (input.isKeyPressed(SDL_SCANCODE_W) || input.isKeyPressed(SDL_SCANCODE_S) || input.isKeyPressed(SDL_SCANCODE_A) || input.isKeyPressed(SDL_SCANCODE_D) || input.isKeyPressed(SDL_SCANCODE_SPACE))) { sitting = false; } // Third-person orbit camera mode if (thirdPerson && followTarget) { // Move the follow target (character position) instead of the camera glm::vec3 targetPos = *followTarget; // Check for water at current position std::optional waterH; if (waterRenderer) { waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y); } bool inWater = waterH && targetPos.z < *waterH; if (inWater) { swimming = true; // Reduce horizontal speed while swimming float swimSpeed = speed * SWIM_SPEED_FACTOR; if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); targetPos += movement * swimSpeed * deltaTime; } // Spacebar = swim up (continuous, not a jump) if (nowJump) { verticalVelocity = SWIM_BUOYANCY; } else { // Gentle sink when not pressing space verticalVelocity += SWIM_GRAVITY * deltaTime; if (verticalVelocity < SWIM_SINK_SPEED) { verticalVelocity = SWIM_SINK_SPEED; } } targetPos.z += verticalVelocity * deltaTime; // Don't rise above water surface if (waterH && targetPos.z > *waterH - WATER_SURFACE_OFFSET) { targetPos.z = *waterH - WATER_SURFACE_OFFSET; if (verticalVelocity > 0.0f) verticalVelocity = 0.0f; } grounded = false; } else { swimming = false; if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); targetPos += movement * speed * deltaTime; } // Jump if (nowJump && grounded) { verticalVelocity = jumpVel; grounded = false; } // Apply gravity verticalVelocity += gravity * deltaTime; targetPos.z += verticalVelocity * deltaTime; } // Wall collision for character (WMO buildings) if (wmoRenderer) { glm::vec3 feetPos = targetPos; glm::vec3 oldFeetPos = *followTarget; glm::vec3 adjusted; if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) { targetPos.x = adjusted.x; targetPos.y = adjusted.y; targetPos.z = adjusted.z; } } // Collision with M2 doodads (fences, boxes, etc.) if (m2Renderer) { glm::vec3 adjusted; if (m2Renderer->checkCollision(*followTarget, targetPos, adjusted)) { targetPos.x = adjusted.x; targetPos.y = adjusted.y; } } // Ground the character to terrain or WMO floor { std::optional terrainH; std::optional wmoH; if (terrainManager) { terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y); } if (wmoRenderer) { wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight); } std::optional groundH; if (terrainH && wmoH) { groundH = std::max(*terrainH, *wmoH); } else if (terrainH) { groundH = terrainH; } else if (wmoH) { groundH = wmoH; } if (groundH) { lastGroundZ = *groundH; if (targetPos.z <= *groundH) { targetPos.z = *groundH; verticalVelocity = 0.0f; grounded = true; swimming = false; // Touching ground = wading, not swimming } else if (!swimming) { grounded = false; } } else if (!swimming) { // No terrain found — hold at last known ground targetPos.z = lastGroundZ; verticalVelocity = 0.0f; grounded = true; } } // Update follow target position *followTarget = targetPos; // Compute camera position orbiting behind the character glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight); glm::vec3 camPos = lookAtPoint - forward3D * orbitDistance; // Clamp camera above terrain/WMO floor { float minCamZ = camPos.z; if (terrainManager) { auto h = terrainManager->getHeightAt(camPos.x, camPos.y); if (h) minCamZ = *h + 1.0f; // 1 unit above ground } if (wmoRenderer) { auto wh = wmoRenderer->getFloorHeight(camPos.x, camPos.y, camPos.z + eyeHeight); if (wh && (*wh + 1.0f) > minCamZ) minCamZ = *wh + 1.0f; } if (camPos.z < minCamZ) { camPos.z = minCamZ; } } camera->setPosition(camPos); } else { // Free-fly camera mode (original behavior) glm::vec3 newPos = camera->getPosition(); float feetZ = newPos.z - eyeHeight; // Check for water at feet position std::optional waterH; if (waterRenderer) { waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y); } bool inWater = waterH && feetZ < *waterH; if (inWater) { swimming = true; float swimSpeed = speed * SWIM_SPEED_FACTOR; if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); newPos += movement * swimSpeed * deltaTime; } if (nowJump) { verticalVelocity = SWIM_BUOYANCY; } else { verticalVelocity += SWIM_GRAVITY * deltaTime; if (verticalVelocity < SWIM_SINK_SPEED) { verticalVelocity = SWIM_SINK_SPEED; } } newPos.z += verticalVelocity * deltaTime; // Don't rise above water surface (feet at water level) if (waterH && (newPos.z - eyeHeight) > *waterH - WATER_SURFACE_OFFSET) { newPos.z = *waterH - WATER_SURFACE_OFFSET + eyeHeight; if (verticalVelocity > 0.0f) verticalVelocity = 0.0f; } grounded = false; } else { swimming = false; if (glm::length(movement) > 0.001f) { movement = glm::normalize(movement); newPos += movement * speed * deltaTime; } // Jump if (nowJump && grounded) { verticalVelocity = jumpVel; grounded = false; } // Apply gravity verticalVelocity += gravity * deltaTime; newPos.z += verticalVelocity * deltaTime; } // Wall collision — push out of WMO walls before grounding if (wmoRenderer) { glm::vec3 feetPos = newPos - glm::vec3(0, 0, eyeHeight); glm::vec3 oldFeetPos = camera->getPosition() - glm::vec3(0, 0, eyeHeight); glm::vec3 adjusted; if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) { newPos.x = adjusted.x; newPos.y = adjusted.y; newPos.z = adjusted.z + eyeHeight; } } // Ground to terrain or WMO floor { std::optional terrainH; std::optional wmoH; if (terrainManager) { terrainH = terrainManager->getHeightAt(newPos.x, newPos.y); } if (wmoRenderer) { wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z); } std::optional groundH; if (terrainH && wmoH) { groundH = std::max(*terrainH, *wmoH); } else if (terrainH) { groundH = terrainH; } else if (wmoH) { groundH = wmoH; } if (groundH) { lastGroundZ = *groundH; float groundZ = *groundH + eyeHeight; if (newPos.z <= groundZ) { newPos.z = groundZ; verticalVelocity = 0.0f; grounded = true; swimming = false; // Touching ground = wading } else if (!swimming) { grounded = false; } } else if (!swimming) { float groundZ = lastGroundZ + eyeHeight; newPos.z = groundZ; verticalVelocity = 0.0f; grounded = true; } } camera->setPosition(newPos); } // --- Edge-detection: send movement opcodes on state transitions --- if (movementCallback) { // Forward/backward if (nowForward && !wasMovingForward) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_START_FORWARD)); } if (nowBackward && !wasMovingBackward) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_START_BACKWARD)); } if ((!nowForward && wasMovingForward) || (!nowBackward && wasMovingBackward)) { if (!nowForward && !nowBackward) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_STOP)); } } // Strafing if (nowStrafeLeft && !wasStrafingLeft) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_START_STRAFE_LEFT)); } if (nowStrafeRight && !wasStrafingRight) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_START_STRAFE_RIGHT)); } if ((!nowStrafeLeft && wasStrafingLeft) || (!nowStrafeRight && wasStrafingRight)) { if (!nowStrafeLeft && !nowStrafeRight) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_STOP_STRAFE)); } } // Jump if (nowJump && !wasJumping && grounded) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_JUMP)); } // Fall landing if (wasFalling && grounded) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_FALL_LAND)); } } // Swimming state transitions if (movementCallback) { if (swimming && !wasSwimming) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_START_SWIM)); } else if (!swimming && wasSwimming) { movementCallback(static_cast(game::Opcode::CMSG_MOVE_STOP_SWIM)); } } // Update previous-frame state wasSwimming = swimming; wasMovingForward = nowForward; wasMovingBackward = nowBackward; wasStrafingLeft = nowStrafeLeft; wasStrafingRight = nowStrafeRight; wasJumping = nowJump; wasFalling = !grounded && verticalVelocity <= 0.0f; // Reset camera (R key) if (!uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R)) { reset(); } } void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) { if (!enabled || !camera) { return; } if (!mouseButtonDown) { return; } // Directly update stored yaw/pitch (no lossy forward-vector derivation) yaw -= event.xrel * mouseSensitivity; pitch += event.yrel * mouseSensitivity; pitch = glm::clamp(pitch, -89.0f, 89.0f); camera->setRotation(yaw, pitch); } void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) { if (!enabled) { return; } if (event.button == SDL_BUTTON_LEFT) { leftMouseDown = (event.state == SDL_PRESSED); } if (event.button == SDL_BUTTON_RIGHT) { rightMouseDown = (event.state == SDL_PRESSED); } bool anyDown = leftMouseDown || rightMouseDown; if (anyDown && !mouseButtonDown) { SDL_SetRelativeMouseMode(SDL_TRUE); } else if (!anyDown && mouseButtonDown) { SDL_SetRelativeMouseMode(SDL_FALSE); } mouseButtonDown = anyDown; } void CameraController::reset() { if (!camera) { return; } yaw = defaultYaw; pitch = defaultPitch; verticalVelocity = 0.0f; grounded = false; glm::vec3 spawnPos = defaultPosition; // Snap spawn to terrain or WMO surface std::optional h; if (terrainManager) { h = terrainManager->getHeightAt(spawnPos.x, spawnPos.y); } if (wmoRenderer) { auto wh = wmoRenderer->getFloorHeight(spawnPos.x, spawnPos.y, spawnPos.z); if (wh && (!h || *wh > *h)) { h = wh; } } if (h) { lastGroundZ = *h; spawnPos.z = *h + eyeHeight; } camera->setPosition(spawnPos); camera->setRotation(yaw, pitch); LOG_INFO("Camera reset to default position"); } void CameraController::processMouseWheel(float delta) { orbitDistance -= delta * zoomSpeed; orbitDistance = glm::clamp(orbitDistance, minOrbitDistance, maxOrbitDistance); } void CameraController::setFollowTarget(glm::vec3* target) { followTarget = target; if (target) { thirdPerson = true; LOG_INFO("Third-person camera enabled"); } else { thirdPerson = false; LOG_INFO("Free-fly camera enabled"); } } bool CameraController::isMoving() const { if (!enabled || !camera) { return false; } if (ImGui::GetIO().WantCaptureKeyboard) { return false; } auto& input = core::Input::getInstance(); return input.isKeyPressed(SDL_SCANCODE_W) || input.isKeyPressed(SDL_SCANCODE_S) || input.isKeyPressed(SDL_SCANCODE_A) || input.isKeyPressed(SDL_SCANCODE_D); } 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)); } } // namespace rendering } // namespace wowee