mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Add spellbook, fix WMO floor clipping, and polish UI/visuals
- Add spellbook screen (P key) with Spell.dbc name lookup and action bar assignment - Default Attack and Hearthstone spells available in single player - Fix WMO floor clipping (gryphon roost) by tightening ceiling rejection threshold - Darken ocean water, increase wave motion and opacity - Add M2 model distance fade-in to prevent pop-in - Reposition chat window, add slash/enter key focus - Remove debug key commands (keep only F1 perf HUD, N minimap) - Performance: return chat history by const ref, use deque for O(1) pop_front
This commit is contained in:
parent
c49bb58e47
commit
4bc5064515
17 changed files with 486 additions and 431 deletions
|
|
@ -144,6 +144,7 @@ set(WOWEE_SOURCES
|
||||||
src/ui/character_screen.cpp
|
src/ui/character_screen.cpp
|
||||||
src/ui/game_screen.cpp
|
src/ui/game_screen.cpp
|
||||||
src/ui/inventory_screen.cpp
|
src/ui/inventory_screen.cpp
|
||||||
|
src/ui/spellbook_screen.cpp
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
|
@ -226,6 +227,7 @@ set(WOWEE_HEADERS
|
||||||
include/ui/character_screen.hpp
|
include/ui/character_screen.hpp
|
||||||
include/ui/game_screen.hpp
|
include/ui/game_screen.hpp
|
||||||
include/ui/inventory_screen.hpp
|
include/ui/inventory_screen.hpp
|
||||||
|
include/ui/spellbook_screen.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create executable
|
# Create executable
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
@ -151,7 +152,7 @@ public:
|
||||||
* @param maxMessages Maximum number of messages to return (0 = all)
|
* @param maxMessages Maximum number of messages to return (0 = all)
|
||||||
* @return Vector of chat messages
|
* @return Vector of chat messages
|
||||||
*/
|
*/
|
||||||
std::vector<MessageChatData> getChatHistory(size_t maxMessages = 50) const;
|
const std::deque<MessageChatData>& getChatHistory() const { return chatHistory; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a locally-generated chat message (e.g., emote feedback)
|
* Add a locally-generated chat message (e.g., emote feedback)
|
||||||
|
|
@ -401,7 +402,7 @@ private:
|
||||||
EntityManager entityManager; // Manages all entities in view
|
EntityManager entityManager; // Manages all entities in view
|
||||||
|
|
||||||
// Chat
|
// Chat
|
||||||
std::vector<MessageChatData> chatHistory; // Recent chat messages
|
std::deque<MessageChatData> chatHistory; // Recent chat messages
|
||||||
size_t maxChatHistory = 100; // Maximum chat messages to keep
|
size_t maxChatHistory = 100; // Maximum chat messages to keep
|
||||||
|
|
||||||
// Targeting
|
// Targeting
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ struct M2ModelGPU {
|
||||||
bool collisionSmallSolidProp = false;
|
bool collisionSmallSolidProp = false;
|
||||||
bool collisionNarrowVerticalProp = false;
|
bool collisionNarrowVerticalProp = false;
|
||||||
bool collisionNoBlock = false;
|
bool collisionNoBlock = false;
|
||||||
|
bool collisionStatue = false;
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ private:
|
||||||
glm::mat4 invModelMatrix; // Cached inverse for collision
|
glm::mat4 invModelMatrix; // Cached inverse for collision
|
||||||
glm::vec3 worldBoundsMin;
|
glm::vec3 worldBoundsMin;
|
||||||
glm::vec3 worldBoundsMax;
|
glm::vec3 worldBoundsMax;
|
||||||
|
std::vector<std::pair<glm::vec3, glm::vec3>> worldGroupBounds;
|
||||||
|
|
||||||
void updateModelMatrix();
|
void updateModelMatrix();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
#include "ui/inventory_screen.hpp"
|
#include "ui/inventory_screen.hpp"
|
||||||
|
#include "ui/spellbook_screen.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|
@ -110,6 +111,7 @@ private:
|
||||||
* Inventory screen
|
* Inventory screen
|
||||||
*/
|
*/
|
||||||
InventoryScreen inventoryScreen;
|
InventoryScreen inventoryScreen;
|
||||||
|
SpellbookScreen spellbookScreen;
|
||||||
};
|
};
|
||||||
|
|
||||||
}} // namespace wowee::ui
|
}} // namespace wowee::ui
|
||||||
|
|
|
||||||
38
include/ui/spellbook_screen.hpp
Normal file
38
include/ui/spellbook_screen.hpp
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
class SpellbookScreen {
|
||||||
|
public:
|
||||||
|
void render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager);
|
||||||
|
bool isOpen() const { return open; }
|
||||||
|
void toggle() { open = !open; }
|
||||||
|
void setOpen(bool o) { open = o; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool open = false;
|
||||||
|
bool pKeyWasDown = false;
|
||||||
|
|
||||||
|
// Spell name cache (loaded from Spell.dbc)
|
||||||
|
bool dbcLoaded = false;
|
||||||
|
bool dbcLoadAttempted = false;
|
||||||
|
std::unordered_map<uint32_t, std::string> spellNames;
|
||||||
|
|
||||||
|
// Action bar assignment
|
||||||
|
int assigningSlot = -1; // Which action bar slot is being assigned (-1 = none)
|
||||||
|
|
||||||
|
void loadSpellDBC(pipeline::AssetManager* assetManager);
|
||||||
|
std::string getSpellName(uint32_t spellId) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -214,282 +214,10 @@ void Application::run() {
|
||||||
LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF");
|
LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// F2: Toggle wireframe
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F2) {
|
|
||||||
static bool wireframe = false;
|
|
||||||
wireframe = !wireframe;
|
|
||||||
if (renderer) {
|
|
||||||
renderer->setWireframeMode(wireframe);
|
|
||||||
LOG_INFO("Wireframe mode: ", wireframe ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F3: Load test terrain (if in main menu/auth state)
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F3) {
|
|
||||||
if (assetManager && assetManager->isInitialized()) {
|
|
||||||
LOG_INFO("Loading test terrain...");
|
|
||||||
// Load a test ADT tile (Elwynn Forest)
|
|
||||||
if (renderer->loadTestTerrain(assetManager.get(),
|
|
||||||
"World\\Maps\\Azeroth\\Azeroth_32_49.adt")) {
|
|
||||||
LOG_INFO("Test terrain loaded! Use WASD/QE to move, hold right mouse to look");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_WARNING("Asset manager not initialized. Set WOW_DATA_PATH environment variable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F4: Toggle frustum culling
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F4) {
|
|
||||||
if (renderer && renderer->getTerrainRenderer()) {
|
|
||||||
static bool culling = true;
|
|
||||||
culling = !culling;
|
|
||||||
renderer->getTerrainRenderer()->setFrustumCulling(culling);
|
|
||||||
LOG_INFO("Frustum culling: ", culling ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F5: Show rendering statistics
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F5) {
|
|
||||||
if (renderer && renderer->getTerrainRenderer()) {
|
|
||||||
auto* terrain = renderer->getTerrainRenderer();
|
|
||||||
LOG_INFO("=== Rendering Statistics ===");
|
|
||||||
LOG_INFO(" Total chunks: ", terrain->getChunkCount());
|
|
||||||
LOG_INFO(" Rendered: ", terrain->getRenderedChunkCount());
|
|
||||||
LOG_INFO(" Culled: ", terrain->getCulledChunkCount());
|
|
||||||
LOG_INFO(" Triangles: ", terrain->getTriangleCount());
|
|
||||||
|
|
||||||
if (terrain->getChunkCount() > 0) {
|
|
||||||
float visiblePercent = (terrain->getRenderedChunkCount() * 100.0f) / terrain->getChunkCount();
|
|
||||||
LOG_INFO(" Visible: ", static_cast<int>(visiblePercent), "%");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show terrain manager stats
|
|
||||||
if (renderer->getTerrainManager()) {
|
|
||||||
auto* manager = renderer->getTerrainManager();
|
|
||||||
LOG_INFO(" Loaded tiles: ", manager->getLoadedTileCount());
|
|
||||||
auto currentTile = manager->getCurrentTile();
|
|
||||||
LOG_INFO(" Current tile: [", currentTile.x, ",", currentTile.y, "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F6: Load multi-tile terrain area
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F6) {
|
|
||||||
if (assetManager && assetManager->isInitialized()) {
|
|
||||||
LOG_INFO("Loading 3x3 terrain area (Elwynn Forest)...");
|
|
||||||
// Load 3x3 grid of tiles (Elwynn Forest area)
|
|
||||||
if (renderer->loadTerrainArea("Azeroth", 32, 49, 1)) {
|
|
||||||
LOG_INFO("Terrain area loaded! Streaming enabled.");
|
|
||||||
LOG_INFO("Move around to see dynamic tile loading/unloading");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_WARNING("Asset manager not initialized. Set WOW_DATA_PATH environment variable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F7: Toggle terrain streaming
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F7) {
|
|
||||||
if (renderer && renderer->getTerrainManager()) {
|
|
||||||
static bool streaming = true;
|
|
||||||
streaming = !streaming;
|
|
||||||
renderer->setTerrainStreaming(streaming);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F8: Toggle water rendering
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F8) {
|
|
||||||
if (renderer && renderer->getWaterRenderer()) {
|
|
||||||
static bool water = true;
|
|
||||||
water = !water;
|
|
||||||
renderer->getWaterRenderer()->setEnabled(water);
|
|
||||||
LOG_INFO("Water rendering: ", water ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F9: Toggle time progression
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F9) {
|
|
||||||
if (renderer && renderer->getSkybox()) {
|
|
||||||
bool progression = !renderer->getSkybox()->isTimeProgressionEnabled();
|
|
||||||
renderer->getSkybox()->setTimeProgression(progression);
|
|
||||||
LOG_INFO("Time progression: ", progression ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Plus/Equals: Advance time
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_EQUALS ||
|
|
||||||
event.key.keysym.scancode == SDL_SCANCODE_KP_PLUS) {
|
|
||||||
if (renderer && renderer->getSkybox()) {
|
|
||||||
float time = renderer->getSkybox()->getTimeOfDay() + 1.0f;
|
|
||||||
renderer->getSkybox()->setTimeOfDay(time);
|
|
||||||
LOG_INFO("Time of day: ", static_cast<int>(time), ":00");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Minus: Rewind time
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_MINUS ||
|
|
||||||
event.key.keysym.scancode == SDL_SCANCODE_KP_MINUS) {
|
|
||||||
if (renderer && renderer->getSkybox()) {
|
|
||||||
float time = renderer->getSkybox()->getTimeOfDay() - 1.0f;
|
|
||||||
renderer->getSkybox()->setTimeOfDay(time);
|
|
||||||
LOG_INFO("Time of day: ", static_cast<int>(time), ":00");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F10: Toggle celestial rendering (sun/moon)
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F10) {
|
|
||||||
if (renderer && renderer->getCelestial()) {
|
|
||||||
bool enabled = !renderer->getCelestial()->isEnabled();
|
|
||||||
renderer->getCelestial()->setEnabled(enabled);
|
|
||||||
LOG_INFO("Celestial rendering: ", enabled ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F11: Toggle star field
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F11) {
|
|
||||||
if (renderer && renderer->getStarField()) {
|
|
||||||
bool enabled = !renderer->getStarField()->isEnabled();
|
|
||||||
renderer->getStarField()->setEnabled(enabled);
|
|
||||||
LOG_INFO("Star field: ", enabled ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F12: Toggle distance fog
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F12) {
|
|
||||||
if (renderer && renderer->getTerrainRenderer()) {
|
|
||||||
bool enabled = !renderer->getTerrainRenderer()->isFogEnabled();
|
|
||||||
renderer->getTerrainRenderer()->setFogEnabled(enabled);
|
|
||||||
LOG_INFO("Distance fog: ", enabled ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// C: Toggle clouds
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_C) {
|
|
||||||
if (renderer && renderer->getClouds()) {
|
|
||||||
bool enabled = !renderer->getClouds()->isEnabled();
|
|
||||||
renderer->getClouds()->setEnabled(enabled);
|
|
||||||
LOG_INFO("Clouds: ", enabled ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [ (Left bracket): Decrease cloud density
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_LEFTBRACKET) {
|
|
||||||
if (renderer && renderer->getClouds()) {
|
|
||||||
float density = renderer->getClouds()->getDensity() - 0.1f;
|
|
||||||
renderer->getClouds()->setDensity(density);
|
|
||||||
LOG_INFO("Cloud density: ", static_cast<int>(density * 100), "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ] (Right bracket): Increase cloud density
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_RIGHTBRACKET) {
|
|
||||||
if (renderer && renderer->getClouds()) {
|
|
||||||
float density = renderer->getClouds()->getDensity() + 0.1f;
|
|
||||||
renderer->getClouds()->setDensity(density);
|
|
||||||
LOG_INFO("Cloud density: ", static_cast<int>(density * 100), "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// L: Toggle lens flare
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_L) {
|
|
||||||
if (renderer && renderer->getLensFlare()) {
|
|
||||||
bool enabled = !renderer->getLensFlare()->isEnabled();
|
|
||||||
renderer->getLensFlare()->setEnabled(enabled);
|
|
||||||
LOG_INFO("Lens flare: ", enabled ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// , (Comma): Decrease lens flare intensity
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_COMMA) {
|
|
||||||
if (renderer && renderer->getLensFlare()) {
|
|
||||||
float intensity = renderer->getLensFlare()->getIntensity() - 0.1f;
|
|
||||||
renderer->getLensFlare()->setIntensity(intensity);
|
|
||||||
LOG_INFO("Lens flare intensity: ", static_cast<int>(intensity * 100), "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// . (Period): Increase lens flare intensity
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_PERIOD) {
|
|
||||||
if (renderer && renderer->getLensFlare()) {
|
|
||||||
float intensity = renderer->getLensFlare()->getIntensity() + 0.1f;
|
|
||||||
renderer->getLensFlare()->setIntensity(intensity);
|
|
||||||
LOG_INFO("Lens flare intensity: ", static_cast<int>(intensity * 100), "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// M: Toggle moon phase cycling
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_M) {
|
|
||||||
if (renderer && renderer->getCelestial()) {
|
|
||||||
bool cycling = !renderer->getCelestial()->isMoonPhaseCycling();
|
|
||||||
renderer->getCelestial()->setMoonPhaseCycling(cycling);
|
|
||||||
LOG_INFO("Moon phase cycling: ", cycling ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ; (Semicolon): Previous moon phase
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_SEMICOLON) {
|
|
||||||
if (renderer && renderer->getCelestial()) {
|
|
||||||
float phase = renderer->getCelestial()->getMoonPhase() - 0.05f;
|
|
||||||
if (phase < 0.0f) phase += 1.0f;
|
|
||||||
renderer->getCelestial()->setMoonPhase(phase);
|
|
||||||
|
|
||||||
// Log phase name
|
|
||||||
const char* phaseName = "Unknown";
|
|
||||||
if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New Moon";
|
|
||||||
else if (phase < 0.1875f) phaseName = "Waxing Crescent";
|
|
||||||
else if (phase < 0.3125f) phaseName = "First Quarter";
|
|
||||||
else if (phase < 0.4375f) phaseName = "Waxing Gibbous";
|
|
||||||
else if (phase < 0.5625f) phaseName = "Full Moon";
|
|
||||||
else if (phase < 0.6875f) phaseName = "Waning Gibbous";
|
|
||||||
else if (phase < 0.8125f) phaseName = "Last Quarter";
|
|
||||||
else phaseName = "Waning Crescent";
|
|
||||||
|
|
||||||
LOG_INFO("Moon phase: ", phaseName, " (", static_cast<int>(phase * 100), "%)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ' (Apostrophe): Next moon phase
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_APOSTROPHE) {
|
|
||||||
if (renderer && renderer->getCelestial()) {
|
|
||||||
float phase = renderer->getCelestial()->getMoonPhase() + 0.05f;
|
|
||||||
if (phase >= 1.0f) phase -= 1.0f;
|
|
||||||
renderer->getCelestial()->setMoonPhase(phase);
|
|
||||||
|
|
||||||
// Log phase name
|
|
||||||
const char* phaseName = "Unknown";
|
|
||||||
if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New Moon";
|
|
||||||
else if (phase < 0.1875f) phaseName = "Waxing Crescent";
|
|
||||||
else if (phase < 0.3125f) phaseName = "First Quarter";
|
|
||||||
else if (phase < 0.4375f) phaseName = "Waxing Gibbous";
|
|
||||||
else if (phase < 0.5625f) phaseName = "Full Moon";
|
|
||||||
else if (phase < 0.6875f) phaseName = "Waning Gibbous";
|
|
||||||
else if (phase < 0.8125f) phaseName = "Last Quarter";
|
|
||||||
else phaseName = "Waning Crescent";
|
|
||||||
|
|
||||||
LOG_INFO("Moon phase: ", phaseName, " (", static_cast<int>(phase * 100), "%)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// X key reserved for sit (handled in camera_controller)
|
|
||||||
// < (Shift+,): Decrease weather intensity
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_COMMA &&
|
|
||||||
(event.key.keysym.mod & KMOD_SHIFT)) {
|
|
||||||
if (renderer && renderer->getWeather()) {
|
|
||||||
float intensity = renderer->getWeather()->getIntensity() - 0.1f;
|
|
||||||
renderer->getWeather()->setIntensity(intensity);
|
|
||||||
LOG_INFO("Weather intensity: ", static_cast<int>(intensity * 100), "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// > (Shift+.): Increase weather intensity
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_PERIOD &&
|
|
||||||
(event.key.keysym.mod & KMOD_SHIFT)) {
|
|
||||||
if (renderer && renderer->getWeather()) {
|
|
||||||
float intensity = renderer->getWeather()->getIntensity() + 0.1f;
|
|
||||||
renderer->getWeather()->setIntensity(intensity);
|
|
||||||
LOG_INFO("Weather intensity: ", static_cast<int>(intensity * 100), "%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// K: Spawn player character at camera position
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_K) {
|
|
||||||
spawnPlayerCharacter();
|
|
||||||
}
|
|
||||||
// J: Remove all characters
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_J) {
|
|
||||||
if (renderer && renderer->getCharacterRenderer()) {
|
|
||||||
// Note: CharacterRenderer doesn't have removeAll(), so we'd need to track IDs
|
|
||||||
// For now, just log
|
|
||||||
LOG_INFO("Character removal not yet implemented");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// N: Toggle minimap
|
// N: Toggle minimap
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_N) {
|
else if (event.key.keysym.scancode == SDL_SCANCODE_N) {
|
||||||
if (renderer && renderer->getMinimap()) {
|
if (renderer && renderer->getMinimap()) {
|
||||||
renderer->getMinimap()->toggle();
|
renderer->getMinimap()->toggle();
|
||||||
LOG_INFO("Minimap ", renderer->getMinimap()->isEnabled() ? "enabled" : "disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// P: Remove all WMO buildings (O key removed)
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_P) {
|
|
||||||
if (renderer && renderer->getWMORenderer()) {
|
|
||||||
renderer->getWMORenderer()->clearInstances();
|
|
||||||
LOG_INFO("Cleared all WMO instances");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ namespace game {
|
||||||
|
|
||||||
GameHandler::GameHandler() {
|
GameHandler::GameHandler() {
|
||||||
LOG_DEBUG("GameHandler created");
|
LOG_DEBUG("GameHandler created");
|
||||||
|
|
||||||
|
// Default spells always available
|
||||||
|
knownSpells.push_back(6603); // Attack
|
||||||
|
knownSpells.push_back(8690); // Hearthstone
|
||||||
|
|
||||||
|
// Default action bar layout
|
||||||
|
actionBar[0].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[0].id = 6603; // Attack in slot 1
|
||||||
|
actionBar[11].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[11].id = 8690; // Hearthstone in slot 12
|
||||||
}
|
}
|
||||||
|
|
||||||
GameHandler::~GameHandler() {
|
GameHandler::~GameHandler() {
|
||||||
|
|
@ -780,19 +790,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract health/mana/power from fields (Phase 2)
|
// Extract health/mana/power from fields (Phase 2) — single pass
|
||||||
if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) {
|
if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
auto hpIt = block.fields.find(24); // UNIT_FIELD_HEALTH
|
for (const auto& [key, val] : block.fields) {
|
||||||
if (hpIt != block.fields.end()) unit->setHealth(hpIt->second);
|
switch (key) {
|
||||||
auto maxHpIt = block.fields.find(32); // UNIT_FIELD_MAXHEALTH
|
case 24: unit->setHealth(val); break;
|
||||||
if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second);
|
case 25: unit->setPower(val); break;
|
||||||
auto powerIt = block.fields.find(25); // UNIT_FIELD_POWER1
|
case 32: unit->setMaxHealth(val); break;
|
||||||
if (powerIt != block.fields.end()) unit->setPower(powerIt->second);
|
case 33: unit->setMaxPower(val); break;
|
||||||
auto maxPowerIt = block.fields.find(33); // UNIT_FIELD_MAXPOWER1
|
case 54: unit->setLevel(val); break;
|
||||||
if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second);
|
default: break;
|
||||||
auto levelIt = block.fields.find(54); // UNIT_FIELD_LEVEL
|
}
|
||||||
if (levelIt != block.fields.end()) unit->setLevel(levelIt->second);
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -805,19 +815,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
entity->setField(field.first, field.second);
|
entity->setField(field.first, field.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cached health/mana/power values (Phase 2)
|
// Update cached health/mana/power values (Phase 2) — single pass
|
||||||
if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
auto hpIt = block.fields.find(24);
|
for (const auto& [key, val] : block.fields) {
|
||||||
if (hpIt != block.fields.end()) unit->setHealth(hpIt->second);
|
switch (key) {
|
||||||
auto maxHpIt = block.fields.find(32);
|
case 24: unit->setHealth(val); break;
|
||||||
if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second);
|
case 25: unit->setPower(val); break;
|
||||||
auto powerIt = block.fields.find(25);
|
case 32: unit->setMaxHealth(val); break;
|
||||||
if (powerIt != block.fields.end()) unit->setPower(powerIt->second);
|
case 33: unit->setMaxPower(val); break;
|
||||||
auto maxPowerIt = block.fields.find(33);
|
case 54: unit->setLevel(val); break;
|
||||||
if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second);
|
default: break;
|
||||||
auto levelIt = block.fields.find(54);
|
}
|
||||||
if (levelIt != block.fields.end()) unit->setLevel(levelIt->second);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
||||||
|
|
@ -1013,22 +1023,10 @@ void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
||||||
void GameHandler::addLocalChatMessage(const MessageChatData& msg) {
|
void GameHandler::addLocalChatMessage(const MessageChatData& msg) {
|
||||||
chatHistory.push_back(msg);
|
chatHistory.push_back(msg);
|
||||||
if (chatHistory.size() > maxChatHistory) {
|
if (chatHistory.size() > maxChatHistory) {
|
||||||
chatHistory.erase(chatHistory.begin());
|
chatHistory.pop_front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<MessageChatData> GameHandler::getChatHistory(size_t maxMessages) const {
|
|
||||||
if (maxMessages == 0 || maxMessages >= chatHistory.size()) {
|
|
||||||
return chatHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return last N messages
|
|
||||||
return std::vector<MessageChatData>(
|
|
||||||
chatHistory.end() - maxMessages,
|
|
||||||
chatHistory.end()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Phase 1: Name Queries
|
// Phase 1: Name Queries
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1207,6 +1205,20 @@ void GameHandler::handleSpellHealLog(network::Packet& packet) {
|
||||||
|
|
||||||
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||||
if (state != WorldState::IN_WORLD || !socket) return;
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
|
|
||||||
|
// Attack (6603) routes to auto-attack instead of cast
|
||||||
|
if (spellId == 6603) {
|
||||||
|
uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid;
|
||||||
|
if (target != 0) {
|
||||||
|
if (autoAttacking) {
|
||||||
|
stopAutoAttack();
|
||||||
|
} else {
|
||||||
|
startAutoAttack(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (casting) return; // Already casting
|
if (casting) return; // Already casting
|
||||||
|
|
||||||
uint64_t target = targetGuid != 0 ? targetGuid : targetGuid;
|
uint64_t target = targetGuid != 0 ? targetGuid : targetGuid;
|
||||||
|
|
@ -1249,6 +1261,14 @@ void GameHandler::handleInitialSpells(network::Packet& packet) {
|
||||||
|
|
||||||
knownSpells = data.spellIds;
|
knownSpells = data.spellIds;
|
||||||
|
|
||||||
|
// Ensure Attack (6603) and Hearthstone (8690) are always present
|
||||||
|
if (std::find(knownSpells.begin(), knownSpells.end(), 6603u) == knownSpells.end()) {
|
||||||
|
knownSpells.insert(knownSpells.begin(), 6603u);
|
||||||
|
}
|
||||||
|
if (std::find(knownSpells.begin(), knownSpells.end(), 8690u) == knownSpells.end()) {
|
||||||
|
knownSpells.push_back(8690u);
|
||||||
|
}
|
||||||
|
|
||||||
// Set initial cooldowns
|
// Set initial cooldowns
|
||||||
for (const auto& cd : data.cooldowns) {
|
for (const auto& cd : data.cooldowns) {
|
||||||
if (cd.cooldownMs > 0) {
|
if (cd.cooldownMs > 0) {
|
||||||
|
|
@ -1256,10 +1276,17 @@ void GameHandler::handleInitialSpells(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-populate action bar with first 12 spells
|
// Auto-populate action bar: Attack in slot 1, Hearthstone in slot 12, rest filled with known spells
|
||||||
for (int i = 0; i < ACTION_BAR_SLOTS && i < static_cast<int>(knownSpells.size()); ++i) {
|
actionBar[0].type = ActionBarSlot::SPELL;
|
||||||
actionBar[i].type = ActionBarSlot::SPELL;
|
actionBar[0].id = 6603; // Attack
|
||||||
actionBar[i].id = knownSpells[i];
|
actionBar[11].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[11].id = 8690; // Hearthstone
|
||||||
|
int slot = 1;
|
||||||
|
for (int i = 0; i < static_cast<int>(knownSpells.size()) && slot < 11; ++i) {
|
||||||
|
if (knownSpells[i] == 6603 || knownSpells[i] == 8690) continue;
|
||||||
|
actionBar[slot].type = ActionBarSlot::SPELL;
|
||||||
|
actionBar[slot].id = knownSpells[i];
|
||||||
|
slot++;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Learned ", knownSpells.size(), " spells");
|
LOG_INFO("Learned ", knownSpells.size(), " spells");
|
||||||
|
|
|
||||||
|
|
@ -757,11 +757,10 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
||||||
// Read sender name length + name
|
// Read sender name length + name
|
||||||
uint32_t nameLen = packet.readUInt32();
|
uint32_t nameLen = packet.readUInt32();
|
||||||
if (nameLen > 0 && nameLen < 256) {
|
if (nameLen > 0 && nameLen < 256) {
|
||||||
std::vector<char> nameBuffer(nameLen);
|
data.senderName.resize(nameLen);
|
||||||
for (uint32_t i = 0; i < nameLen; ++i) {
|
for (uint32_t i = 0; i < nameLen; ++i) {
|
||||||
nameBuffer[i] = static_cast<char>(packet.readUInt8());
|
data.senderName[i] = static_cast<char>(packet.readUInt8());
|
||||||
}
|
}
|
||||||
data.senderName = std::string(nameBuffer.begin(), nameBuffer.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read receiver GUID (usually 0 for monsters)
|
// Read receiver GUID (usually 0 for monsters)
|
||||||
|
|
@ -798,11 +797,10 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
||||||
|
|
||||||
// Read message
|
// Read message
|
||||||
if (messageLen > 0 && messageLen < 8192) {
|
if (messageLen > 0 && messageLen < 8192) {
|
||||||
std::vector<char> msgBuffer(messageLen);
|
data.message.resize(messageLen);
|
||||||
for (uint32_t i = 0; i < messageLen; ++i) {
|
for (uint32_t i = 0; i < messageLen; ++i) {
|
||||||
msgBuffer[i] = static_cast<char>(packet.readUInt8());
|
data.message[i] = static_cast<char>(packet.readUInt8());
|
||||||
}
|
}
|
||||||
data.message = std::string(msgBuffer.begin(), msgBuffer.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read chat tag
|
// Read chat tag
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,12 @@ float getEffectiveCollisionTopLocal(const M2ModelGPU& model,
|
||||||
|
|
||||||
float h = localMax.z - localMin.z;
|
float h = localMax.z - localMin.z;
|
||||||
if (model.collisionSteppedFountain) {
|
if (model.collisionSteppedFountain) {
|
||||||
if (r > 0.88f) return localMin.z + h * 0.20f; // outer lip
|
if (r > 0.85f) return localMin.z + h * 0.18f; // outer lip
|
||||||
if (r > 0.62f) return localMin.z + h * 0.42f; // mid step
|
if (r > 0.65f) return localMin.z + h * 0.36f; // mid step
|
||||||
if (r > 0.36f) return localMin.z + h * 0.66f; // inner step
|
if (r > 0.45f) return localMin.z + h * 0.54f; // inner step
|
||||||
return localMin.z + h * 0.90f; // center/top approach
|
if (r > 0.28f) return localMin.z + h * 0.70f; // center platform / statue base
|
||||||
|
if (r > 0.14f) return localMin.z + h * 0.84f; // statue body / sword
|
||||||
|
return localMin.z + h * 0.96f; // statue head / top
|
||||||
}
|
}
|
||||||
|
|
||||||
// Low square curb/planter profile:
|
// Low square curb/planter profile:
|
||||||
|
|
@ -239,6 +241,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
uniform sampler2D uTexture;
|
uniform sampler2D uTexture;
|
||||||
uniform bool uHasTexture;
|
uniform bool uHasTexture;
|
||||||
uniform bool uAlphaTest;
|
uniform bool uAlphaTest;
|
||||||
|
uniform float uFadeAlpha;
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
|
@ -255,6 +258,12 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Distance fade - discard nearly invisible fragments
|
||||||
|
float finalAlpha = texColor.a * uFadeAlpha;
|
||||||
|
if (finalAlpha < 0.02) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
vec3 normal = normalize(Normal);
|
vec3 normal = normalize(Normal);
|
||||||
vec3 lightDir = normalize(uLightDir);
|
vec3 lightDir = normalize(uLightDir);
|
||||||
|
|
||||||
|
|
@ -265,7 +274,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
vec3 diffuse = diff * texColor.rgb;
|
vec3 diffuse = diff * texColor.rgb;
|
||||||
|
|
||||||
vec3 result = ambient + diffuse;
|
vec3 result = ambient + diffuse;
|
||||||
FragColor = vec4(result, texColor.a);
|
FragColor = vec4(result, finalAlpha);
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
|
@ -364,7 +373,13 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
|
|
||||||
bool isPlanter = (lowerName.find("planter") != std::string::npos);
|
bool isPlanter = (lowerName.find("planter") != std::string::npos);
|
||||||
gpuModel.collisionPlanter = isPlanter;
|
gpuModel.collisionPlanter = isPlanter;
|
||||||
|
bool statueName =
|
||||||
|
(lowerName.find("statue") != std::string::npos) ||
|
||||||
|
(lowerName.find("monument") != std::string::npos) ||
|
||||||
|
(lowerName.find("sculpture") != std::string::npos);
|
||||||
|
gpuModel.collisionStatue = statueName;
|
||||||
bool smallSolidPropName =
|
bool smallSolidPropName =
|
||||||
|
statueName ||
|
||||||
(lowerName.find("crate") != std::string::npos) ||
|
(lowerName.find("crate") != std::string::npos) ||
|
||||||
(lowerName.find("box") != std::string::npos) ||
|
(lowerName.find("box") != std::string::npos) ||
|
||||||
(lowerName.find("chest") != std::string::npos) ||
|
(lowerName.find("chest") != std::string::npos) ||
|
||||||
|
|
@ -402,7 +417,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
!gpuModel.collisionSteppedLowPlatform &&
|
!gpuModel.collisionSteppedLowPlatform &&
|
||||||
(narrowVerticalName || narrowVerticalShape);
|
(narrowVerticalName || narrowVerticalShape);
|
||||||
bool genericSolidPropShape =
|
bool genericSolidPropShape =
|
||||||
(horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f);
|
(horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f) ||
|
||||||
|
statueName;
|
||||||
bool curbLikeName =
|
bool curbLikeName =
|
||||||
(lowerName.find("curb") != std::string::npos) ||
|
(lowerName.find("curb") != std::string::npos) ||
|
||||||
(lowerName.find("planter") != std::string::npos) ||
|
(lowerName.find("planter") != std::string::npos) ||
|
||||||
|
|
@ -619,12 +635,10 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
// Set up GL state for M2 rendering
|
// Set up GL state for M2 rendering
|
||||||
glEnable(GL_DEPTH_TEST);
|
glEnable(GL_DEPTH_TEST);
|
||||||
glDepthFunc(GL_LEQUAL);
|
glDepthFunc(GL_LEQUAL);
|
||||||
glDisable(GL_BLEND); // No blend leaking from prior renderers
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided
|
glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided
|
||||||
|
|
||||||
// Make models render with a bright color for debugging
|
|
||||||
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Wireframe mode
|
|
||||||
|
|
||||||
// Build frustum for culling
|
// Build frustum for culling
|
||||||
Frustum frustum;
|
Frustum frustum;
|
||||||
frustum.extractFromMatrix(projection * view);
|
frustum.extractFromMatrix(projection * view);
|
||||||
|
|
@ -640,6 +654,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
// Distance-based culling threshold for M2 models
|
// Distance-based culling threshold for M2 models
|
||||||
const float maxRenderDistance = 180.0f; // Aggressive culling for city performance
|
const float maxRenderDistance = 180.0f; // Aggressive culling for city performance
|
||||||
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||||
|
const float fadeStartFraction = 0.75f; // Start fading at 75% of max distance
|
||||||
const glm::vec3 camPos = camera.getPosition();
|
const glm::vec3 camPos = camera.getPosition();
|
||||||
|
|
||||||
for (const auto& instance : instances) {
|
for (const auto& instance : instances) {
|
||||||
|
|
@ -669,9 +684,25 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Distance-based fade alpha for smooth pop-in
|
||||||
|
float fadeAlpha = 1.0f;
|
||||||
|
float fadeStartDistSq = effectiveMaxDistSq * fadeStartFraction * fadeStartFraction;
|
||||||
|
if (distSq > fadeStartDistSq) {
|
||||||
|
float dist = std::sqrt(distSq);
|
||||||
|
float effectiveMaxDist = std::sqrt(effectiveMaxDistSq);
|
||||||
|
float fadeStartDist = effectiveMaxDist * fadeStartFraction;
|
||||||
|
fadeAlpha = std::clamp((effectiveMaxDist - dist) / (effectiveMaxDist - fadeStartDist), 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
shader->setUniform("uModel", instance.modelMatrix);
|
shader->setUniform("uModel", instance.modelMatrix);
|
||||||
shader->setUniform("uTime", instance.animTime);
|
shader->setUniform("uTime", instance.animTime);
|
||||||
shader->setUniform("uAnimScale", 0.0f); // Disabled - proper M2 animation needs bone/particle systems
|
shader->setUniform("uAnimScale", 0.0f); // Disabled - proper M2 animation needs bone/particle systems
|
||||||
|
shader->setUniform("uFadeAlpha", fadeAlpha);
|
||||||
|
|
||||||
|
// Disable depth writes for fading objects to avoid z-fighting
|
||||||
|
if (fadeAlpha < 1.0f) {
|
||||||
|
glDepthMask(GL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
glBindVertexArray(model.vao);
|
glBindVertexArray(model.vao);
|
||||||
|
|
||||||
|
|
@ -694,22 +725,15 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
lastDrawCallCount++;
|
lastDrawCallCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for GL errors (only first draw)
|
|
||||||
static bool checkedOnce = false;
|
|
||||||
if (!checkedOnce) {
|
|
||||||
checkedOnce = true;
|
|
||||||
GLenum err = glGetError();
|
|
||||||
if (err != GL_NO_ERROR) {
|
|
||||||
LOG_ERROR("GL error after M2 draw: ", err);
|
|
||||||
} else {
|
|
||||||
LOG_INFO("M2 draw successful: ", model.indexCount, " indices");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
if (fadeAlpha < 1.0f) {
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore cull face state
|
// Restore state
|
||||||
|
glDisable(GL_BLEND);
|
||||||
glEnable(GL_CULL_FACE);
|
glEnable(GL_CULL_FACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -942,8 +966,12 @@ std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ)
|
||||||
|
|
||||||
// Reachability filter: allow a bit more climb for stepped low platforms.
|
// Reachability filter: allow a bit more climb for stepped low platforms.
|
||||||
float maxStepUp = 1.0f;
|
float maxStepUp = 1.0f;
|
||||||
if (model.collisionSmallSolidProp) {
|
if (model.collisionStatue) {
|
||||||
|
maxStepUp = 2.5f;
|
||||||
|
} else if (model.collisionSmallSolidProp) {
|
||||||
maxStepUp = 2.0f;
|
maxStepUp = 2.0f;
|
||||||
|
} else if (model.collisionSteppedFountain) {
|
||||||
|
maxStepUp = 2.5f;
|
||||||
} else if (model.collisionSteppedLowPlatform) {
|
} else if (model.collisionSteppedLowPlatform) {
|
||||||
maxStepUp = model.collisionPlanter ? 3.0f : 2.4f;
|
maxStepUp = model.collisionPlanter ? 3.0f : 2.4f;
|
||||||
}
|
}
|
||||||
|
|
@ -1020,9 +1048,13 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
||||||
// Swept hard clamp for taller blockers only.
|
// Swept hard clamp for taller blockers only.
|
||||||
// Low/stepable objects should be climbable and not "shove" the player off.
|
// Low/stepable objects should be climbable and not "shove" the player off.
|
||||||
float maxStepUp = 1.20f;
|
float maxStepUp = 1.20f;
|
||||||
if (model.collisionSmallSolidProp) {
|
if (model.collisionStatue) {
|
||||||
|
maxStepUp = 2.5f;
|
||||||
|
} else if (model.collisionSmallSolidProp) {
|
||||||
// Keep box/crate-class props hard-solid to prevent phase-through.
|
// Keep box/crate-class props hard-solid to prevent phase-through.
|
||||||
maxStepUp = 0.75f;
|
maxStepUp = 0.75f;
|
||||||
|
} else if (model.collisionSteppedFountain) {
|
||||||
|
maxStepUp = 2.5f;
|
||||||
} else if (model.collisionSteppedLowPlatform) {
|
} else if (model.collisionSteppedLowPlatform) {
|
||||||
maxStepUp = model.collisionPlanter ? 2.8f : 2.4f;
|
maxStepUp = model.collisionPlanter ? 2.8f : 2.4f;
|
||||||
}
|
}
|
||||||
|
|
@ -1070,7 +1102,7 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
|
||||||
if (allowEscapeRelax) {
|
if (allowEscapeRelax) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (model.collisionSteppedLowPlatform && stepableLowObject) {
|
if ((model.collisionSteppedLowPlatform || model.collisionSteppedFountain) && stepableLowObject) {
|
||||||
// Already on/near top surface: don't apply lateral push that ejects
|
// Already on/near top surface: don't apply lateral push that ejects
|
||||||
// the player from the object when landing.
|
// the player from the object when landing.
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -974,17 +974,17 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
swimEffects->render(*camera);
|
swimEffects->render(*camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute view/projection once for all sub-renderers
|
||||||
|
const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f);
|
||||||
|
const glm::mat4& projection = camera ? camera->getProjectionMatrix() : glm::mat4(1.0f);
|
||||||
|
|
||||||
// Render characters (after weather)
|
// Render characters (after weather)
|
||||||
if (characterRenderer && camera) {
|
if (characterRenderer && camera) {
|
||||||
glm::mat4 view = camera->getViewMatrix();
|
|
||||||
glm::mat4 projection = camera->getProjectionMatrix();
|
|
||||||
characterRenderer->render(*camera, view, projection);
|
characterRenderer->render(*camera, view, projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render WMO buildings (after characters, before UI)
|
// Render WMO buildings (after characters, before UI)
|
||||||
if (wmoRenderer && camera) {
|
if (wmoRenderer && camera) {
|
||||||
glm::mat4 view = camera->getViewMatrix();
|
|
||||||
glm::mat4 projection = camera->getProjectionMatrix();
|
|
||||||
auto wmoStart = std::chrono::steady_clock::now();
|
auto wmoStart = std::chrono::steady_clock::now();
|
||||||
wmoRenderer->render(*camera, view, projection);
|
wmoRenderer->render(*camera, view, projection);
|
||||||
auto wmoEnd = std::chrono::steady_clock::now();
|
auto wmoEnd = std::chrono::steady_clock::now();
|
||||||
|
|
@ -993,8 +993,6 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
|
|
||||||
// Render M2 doodads (trees, rocks, etc.)
|
// Render M2 doodads (trees, rocks, etc.)
|
||||||
if (m2Renderer && camera) {
|
if (m2Renderer && camera) {
|
||||||
glm::mat4 view = camera->getViewMatrix();
|
|
||||||
glm::mat4 projection = camera->getProjectionMatrix();
|
|
||||||
auto m2Start = std::chrono::steady_clock::now();
|
auto m2Start = std::chrono::steady_clock::now();
|
||||||
m2Renderer->render(*camera, view, projection);
|
m2Renderer->render(*camera, view, projection);
|
||||||
auto m2End = std::chrono::steady_clock::now();
|
auto m2End = std::chrono::steady_clock::now();
|
||||||
|
|
|
||||||
|
|
@ -936,9 +936,10 @@ std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float
|
||||||
int alphaY = glm::clamp(static_cast<int>((fracY / 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;
|
int alphaIndex = alphaY * 64 + alphaX;
|
||||||
|
|
||||||
std::vector<int> weights(chunk.layers.size(), 0);
|
int weights[4] = {0, 0, 0, 0};
|
||||||
|
size_t numLayers = std::min(chunk.layers.size(), static_cast<size_t>(4));
|
||||||
int accum = 0;
|
int accum = 0;
|
||||||
for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) {
|
for (size_t layerIdx = 1; layerIdx < numLayers; layerIdx++) {
|
||||||
int alpha = 0;
|
int alpha = 0;
|
||||||
if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast<int>(alphaScratch.size())) {
|
if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast<int>(alphaScratch.size())) {
|
||||||
alpha = alphaScratch[alphaIndex];
|
alpha = alphaScratch[alphaIndex];
|
||||||
|
|
@ -950,7 +951,7 @@ std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float
|
||||||
|
|
||||||
size_t bestLayer = 0;
|
size_t bestLayer = 0;
|
||||||
int bestWeight = weights[0];
|
int bestWeight = weights[0];
|
||||||
for (size_t i = 1; i < weights.size(); i++) {
|
for (size_t i = 1; i < numLayers; i++) {
|
||||||
if (weights[i] > bestWeight) {
|
if (weights[i] > bestWeight) {
|
||||||
bestWeight = weights[i];
|
bestWeight = weights[i];
|
||||||
bestLayer = i;
|
bestLayer = i;
|
||||||
|
|
|
||||||
|
|
@ -336,9 +336,12 @@ void TerrainRenderer::render(const Camera& camera) {
|
||||||
frustum.extractFromMatrix(viewProj);
|
frustum.extractFromMatrix(viewProj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render each chunk
|
// Render each chunk — track last-bound textures to skip redundant binds
|
||||||
renderedChunks = 0;
|
renderedChunks = 0;
|
||||||
culledChunks = 0;
|
culledChunks = 0;
|
||||||
|
GLuint lastBound[7] = {0, 0, 0, 0, 0, 0, 0};
|
||||||
|
int lastLayerConfig = -1; // track hasLayer1|hasLayer2|hasLayer3 bitmask
|
||||||
|
|
||||||
for (const auto& chunk : chunks) {
|
for (const auto& chunk : chunks) {
|
||||||
if (!chunk.isValid()) {
|
if (!chunk.isValid()) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -350,48 +353,63 @@ void TerrainRenderer::render(const Camera& camera) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind textures
|
// Bind base texture (slot 0) — skip if same as last chunk
|
||||||
glActiveTexture(GL_TEXTURE0);
|
if (chunk.baseTexture != lastBound[0]) {
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.baseTexture);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
shader->setUniform("uBaseTexture", 0);
|
glBindTexture(GL_TEXTURE_2D, chunk.baseTexture);
|
||||||
|
lastBound[0] = chunk.baseTexture;
|
||||||
|
}
|
||||||
|
|
||||||
// Bind layer textures and alphas
|
// Layer configuration
|
||||||
bool hasLayer1 = chunk.layerTextures.size() > 0;
|
bool hasLayer1 = chunk.layerTextures.size() > 0;
|
||||||
bool hasLayer2 = chunk.layerTextures.size() > 1;
|
bool hasLayer2 = chunk.layerTextures.size() > 1;
|
||||||
bool hasLayer3 = chunk.layerTextures.size() > 2;
|
bool hasLayer3 = chunk.layerTextures.size() > 2;
|
||||||
|
int layerConfig = (hasLayer1 ? 1 : 0) | (hasLayer2 ? 2 : 0) | (hasLayer3 ? 4 : 0);
|
||||||
|
|
||||||
shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0);
|
if (layerConfig != lastLayerConfig) {
|
||||||
shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0);
|
shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0);
|
||||||
shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0);
|
shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0);
|
||||||
|
shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0);
|
||||||
|
lastLayerConfig = layerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasLayer1) {
|
if (hasLayer1) {
|
||||||
glActiveTexture(GL_TEXTURE1);
|
if (chunk.layerTextures[0] != lastBound[1]) {
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]);
|
glActiveTexture(GL_TEXTURE1);
|
||||||
shader->setUniform("uLayer1Texture", 1);
|
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]);
|
||||||
|
lastBound[1] = chunk.layerTextures[0];
|
||||||
glActiveTexture(GL_TEXTURE4);
|
}
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]);
|
if (chunk.alphaTextures[0] != lastBound[4]) {
|
||||||
shader->setUniform("uLayer1Alpha", 4);
|
glActiveTexture(GL_TEXTURE4);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]);
|
||||||
|
lastBound[4] = chunk.alphaTextures[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLayer2) {
|
if (hasLayer2) {
|
||||||
glActiveTexture(GL_TEXTURE2);
|
if (chunk.layerTextures[1] != lastBound[2]) {
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]);
|
glActiveTexture(GL_TEXTURE2);
|
||||||
shader->setUniform("uLayer2Texture", 2);
|
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]);
|
||||||
|
lastBound[2] = chunk.layerTextures[1];
|
||||||
glActiveTexture(GL_TEXTURE5);
|
}
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]);
|
if (chunk.alphaTextures[1] != lastBound[5]) {
|
||||||
shader->setUniform("uLayer2Alpha", 5);
|
glActiveTexture(GL_TEXTURE5);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]);
|
||||||
|
lastBound[5] = chunk.alphaTextures[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLayer3) {
|
if (hasLayer3) {
|
||||||
glActiveTexture(GL_TEXTURE3);
|
if (chunk.layerTextures[2] != lastBound[3]) {
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]);
|
glActiveTexture(GL_TEXTURE3);
|
||||||
shader->setUniform("uLayer3Texture", 3);
|
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]);
|
||||||
|
lastBound[3] = chunk.layerTextures[2];
|
||||||
glActiveTexture(GL_TEXTURE6);
|
}
|
||||||
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]);
|
if (chunk.alphaTextures[2] != lastBound[6]) {
|
||||||
shader->setUniform("uLayer3Alpha", 6);
|
glActiveTexture(GL_TEXTURE6);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]);
|
||||||
|
lastBound[6] = chunk.alphaTextures[2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw chunk
|
// Draw chunk
|
||||||
|
|
|
||||||
|
|
@ -355,10 +355,7 @@ void WaterRenderer::render(const Camera& camera, float time) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLboolean cullEnabled = glIsEnabled(GL_CULL_FACE);
|
glDisable(GL_CULL_FACE);
|
||||||
if (cullEnabled) {
|
|
||||||
glDisable(GL_CULL_FACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable alpha blending for transparent water
|
// Enable alpha blending for transparent water
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
|
|
@ -395,10 +392,10 @@ void WaterRenderer::render(const Camera& camera, float time) {
|
||||||
// City/canal liquid profile: clearer water + stronger ripples/sun shimmer.
|
// City/canal liquid profile: clearer water + stronger ripples/sun shimmer.
|
||||||
// Stormwind canals typically use LiquidType 5 in this data set.
|
// Stormwind canals typically use LiquidType 5 in this data set.
|
||||||
bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);
|
bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);
|
||||||
float waveAmp = canalProfile ? 0.07f : 0.038f;
|
float waveAmp = canalProfile ? 0.07f : 0.12f;
|
||||||
float waveFreq = canalProfile ? 0.30f : 0.22f;
|
float waveFreq = canalProfile ? 0.30f : 0.18f;
|
||||||
float waveSpeed = canalProfile ? 1.20f : 0.90f;
|
float waveSpeed = canalProfile ? 1.20f : 1.60f;
|
||||||
float shimmerStrength = canalProfile ? 0.95f : 0.35f;
|
float shimmerStrength = canalProfile ? 0.95f : 0.50f;
|
||||||
float alphaScale = canalProfile ? 0.72f : 1.00f;
|
float alphaScale = canalProfile ? 0.72f : 1.00f;
|
||||||
|
|
||||||
waterShader->setUniform("waterColor", color);
|
waterShader->setUniform("waterColor", color);
|
||||||
|
|
@ -418,9 +415,7 @@ void WaterRenderer::render(const Camera& camera, float time) {
|
||||||
// Restore state
|
// Restore state
|
||||||
glDepthMask(GL_TRUE);
|
glDepthMask(GL_TRUE);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
if (cullEnabled) {
|
glEnable(GL_CULL_FACE);
|
||||||
glEnable(GL_CULL_FACE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
||||||
|
|
@ -747,7 +742,7 @@ glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
|
||||||
case 0: // Water
|
case 0: // Water
|
||||||
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
|
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
|
||||||
case 1: // Ocean
|
case 1: // Ocean
|
||||||
return glm::vec4(0.14f, 0.36f, 0.58f, 1.0f);
|
return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f);
|
||||||
case 2: // Magma
|
case 2: // Magma
|
||||||
return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f);
|
return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f);
|
||||||
case 3: // Slime
|
case 3: // Slime
|
||||||
|
|
@ -760,7 +755,7 @@ glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
|
||||||
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
|
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
|
||||||
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
||||||
switch (basicType) {
|
switch (basicType) {
|
||||||
case 1: return 0.48f; // Ocean
|
case 1: return 0.68f; // Ocean
|
||||||
case 2: return 0.72f; // Magma
|
case 2: return 0.72f; // Magma
|
||||||
case 3: return 0.62f; // Slime
|
case 3: return 0.62f; // Slime
|
||||||
default: return 0.38f; // Water
|
default: return 0.38f; // Water
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,16 @@ uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position
|
||||||
transformAABB(instance.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
|
transformAABB(instance.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
|
||||||
instance.worldBoundsMin, instance.worldBoundsMax);
|
instance.worldBoundsMin, instance.worldBoundsMax);
|
||||||
|
|
||||||
|
// Pre-compute world-space group bounds to avoid per-frame transformAABB
|
||||||
|
instance.worldGroupBounds.reserve(model.groups.size());
|
||||||
|
for (const auto& group : model.groups) {
|
||||||
|
glm::vec3 gMin, gMax;
|
||||||
|
transformAABB(instance.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax);
|
||||||
|
gMin -= glm::vec3(0.5f);
|
||||||
|
gMax += glm::vec3(0.5f);
|
||||||
|
instance.worldGroupBounds.emplace_back(gMin, gMax);
|
||||||
|
}
|
||||||
|
|
||||||
instances.push_back(instance);
|
instances.push_back(instance);
|
||||||
size_t idx = instances.size() - 1;
|
size_t idx = instances.size() - 1;
|
||||||
instanceIndexById[instance.id] = idx;
|
instanceIndexById[instance.id] = idx;
|
||||||
|
|
@ -480,22 +490,16 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
|
||||||
const ModelData& model = modelIt->second;
|
const ModelData& model = modelIt->second;
|
||||||
shader->setUniform("uModel", instance.modelMatrix);
|
shader->setUniform("uModel", instance.modelMatrix);
|
||||||
|
|
||||||
// Render all groups
|
// Render all groups using cached world-space bounds
|
||||||
for (const auto& group : model.groups) {
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
||||||
// Proper frustum culling using AABB test
|
if (frustumCulling && gi < instance.worldGroupBounds.size()) {
|
||||||
if (frustumCulling) {
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||||
// Transform all AABB corners to avoid false culling on rotated groups.
|
if (!frustum.intersectsAABB(gMin, gMax)) {
|
||||||
glm::vec3 actualMin, actualMax;
|
|
||||||
transformAABB(instance.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, actualMin, actualMax);
|
|
||||||
// Small pad reduces edge flicker from precision/camera jitter.
|
|
||||||
actualMin -= glm::vec3(0.5f);
|
|
||||||
actualMax += glm::vec3(0.5f);
|
|
||||||
if (!frustum.intersectsAABB(actualMin, actualMax)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGroup(group, model, instance.modelMatrix, view, projection);
|
renderGroup(model.groups[gi], model, instance.modelMatrix, view, projection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -991,8 +995,10 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
||||||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||||
|
|
||||||
// Only use floors below or near the query point
|
// Only use floors below or near the query point.
|
||||||
if (hitWorld.z <= glZ + 2.0f) {
|
// Callers already elevate glZ by +5..+6; keep buffer small
|
||||||
|
// to avoid selecting ceilings above the player.
|
||||||
|
if (hitWorld.z <= glZ + 0.5f) {
|
||||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||||
bestFloor = hitWorld.z;
|
bestFloor = hitWorld.z;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
#include "pipeline/dbc_loader.hpp"
|
#include "pipeline/dbc_loader.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <iomanip>
|
|
||||||
#include <sstream>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
|
|
@ -86,6 +84,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderGossipWindow(gameHandler);
|
renderGossipWindow(gameHandler);
|
||||||
renderVendorWindow(gameHandler);
|
renderVendorWindow(gameHandler);
|
||||||
|
|
||||||
|
// Spellbook (P key toggle handled inside)
|
||||||
|
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
||||||
|
|
||||||
// Inventory (B key toggle handled inside)
|
// Inventory (B key toggle handled inside)
|
||||||
inventoryScreen.render(gameHandler.getInventory());
|
inventoryScreen.render(gameHandler.getInventory());
|
||||||
|
|
||||||
|
|
@ -200,9 +201,9 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
// GUID
|
// GUID
|
||||||
ImGui::TableSetColumnIndex(0);
|
ImGui::TableSetColumnIndex(0);
|
||||||
std::stringstream guidStr;
|
char guidStr[24];
|
||||||
guidStr << "0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << guid;
|
snprintf(guidStr, sizeof(guidStr), "0x%016llX", (unsigned long long)guid);
|
||||||
ImGui::Text("%s", guidStr.str().c_str());
|
ImGui::Text("%s", guidStr);
|
||||||
|
|
||||||
// Type
|
// Type
|
||||||
ImGui::TableSetColumnIndex(1);
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
|
@ -258,9 +259,16 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
||||||
ImGui::SetNextWindowSize(ImVec2(600, 300), ImGuiCond_FirstUseEver);
|
auto* window = core::Application::getInstance().getWindow();
|
||||||
ImGui::SetNextWindowPos(ImVec2(520, 390), ImGuiCond_FirstUseEver);
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||||
ImGui::Begin("Chat", nullptr, ImGuiWindowFlags_NoCollapse);
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||||
|
float chatW = std::min(500.0f, screenW * 0.4f);
|
||||||
|
float chatH = 220.0f;
|
||||||
|
float chatX = 8.0f;
|
||||||
|
float chatY = screenH - chatH - 80.0f; // Above action bar
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(chatX, chatY), ImGuiCond_Always);
|
||||||
|
ImGui::Begin("Chat", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
|
||||||
|
|
||||||
// Chat history
|
// Chat history
|
||||||
const auto& chatHistory = gameHandler.getChatHistory();
|
const auto& chatHistory = gameHandler.getChatHistory();
|
||||||
|
|
@ -271,21 +279,13 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
||||||
ImVec4 color = getChatTypeColor(msg.type);
|
ImVec4 color = getChatTypeColor(msg.type);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
||||||
|
|
||||||
std::stringstream ss;
|
|
||||||
|
|
||||||
if (msg.type == game::ChatType::TEXT_EMOTE) {
|
if (msg.type == game::ChatType::TEXT_EMOTE) {
|
||||||
ss << "You " << msg.message;
|
ImGui::TextWrapped("You %s", msg.message.c_str());
|
||||||
|
} else if (!msg.senderName.empty()) {
|
||||||
|
ImGui::TextWrapped("[%s] %s: %s", getChatTypeName(msg.type), msg.senderName.c_str(), msg.message.c_str());
|
||||||
} else {
|
} else {
|
||||||
ss << "[" << getChatTypeName(msg.type) << "] ";
|
ImGui::TextWrapped("[%s] %s", getChatTypeName(msg.type), msg.message.c_str());
|
||||||
|
|
||||||
if (!msg.senderName.empty()) {
|
|
||||||
ss << msg.senderName << ": ";
|
|
||||||
}
|
|
||||||
|
|
||||||
ss << msg.message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextWrapped("%s", ss.str().c_str());
|
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,6 +379,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slash key: focus chat input
|
||||||
|
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
||||||
|
refocusChatInput = true;
|
||||||
|
chatInputBuffer[0] = '/';
|
||||||
|
chatInputBuffer[1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter key: focus chat input (empty)
|
||||||
|
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
||||||
|
refocusChatInput = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Left-click targeting (when mouse not captured by UI)
|
// Left-click targeting (when mouse not captured by UI)
|
||||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
||||||
auto* renderer = core::Application::getInstance().getRenderer();
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
|
|
|
||||||
195
src/ui/spellbook_screen.cpp
Normal file
195
src/ui/spellbook_screen.cpp
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
#include "ui/spellbook_screen.hpp"
|
||||||
|
#include "core/input.hpp"
|
||||||
|
#include "core/application.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "pipeline/dbc_loader.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace wowee { namespace ui {
|
||||||
|
|
||||||
|
void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||||
|
if (dbcLoadAttempted) return;
|
||||||
|
dbcLoadAttempted = true;
|
||||||
|
|
||||||
|
if (!assetManager || !assetManager->isInitialized()) return;
|
||||||
|
|
||||||
|
auto dbc = assetManager->loadDBC("Spell.dbc");
|
||||||
|
if (!dbc || !dbc->isLoaded()) {
|
||||||
|
LOG_WARNING("Spellbook: Could not load Spell.dbc");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WoW 3.3.5a Spell.dbc: field 0 = SpellID, field 136 = SpellName_enUS
|
||||||
|
// Validate field count to determine name field index
|
||||||
|
uint32_t fieldCount = dbc->getFieldCount();
|
||||||
|
uint32_t nameField = 136;
|
||||||
|
|
||||||
|
if (fieldCount < 137) {
|
||||||
|
LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+");
|
||||||
|
// Try a heuristic: for smaller DBCs, name might be elsewhere
|
||||||
|
if (fieldCount > 10) {
|
||||||
|
nameField = fieldCount > 140 ? 136 : 1;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t count = dbc->getRecordCount();
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
uint32_t spellId = dbc->getUInt32(i, 0);
|
||||||
|
std::string name = dbc->getString(i, nameField);
|
||||||
|
if (!name.empty() && spellId > 0) {
|
||||||
|
spellNames[spellId] = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbcLoaded = true;
|
||||||
|
LOG_INFO("Spellbook: Loaded ", spellNames.size(), " spell names from Spell.dbc");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SpellbookScreen::getSpellName(uint32_t spellId) const {
|
||||||
|
auto it = spellNames.find(spellId);
|
||||||
|
if (it != spellNames.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof(buf), "Spell #%u", spellId);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) {
|
||||||
|
// P key toggle (edge-triggered)
|
||||||
|
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||||
|
bool pDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_P);
|
||||||
|
if (pDown && !pKeyWasDown) {
|
||||||
|
open = !open;
|
||||||
|
}
|
||||||
|
pKeyWasDown = pDown;
|
||||||
|
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
// Lazy-load Spell.dbc on first open
|
||||||
|
if (!dbcLoadAttempted) {
|
||||||
|
loadSpellDBC(assetManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* window = core::Application::getInstance().getWindow();
|
||||||
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||||
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||||
|
|
||||||
|
float bookW = 340.0f;
|
||||||
|
float bookH = std::min(500.0f, screenH - 120.0f);
|
||||||
|
float bookX = screenW - bookW - 10.0f;
|
||||||
|
float bookY = 80.0f;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(bookX, bookY), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(bookW, bookH), ImGuiCond_FirstUseEver);
|
||||||
|
|
||||||
|
bool windowOpen = open;
|
||||||
|
if (ImGui::Begin("Spellbook", &windowOpen)) {
|
||||||
|
const auto& spells = gameHandler.getKnownSpells();
|
||||||
|
|
||||||
|
if (spells.empty()) {
|
||||||
|
ImGui::TextDisabled("No spells known.");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("%zu spells known", spells.size());
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// Action bar assignment mode indicator
|
||||||
|
if (assigningSlot >= 0) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f),
|
||||||
|
"Click a spell to assign to slot %d", assigningSlot + 1);
|
||||||
|
if (ImGui::SmallButton("Cancel")) {
|
||||||
|
assigningSlot = -1;
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell list
|
||||||
|
ImGui::BeginChild("SpellList", ImVec2(0, -60), true);
|
||||||
|
|
||||||
|
for (uint32_t spellId : spells) {
|
||||||
|
ImGui::PushID(static_cast<int>(spellId));
|
||||||
|
|
||||||
|
std::string name = getSpellName(spellId);
|
||||||
|
float cd = gameHandler.getSpellCooldown(spellId);
|
||||||
|
bool onCooldown = cd > 0.0f;
|
||||||
|
|
||||||
|
// Color based on state
|
||||||
|
if (onCooldown) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell entry - clickable
|
||||||
|
char label[256];
|
||||||
|
if (onCooldown) {
|
||||||
|
snprintf(label, sizeof(label), "%s (%.1fs)", name.c_str(), cd);
|
||||||
|
} else {
|
||||||
|
snprintf(label, sizeof(label), "%s", name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Selectable(label, false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||||
|
if (assigningSlot >= 0) {
|
||||||
|
// Assign to action bar slot
|
||||||
|
gameHandler.setActionBarSlot(assigningSlot,
|
||||||
|
game::ActionBarSlot::SPELL, spellId);
|
||||||
|
assigningSlot = -1;
|
||||||
|
} else if (ImGui::IsMouseDoubleClicked(0)) {
|
||||||
|
// Double-click to cast
|
||||||
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||||
|
gameHandler.castSpell(spellId, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip with spell ID
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::Text("%s", name.c_str());
|
||||||
|
ImGui::TextDisabled("Spell ID: %u", spellId);
|
||||||
|
if (!onCooldown) {
|
||||||
|
ImGui::TextDisabled("Double-click to cast");
|
||||||
|
ImGui::TextDisabled("Use action bar buttons below to assign");
|
||||||
|
}
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onCooldown) {
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// Action bar quick-assign buttons
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text("Assign to:");
|
||||||
|
ImGui::SameLine();
|
||||||
|
static const char* slotLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
|
||||||
|
for (int i = 0; i < 12; ++i) {
|
||||||
|
if (i > 0) ImGui::SameLine(0, 2);
|
||||||
|
ImGui::PushID(100 + i);
|
||||||
|
bool isAssigning = (assigningSlot == i);
|
||||||
|
if (isAssigning) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.6f, 0.2f, 1.0f));
|
||||||
|
}
|
||||||
|
if (ImGui::SmallButton(slotLabels[i])) {
|
||||||
|
assigningSlot = isAssigning ? -1 : i;
|
||||||
|
}
|
||||||
|
if (isAssigning) {
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
if (!windowOpen) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace wowee::ui
|
||||||
Loading…
Add table
Add a link
Reference in a new issue