2026-02-02 12:24:50 -08:00
|
|
|
#include "ui/game_screen.hpp"
|
2026-03-25 12:27:43 -07:00
|
|
|
#include "ui/ui_colors.hpp"
|
2026-04-03 03:45:39 -07:00
|
|
|
#include "ui/ui_helpers.hpp"
|
2026-02-22 03:32:08 -08:00
|
|
|
#include "rendering/vk_context.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/application.hpp"
|
2026-04-01 20:38:37 +03:00
|
|
|
#include "core/appearance_composer.hpp"
|
2026-03-20 11:12:07 -07:00
|
|
|
#include "addons/addon_manager.hpp"
|
2026-02-04 17:37:28 -08:00
|
|
|
#include "core/coordinates.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/input.hpp"
|
|
|
|
|
#include "rendering/renderer.hpp"
|
2026-04-05 19:30:44 +03:00
|
|
|
#include "rendering/post_process_pipeline.hpp"
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
#include "rendering/animation_controller.hpp"
|
2026-02-23 01:10:58 -08:00
|
|
|
#include "rendering/wmo_renderer.hpp"
|
2026-02-21 01:26:16 -08:00
|
|
|
#include "rendering/terrain_manager.hpp"
|
2026-02-04 22:27:45 -08:00
|
|
|
#include "rendering/minimap.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#include "rendering/world_map.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/character_renderer.hpp"
|
|
|
|
|
#include "rendering/camera.hpp"
|
2026-02-05 17:55:30 -08:00
|
|
|
#include "rendering/camera_controller.hpp"
|
chore(renderer): extract AnimationController and remove audio pass-throughs
Extract ~1,500 lines of character animation state from Renderer into a dedicated
AnimationController class, and complete the AudioCoordinator migration by removing
all 10 audio pass-through getters from Renderer.
AnimationController:
- New: include/rendering/animation_controller.hpp (182 lines)
- New: src/rendering/animation_controller.cpp (1,703 lines)
- Moves: locomotion state machine (50+ members), mount animation (40+ members),
emote system, footstep triggering, surface detection, melee combat animations
- Renderer holds std::unique_ptr<AnimationController> and delegates completely
- AnimationController accesses audio via renderer_->getAudioCoordinator()
Audio caller migration:
- Migrate ~60 external callers from renderer->getXManager() to AudioCoordinator
directly, grouped by access pattern:
- UIServices: settings_panel, game_screen, toast_manager, chat_panel,
combat_ui, window_manager
- GameServices: game_handler, spell_handler, inventory_handler, quest_handler,
social_handler, combat_handler
- Application singleton: application.cpp, auth_screen.cpp, lua_engine.cpp
- Remove 10 pass-through getter definitions from renderer.cpp
- Remove 10 pass-through getter declarations from renderer.hpp
- Remove individual audio manager forward declarations from renderer.hpp
- Redirect 69 internal renderer.cpp audio calls to audioCoordinator_ directly
- game_handler.cpp: withSoundManager template uses services_.audioCoordinator;
MFP changed from &Renderer::getUiSoundManager to &AudioCoordinator::getUiSoundManager
- GameServices struct: add AudioCoordinator* audioCoordinator member
- settings_panel: applyAudioVolumes(Renderer*) -> applyAudioVolumes(AudioCoordinator*)
2026-04-02 13:06:31 +03:00
|
|
|
#include "audio/audio_coordinator.hpp"
|
2026-02-17 16:26:49 -08:00
|
|
|
#include "audio/audio_engine.hpp"
|
2026-02-05 16:17:04 -08:00
|
|
|
#include "audio/music_manager.hpp"
|
2026-02-17 16:26:49 -08:00
|
|
|
#include "game/zone_manager.hpp"
|
2026-02-05 17:55:30 -08:00
|
|
|
#include "audio/footstep_manager.hpp"
|
|
|
|
|
#include "audio/activity_sound_manager.hpp"
|
Implement comprehensive audio control panel with tabbed settings interface
Adds complete audio volume controls for all 11 audio systems with master volume. Reorganizes settings window into Video, Audio, and Gameplay tabs for better UX.
Audio Features:
- Master volume control affecting all audio systems
- Individual volume sliders for: Music, Ambient, UI, Combat, Spell, Movement, Footsteps, NPC Voices, Mounts, Activity sounds
- Real-time volume adjustment with master volume multiplier
- Restore defaults button per tab
Technical Changes:
- Added getVolumeScale() getters to all audio managers
- Integrated all 10 audio managers into renderer (UI, Combat, Spell, Movement added)
- Expanded game_screen.hpp with 11 pending volume variables
- Reorganized settings window using ImGui tab bars (Video/Audio/Gameplay)
- Audio settings uses scrollable child window for 11 volume controls
- Settings window expanded to 520x720px to accommodate comprehensive controls
2026-02-09 17:07:22 -08:00
|
|
|
#include "audio/mount_sound_manager.hpp"
|
|
|
|
|
#include "audio/npc_voice_manager.hpp"
|
|
|
|
|
#include "audio/ambient_sound_manager.hpp"
|
|
|
|
|
#include "audio/ui_sound_manager.hpp"
|
|
|
|
|
#include "audio/combat_sound_manager.hpp"
|
|
|
|
|
#include "audio/spell_sound_manager.hpp"
|
|
|
|
|
#include "audio/movement_sound_manager.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/dbc_loader.hpp"
|
2026-02-12 22:56:36 -08:00
|
|
|
#include "pipeline/dbc_layout.hpp"
|
2026-02-15 04:18:34 -08:00
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
#include "game/expansion_profile.hpp"
|
2026-03-12 10:41:18 -07:00
|
|
|
#include "game/character.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <imgui.h>
|
2026-02-14 22:00:26 -08:00
|
|
|
#include <imgui_internal.h>
|
2026-02-06 18:34:45 -08:00
|
|
|
#include <algorithm>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cmath>
|
2026-02-06 18:34:45 -08:00
|
|
|
#include <cstring>
|
2026-03-18 02:23:47 -07:00
|
|
|
#include <sstream>
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
2026-02-11 21:14:35 -08:00
|
|
|
#include <cctype>
|
2026-02-14 18:27:59 -08:00
|
|
|
#include <chrono>
|
|
|
|
|
#include <ctime>
|
2026-04-03 20:19:33 -07:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <unordered_set>
|
|
|
|
|
|
|
|
|
|
namespace {
|
2026-03-25 12:27:43 -07:00
|
|
|
using namespace wowee::ui::colors;
|
2026-04-03 03:45:39 -07:00
|
|
|
using namespace wowee::ui::helpers;
|
2026-03-25 12:27:43 -07:00
|
|
|
constexpr auto& kColorRed = kRed;
|
|
|
|
|
constexpr auto& kColorGreen = kGreen;
|
|
|
|
|
constexpr auto& kColorBrightGreen= kBrightGreen;
|
|
|
|
|
constexpr auto& kColorYellow = kYellow;
|
|
|
|
|
constexpr auto& kColorGray = kGray;
|
|
|
|
|
constexpr auto& kColorDarkGray = kDarkGray;
|
2026-03-25 11:57:22 -07:00
|
|
|
|
refactor: add 9 button/bar color constants, batch constexpr promotions
New ui_colors.hpp constants: kBtnGreen, kBtnGreenHover, kBtnRed,
kBtnRedHover, kBtnDkGreen/Hover, kBtnDkRed/Hover, kMidHealthYellow
— replacing 21 inline literals across accept/decline button and
health bar patterns.
Deduplicate kMon/kMonths month arrays (2 copies → 1 kMonthAbbrev).
Promote 22 remaining static const char*/int arrays to constexpr
(kQualHex, resLabels, kRepRankNames, kTotemNames, kReactLabels,
kChatHelp, kMacroHelp, kHelpLines, kMarkWords, componentDirs,
keyLabels, kRollLabels, gossipIcons, kMarkNames, kDiffLabels,
kStatLabels, kCatHeaders, kSlotNames, kResolutions, displayToInternal).
2026-03-27 14:44:52 -07:00
|
|
|
// Abbreviated month names (indexed 0-11)
|
|
|
|
|
constexpr const char* kMonthAbbrev[12] = {
|
|
|
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
|
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-25 11:57:22 -07:00
|
|
|
// Common ImGui window flags for popup dialogs
|
|
|
|
|
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) {
|
|
|
|
|
glm::vec3 oc = ray.origin - center;
|
|
|
|
|
float b = glm::dot(oc, ray.direction);
|
|
|
|
|
float c = glm::dot(oc, oc) - radius * radius;
|
|
|
|
|
float discriminant = b * b - c;
|
|
|
|
|
if (discriminant < 0.0f) return false;
|
|
|
|
|
float t = -b - std::sqrt(discriminant);
|
|
|
|
|
if (t < 0.0f) t = -b + std::sqrt(discriminant);
|
|
|
|
|
if (t < 0.0f) return false;
|
|
|
|
|
tOut = t;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string getEntityName(const std::shared_ptr<wowee::game::Entity>& entity) {
|
|
|
|
|
if (entity->getType() == wowee::game::ObjectType::PLAYER) {
|
|
|
|
|
auto player = std::static_pointer_cast<wowee::game::Player>(entity);
|
|
|
|
|
if (!player->getName().empty()) return player->getName();
|
|
|
|
|
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
|
|
|
|
|
if (!unit->getName().empty()) return unit->getName();
|
2026-02-08 00:59:40 -08:00
|
|
|
} else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
auto go = std::static_pointer_cast<wowee::game::GameObject>(entity);
|
|
|
|
|
if (!go->getName().empty()) return go->getName();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
return "Unknown";
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
2026-04-06 22:43:13 +03:00
|
|
|
// Draw a four-edge screen vignette (gradient overlay along each edge).
|
|
|
|
|
// Used for damage flash, low-health pulse, and level-up golden burst.
|
|
|
|
|
void drawScreenEdgeVignette(uint8_t r, uint8_t g, uint8_t b,
|
|
|
|
|
int alpha, float thicknessRatio) {
|
|
|
|
|
if (alpha <= 0) return;
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
const float W = ImGui::GetIO().DisplaySize.x;
|
|
|
|
|
const float H = ImGui::GetIO().DisplaySize.y;
|
|
|
|
|
const float thickness = std::min(W, H) * thicknessRatio;
|
|
|
|
|
const ImU32 edgeCol = IM_COL32(r, g, b, alpha);
|
|
|
|
|
const ImU32 fadeCol = IM_COL32(r, g, b, 0);
|
|
|
|
|
// Top
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness),
|
|
|
|
|
edgeCol, edgeCol, fadeCol, fadeCol);
|
|
|
|
|
// Bottom
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H),
|
|
|
|
|
fadeCol, fadeCol, edgeCol, edgeCol);
|
|
|
|
|
// Left
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H),
|
|
|
|
|
edgeCol, fadeCol, fadeCol, edgeCol);
|
|
|
|
|
// Right
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H),
|
|
|
|
|
fadeCol, edgeCol, edgeCol, fadeCol);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
|
|
|
|
|
|
GameScreen::GameScreen() {
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
loadSettings();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
// Set UI services and propagate to child components
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
void GameScreen::setServices(const UIServices& services) {
|
|
|
|
|
services_ = services;
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
// Update legacy pointer for compatibility
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
appearanceComposer_ = services.appearanceComposer;
|
|
|
|
|
// Propagate to child panels
|
|
|
|
|
chatPanel_.setServices(services);
|
|
|
|
|
toastManager_.setServices(services);
|
|
|
|
|
dialogManager_.setServices(services);
|
|
|
|
|
settingsPanel_.setServices(services);
|
|
|
|
|
combatUI_.setServices(services);
|
|
|
|
|
socialPanel_.setServices(services);
|
|
|
|
|
actionBarPanel_.setServices(services);
|
|
|
|
|
windowManager_.setServices(services);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void GameScreen::render(game::GameHandler& gameHandler) {
|
2026-03-31 08:53:14 +03:00
|
|
|
// Set up chat bubble callback (once) and cache game handler in ChatPanel
|
|
|
|
|
chatPanel_.setupCallbacks(gameHandler);
|
2026-03-31 09:18:17 +03:00
|
|
|
toastManager_.setupCallbacks(gameHandler);
|
2026-03-12 16:33:08 -07:00
|
|
|
|
2026-03-18 12:17:00 -07:00
|
|
|
// Set up appearance-changed callback to refresh inventory preview (barber shop, etc.)
|
|
|
|
|
if (!appearanceCallbackSet_) {
|
|
|
|
|
gameHandler.setAppearanceChangedCallback([this]() {
|
|
|
|
|
inventoryScreenCharGuid_ = 0; // force preview re-sync on next frame
|
|
|
|
|
});
|
|
|
|
|
appearanceCallbackSet_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 01:15:11 -07:00
|
|
|
// Set up UI error frame callback (once)
|
|
|
|
|
if (!uiErrorCallbackSet_) {
|
|
|
|
|
gameHandler.setUIErrorCallback([this](const std::string& msg) {
|
|
|
|
|
uiErrors_.push_back({msg, 0.0f});
|
|
|
|
|
if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin());
|
2026-03-13 10:05:10 -07:00
|
|
|
// Play error sound for each new error (rate-limited by deque cap of 5)
|
chore(renderer): extract AnimationController and remove audio pass-throughs
Extract ~1,500 lines of character animation state from Renderer into a dedicated
AnimationController class, and complete the AudioCoordinator migration by removing
all 10 audio pass-through getters from Renderer.
AnimationController:
- New: include/rendering/animation_controller.hpp (182 lines)
- New: src/rendering/animation_controller.cpp (1,703 lines)
- Moves: locomotion state machine (50+ members), mount animation (40+ members),
emote system, footstep triggering, surface detection, melee combat animations
- Renderer holds std::unique_ptr<AnimationController> and delegates completely
- AnimationController accesses audio via renderer_->getAudioCoordinator()
Audio caller migration:
- Migrate ~60 external callers from renderer->getXManager() to AudioCoordinator
directly, grouped by access pattern:
- UIServices: settings_panel, game_screen, toast_manager, chat_panel,
combat_ui, window_manager
- GameServices: game_handler, spell_handler, inventory_handler, quest_handler,
social_handler, combat_handler
- Application singleton: application.cpp, auth_screen.cpp, lua_engine.cpp
- Remove 10 pass-through getter definitions from renderer.cpp
- Remove 10 pass-through getter declarations from renderer.hpp
- Remove individual audio manager forward declarations from renderer.hpp
- Redirect 69 internal renderer.cpp audio calls to audioCoordinator_ directly
- game_handler.cpp: withSoundManager template uses services_.audioCoordinator;
MFP changed from &Renderer::getUiSoundManager to &AudioCoordinator::getUiSoundManager
- GameServices struct: add AudioCoordinator* audioCoordinator member
- settings_panel: applyAudioVolumes(Renderer*) -> applyAudioVolumes(AudioCoordinator*)
2026-04-02 13:06:31 +03:00
|
|
|
if (auto* ac = services_.audioCoordinator) {
|
|
|
|
|
if (auto* sfx = ac->getUiSoundManager()) sfx->playError();
|
2026-03-13 10:05:10 -07:00
|
|
|
}
|
2026-03-12 01:15:11 -07:00
|
|
|
});
|
|
|
|
|
uiErrorCallbackSet_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 04:30:33 -07:00
|
|
|
// Flash the action bar button whose spell just failed (0.5 s red overlay).
|
|
|
|
|
if (!castFailedCallbackSet_) {
|
|
|
|
|
gameHandler.setSpellCastFailedCallback([this](uint32_t spellId) {
|
|
|
|
|
if (spellId == 0) return;
|
|
|
|
|
float now = static_cast<float>(ImGui::GetTime());
|
2026-03-31 19:49:52 +03:00
|
|
|
actionBarPanel_.actionFlashEndTimes_[spellId] = now + actionBarPanel_.kActionFlashDuration;
|
2026-03-18 04:30:33 -07:00
|
|
|
});
|
|
|
|
|
castFailedCallbackSet_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:19:39 -08:00
|
|
|
// Apply UI transparency setting
|
|
|
|
|
float prevAlpha = ImGui::GetStyle().Alpha;
|
2026-03-31 10:07:58 +03:00
|
|
|
ImGui::GetStyle().Alpha = settingsPanel_.uiOpacity_;
|
2026-02-06 20:19:39 -08:00
|
|
|
|
2026-02-23 08:01:20 -08:00
|
|
|
// Sync minimap opacity with UI opacity
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-23 08:01:20 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
minimap->setOpacity(settingsPanel_.uiOpacity_);
|
2026-02-23 08:01:20 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 16:26:49 -08:00
|
|
|
// Apply initial settings when renderer becomes available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.minimapSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-09 17:39:21 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.minimapRotate_ = false;
|
|
|
|
|
settingsPanel_.pendingMinimapRotate = false;
|
2026-02-11 17:30:57 -08:00
|
|
|
minimap->setRotateWithCamera(false);
|
2026-03-31 10:07:58 +03:00
|
|
|
minimap->setSquareShape(settingsPanel_.minimapSquare_);
|
|
|
|
|
settingsPanel_.minimapSettingsApplied_ = true;
|
2026-02-09 17:39:21 -08:00
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
if (auto* zm = renderer->getZoneManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
zm->setUseOriginalSoundtrack(settingsPanel_.pendingUseOriginalSoundtrack);
|
2026-02-17 16:26:49 -08:00
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
tm->setGroundClutterDensityScale(static_cast<float>(settingsPanel_.pendingGroundClutterDensity) / 100.0f);
|
2026-02-21 01:26:16 -08:00
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
// Restore mute state: save actual master volume first, then apply mute
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.soundMuted_) {
|
2026-02-17 16:26:49 -08:00
|
|
|
float actual = audio::AudioEngine::instance().getMasterVolume();
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.preMuteVolume_ = (actual > 0.0f) ? actual
|
|
|
|
|
: static_cast<float>(settingsPanel_.pendingMasterVolume) / 100.0f;
|
2026-02-17 16:26:49 -08:00
|
|
|
audio::AudioEngine::instance().setMasterVolume(0.0f);
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:37:20 -08:00
|
|
|
// Apply saved volume settings once when audio managers first become available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.volumeSettingsApplied_) {
|
chore(renderer): extract AnimationController and remove audio pass-throughs
Extract ~1,500 lines of character animation state from Renderer into a dedicated
AnimationController class, and complete the AudioCoordinator migration by removing
all 10 audio pass-through getters from Renderer.
AnimationController:
- New: include/rendering/animation_controller.hpp (182 lines)
- New: src/rendering/animation_controller.cpp (1,703 lines)
- Moves: locomotion state machine (50+ members), mount animation (40+ members),
emote system, footstep triggering, surface detection, melee combat animations
- Renderer holds std::unique_ptr<AnimationController> and delegates completely
- AnimationController accesses audio via renderer_->getAudioCoordinator()
Audio caller migration:
- Migrate ~60 external callers from renderer->getXManager() to AudioCoordinator
directly, grouped by access pattern:
- UIServices: settings_panel, game_screen, toast_manager, chat_panel,
combat_ui, window_manager
- GameServices: game_handler, spell_handler, inventory_handler, quest_handler,
social_handler, combat_handler
- Application singleton: application.cpp, auth_screen.cpp, lua_engine.cpp
- Remove 10 pass-through getter definitions from renderer.cpp
- Remove 10 pass-through getter declarations from renderer.hpp
- Remove individual audio manager forward declarations from renderer.hpp
- Redirect 69 internal renderer.cpp audio calls to audioCoordinator_ directly
- game_handler.cpp: withSoundManager template uses services_.audioCoordinator;
MFP changed from &Renderer::getUiSoundManager to &AudioCoordinator::getUiSoundManager
- GameServices struct: add AudioCoordinator* audioCoordinator member
- settings_panel: applyAudioVolumes(Renderer*) -> applyAudioVolumes(AudioCoordinator*)
2026-04-02 13:06:31 +03:00
|
|
|
auto* ac = services_.audioCoordinator;
|
|
|
|
|
if (ac && ac->getUiSoundManager()) {
|
|
|
|
|
settingsPanel_.applyAudioVolumes(ac);
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.volumeSettingsApplied_ = true;
|
2026-02-17 17:37:20 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
// Apply saved MSAA setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.msaaSettingsApplied_ && settingsPanel_.pendingAntiAliasing > 0) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-22 02:59:24 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
static const VkSampleCountFlagBits aaSamples[] = {
|
|
|
|
|
VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT,
|
|
|
|
|
VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT
|
|
|
|
|
};
|
2026-03-31 10:07:58 +03:00
|
|
|
renderer->setMsaaSamples(aaSamples[settingsPanel_.pendingAntiAliasing]);
|
|
|
|
|
settingsPanel_.msaaSettingsApplied_ = true;
|
2026-02-22 02:59:24 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.msaaSettingsApplied_ = true;
|
2026-02-22 02:59:24 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:43:48 -07:00
|
|
|
// Apply saved FXAA setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.fxaaSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-12 16:43:48 -07:00
|
|
|
if (renderer) {
|
2026-04-05 19:30:44 +03:00
|
|
|
renderer->getPostProcessPipeline()->setFXAAEnabled(settingsPanel_.pendingFXAA);
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.fxaaSettingsApplied_ = true;
|
2026-03-12 16:43:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
// Apply saved water refraction setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.waterRefractionApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-06 19:15:34 -08:00
|
|
|
if (renderer) {
|
2026-03-31 10:07:58 +03:00
|
|
|
renderer->setWaterRefractionEnabled(settingsPanel_.pendingWaterRefraction);
|
|
|
|
|
settingsPanel_.waterRefractionApplied_ = true;
|
2026-03-06 19:15:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
// Apply saved normal mapping / POM settings once when WMO renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.normalMapSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-23 01:10:58 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
wr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping);
|
|
|
|
|
wr->setNormalMapStrength(settingsPanel_.pendingNormalMapStrength);
|
|
|
|
|
wr->setPOMEnabled(settingsPanel_.pendingPOM);
|
|
|
|
|
wr->setPOMQuality(settingsPanel_.pendingPOMQuality);
|
2026-02-23 01:40:23 -08:00
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
cr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping);
|
|
|
|
|
cr->setNormalMapStrength(settingsPanel_.pendingNormalMapStrength);
|
|
|
|
|
cr->setPOMEnabled(settingsPanel_.pendingPOM);
|
|
|
|
|
cr->setPOMQuality(settingsPanel_.pendingPOMQuality);
|
2026-02-23 01:40:23 -08:00
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.normalMapSettingsApplied_ = true;
|
2026-02-23 01:10:58 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:22:11 -07:00
|
|
|
// Apply saved upscaling setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.fsrSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-07 22:03:28 -08:00
|
|
|
if (renderer) {
|
2026-03-27 14:47:58 -07:00
|
|
|
static constexpr float fsrScales[] = { 0.77f, 0.67f, 0.59f, 1.00f };
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.pendingFSRQuality = std::clamp(settingsPanel_.pendingFSRQuality, 0, 3);
|
2026-04-05 19:30:44 +03:00
|
|
|
renderer->getPostProcessPipeline()->setFSRQuality(fsrScales[settingsPanel_.pendingFSRQuality]);
|
|
|
|
|
renderer->getPostProcessPipeline()->setFSRSharpness(settingsPanel_.pendingFSRSharpness);
|
|
|
|
|
renderer->getPostProcessPipeline()->setFSR2DebugTuning(settingsPanel_.pendingFSR2JitterSign, settingsPanel_.pendingFSR2MotionVecScaleX, settingsPanel_.pendingFSR2MotionVecScaleY);
|
|
|
|
|
renderer->getPostProcessPipeline()->setAmdFsr3FramegenEnabled(settingsPanel_.pendingAMDFramegen);
|
2026-03-31 10:07:58 +03:00
|
|
|
int effectiveMode = settingsPanel_.pendingUpscalingMode;
|
2026-03-08 20:48:46 -07:00
|
|
|
|
2026-03-20 07:53:07 -07:00
|
|
|
// Defer FSR2/FSR3 activation until fully in-world to avoid
|
|
|
|
|
// init issues during login/character selection screens.
|
2026-03-08 20:48:46 -07:00
|
|
|
if (effectiveMode == 2 && gameHandler.getState() != game::WorldState::IN_WORLD) {
|
2026-03-08 20:45:26 -07:00
|
|
|
renderer->setFSREnabled(false);
|
|
|
|
|
renderer->setFSR2Enabled(false);
|
|
|
|
|
} else {
|
2026-03-08 20:48:46 -07:00
|
|
|
renderer->setFSREnabled(effectiveMode == 1);
|
|
|
|
|
renderer->setFSR2Enabled(effectiveMode == 2);
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.fsrSettingsApplied_ = true;
|
2026-03-08 20:45:26 -07:00
|
|
|
}
|
2026-03-07 22:03:28 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 20:21:06 -07:00
|
|
|
// Apply auto-loot / auto-sell settings to GameHandler every frame (cheap bool sync)
|
2026-03-31 10:07:58 +03:00
|
|
|
gameHandler.setAutoLoot(settingsPanel_.pendingAutoLoot);
|
|
|
|
|
gameHandler.setAutoSellGrey(settingsPanel_.pendingAutoSellGrey);
|
|
|
|
|
gameHandler.setAutoRepair(settingsPanel_.pendingAutoRepair);
|
2026-02-17 16:31:00 -08:00
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
// Sync chat auto-join settings to GameHandler
|
2026-03-31 08:53:14 +03:00
|
|
|
gameHandler.chatAutoJoin.general = chatPanel_.chatAutoJoinGeneral;
|
|
|
|
|
gameHandler.chatAutoJoin.trade = chatPanel_.chatAutoJoinTrade;
|
|
|
|
|
gameHandler.chatAutoJoin.localDefense = chatPanel_.chatAutoJoinLocalDefense;
|
|
|
|
|
gameHandler.chatAutoJoin.lfg = chatPanel_.chatAutoJoinLFG;
|
|
|
|
|
gameHandler.chatAutoJoin.local = chatPanel_.chatAutoJoinLocal;
|
2026-02-14 18:27:59 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Process targeting input before UI windows
|
|
|
|
|
processTargetInput(gameHandler);
|
|
|
|
|
|
|
|
|
|
renderPlayerFrame(gameHandler);
|
|
|
|
|
|
2026-03-09 17:23:28 -07:00
|
|
|
// Pet frame (below player frame, only when player has an active pet)
|
|
|
|
|
if (gameHandler.hasPet()) {
|
|
|
|
|
renderPetFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 20:59:29 -07:00
|
|
|
// Auto-open pet rename modal when server signals the pet is renameable (first tame)
|
|
|
|
|
if (gameHandler.consumePetRenameablePending()) {
|
|
|
|
|
petRenameOpen_ = true;
|
|
|
|
|
petRenameBuf_[0] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 10:14:44 -07:00
|
|
|
// Totem frame (Shaman only, when any totem is active)
|
|
|
|
|
if (gameHandler.getPlayerClass() == 7) {
|
|
|
|
|
renderTotemFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Target frame (only when we have a target)
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
renderTargetFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 21:15:24 -07:00
|
|
|
// Focus target frame (only when we have a focus)
|
|
|
|
|
if (gameHandler.hasFocus()) {
|
|
|
|
|
renderFocusFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Render windows
|
|
|
|
|
if (showPlayerInfo) {
|
|
|
|
|
renderPlayerInfo(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showEntityWindow) {
|
|
|
|
|
renderEntityList(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showChatWindow) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.getSpellIcon = [this](uint32_t id, pipeline::AssetManager* am) {
|
|
|
|
|
return getSpellIcon(id, am);
|
|
|
|
|
};
|
|
|
|
|
chatPanel_.render(gameHandler, inventoryScreen, spellbookScreen, questLogScreen);
|
|
|
|
|
// Process slash commands that affect GameScreen state
|
|
|
|
|
auto cmds = chatPanel_.consumeSlashCommands();
|
2026-03-31 19:49:52 +03:00
|
|
|
if (cmds.showInspect) socialPanel_.showInspectWindow_ = true;
|
|
|
|
|
if (cmds.toggleThreat) combatUI_.showThreatWindow_ = !combatUI_.showThreatWindow_;
|
|
|
|
|
if (cmds.showBgScore) combatUI_.showBgScoreboard_ = !combatUI_.showBgScoreboard_;
|
|
|
|
|
if (cmds.showGmTicket) windowManager_.showGmTicketWindow_ = true;
|
|
|
|
|
if (cmds.showWho) socialPanel_.showWhoWindow_ = true;
|
|
|
|
|
if (cmds.toggleCombatLog) combatUI_.showCombatLog_ = !combatUI_.showCombatLog_;
|
2026-03-31 08:53:14 +03:00
|
|
|
if (cmds.takeScreenshot) takeScreenshot(gameHandler);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
// ---- New UI elements ----
|
2026-03-31 19:49:52 +03:00
|
|
|
actionBarPanel_.renderActionBar(gameHandler, settingsPanel_, chatPanel_,
|
|
|
|
|
inventoryScreen, spellbookScreen, questLogScreen,
|
|
|
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
|
|
|
actionBarPanel_.renderStanceBar(gameHandler, settingsPanel_, spellbookScreen,
|
|
|
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
|
|
|
actionBarPanel_.renderBagBar(gameHandler, settingsPanel_, inventoryScreen);
|
|
|
|
|
actionBarPanel_.renderXpBar(gameHandler, settingsPanel_);
|
|
|
|
|
actionBarPanel_.renderRepBar(gameHandler, settingsPanel_);
|
|
|
|
|
auto spellIconFn = [this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); };
|
|
|
|
|
combatUI_.renderCastBar(gameHandler, spellIconFn);
|
2026-03-09 14:30:48 -07:00
|
|
|
renderMirrorTimers(gameHandler);
|
2026-03-31 19:49:52 +03:00
|
|
|
combatUI_.renderCooldownTracker(gameHandler, settingsPanel_, spellIconFn);
|
2026-03-09 15:05:38 -07:00
|
|
|
renderQuestObjectiveTracker(gameHandler);
|
2026-03-10 07:25:04 -07:00
|
|
|
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
|
2026-03-31 19:49:52 +03:00
|
|
|
combatUI_.renderBattlegroundScore(gameHandler);
|
|
|
|
|
combatUI_.renderRaidWarningOverlay(gameHandler);
|
|
|
|
|
combatUI_.renderCombatText(gameHandler);
|
|
|
|
|
combatUI_.renderDPSMeter(gameHandler, settingsPanel_);
|
2026-03-12 14:25:37 -07:00
|
|
|
renderDurabilityWarning(gameHandler);
|
2026-03-12 01:15:11 -07:00
|
|
|
renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime);
|
2026-03-31 09:18:17 +03:00
|
|
|
toastManager_.renderEarlyToasts(ImGui::GetIO().DeltaTime, gameHandler);
|
2026-03-31 19:49:52 +03:00
|
|
|
if (socialPanel_.showRaidFrames_) {
|
|
|
|
|
socialPanel_.renderPartyFrames(gameHandler, chatPanel_, spellIconFn);
|
2026-03-11 09:24:37 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.renderBossFrames(gameHandler, spellbookScreen, spellIconFn);
|
2026-03-31 10:07:58 +03:00
|
|
|
dialogManager_.renderDialogs(gameHandler, inventoryScreen, chatPanel_);
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.renderGuildRoster(gameHandler, chatPanel_);
|
|
|
|
|
socialPanel_.renderSocialFrame(gameHandler, chatPanel_);
|
|
|
|
|
combatUI_.renderBuffBar(gameHandler, spellbookScreen, spellIconFn);
|
|
|
|
|
windowManager_.renderLootWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderGossipWindow(gameHandler, chatPanel_);
|
|
|
|
|
windowManager_.renderQuestDetailsWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
|
|
|
windowManager_.renderQuestRequestItemsWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
|
|
|
windowManager_.renderQuestOfferRewardWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
|
|
|
windowManager_.renderVendorWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderTrainerWindow(gameHandler,
|
|
|
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
|
|
|
windowManager_.renderBarberShopWindow(gameHandler);
|
|
|
|
|
windowManager_.renderStableWindow(gameHandler);
|
|
|
|
|
windowManager_.renderTaxiWindow(gameHandler);
|
|
|
|
|
windowManager_.renderMailWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderMailComposeWindow(gameHandler, inventoryScreen);
|
|
|
|
|
windowManager_.renderBankWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderGuildBankWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderAuctionHouseWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
socialPanel_.renderDungeonFinderWindow(gameHandler, chatPanel_);
|
|
|
|
|
windowManager_.renderInstanceLockouts(gameHandler);
|
|
|
|
|
socialPanel_.renderWhoWindow(gameHandler, chatPanel_);
|
|
|
|
|
combatUI_.renderCombatLog(gameHandler, spellbookScreen);
|
|
|
|
|
windowManager_.renderAchievementWindow(gameHandler);
|
|
|
|
|
windowManager_.renderSkillsWindow(gameHandler);
|
|
|
|
|
windowManager_.renderTitlesWindow(gameHandler);
|
|
|
|
|
windowManager_.renderEquipSetWindow(gameHandler);
|
|
|
|
|
windowManager_.renderGmTicketWindow(gameHandler);
|
|
|
|
|
socialPanel_.renderInspectWindow(gameHandler, inventoryScreen);
|
|
|
|
|
windowManager_.renderBookWindow(gameHandler);
|
|
|
|
|
combatUI_.renderThreatWindow(gameHandler);
|
|
|
|
|
combatUI_.renderBgScoreboard(gameHandler);
|
2026-03-11 09:24:37 -07:00
|
|
|
if (showMinimap_) {
|
|
|
|
|
renderMinimapMarkers(gameHandler);
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.renderLogoutCountdown(gameHandler);
|
|
|
|
|
windowManager_.renderDeathScreen(gameHandler);
|
|
|
|
|
windowManager_.renderReclaimCorpseButton(gameHandler);
|
2026-03-31 10:07:58 +03:00
|
|
|
dialogManager_.renderLateDialogs(gameHandler);
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.renderBubbles(gameHandler);
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.renderEscapeMenu(settingsPanel_);
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.renderSettingsWindow(inventoryScreen, chatPanel_, [this]() { saveSettings(); });
|
2026-03-31 09:18:17 +03:00
|
|
|
toastManager_.renderLateToasts(gameHandler);
|
2026-03-17 16:34:39 -07:00
|
|
|
renderWeatherOverlay(gameHandler);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-02-04 22:27:45 -08:00
|
|
|
renderWorldMap(gameHandler);
|
|
|
|
|
|
2026-03-11 20:57:39 -07:00
|
|
|
questLogScreen.render(gameHandler, inventoryScreen);
|
2026-02-06 13:47:03 -08:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
spellbookScreen.render(gameHandler, services_.assetManager);
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-03-11 21:57:13 -07:00
|
|
|
// Insert spell link into chat if player shift-clicked a spellbook entry
|
|
|
|
|
{
|
|
|
|
|
std::string pendingSpellLink = spellbookScreen.getAndClearPendingChatLink();
|
|
|
|
|
if (!pendingSpellLink.empty()) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.insertChatLink(pendingSpellLink);
|
2026-03-11 21:57:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
// Talents (N key toggle handled inside)
|
|
|
|
|
talentScreen.render(gameHandler);
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
// Set up inventory screen asset manager + player appearance (re-init on character switch)
|
2026-02-06 14:24:38 -08:00
|
|
|
{
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
uint64_t activeGuid = gameHandler.getActiveCharacterGuid();
|
|
|
|
|
if (activeGuid != 0 && activeGuid != inventoryScreenCharGuid_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* am = services_.assetManager;
|
2026-02-06 14:24:38 -08:00
|
|
|
if (am) {
|
|
|
|
|
inventoryScreen.setAssetManager(am);
|
|
|
|
|
const auto* ch = gameHandler.getActiveCharacter();
|
|
|
|
|
if (ch) {
|
|
|
|
|
uint8_t skin = ch->appearanceBytes & 0xFF;
|
|
|
|
|
uint8_t face = (ch->appearanceBytes >> 8) & 0xFF;
|
|
|
|
|
uint8_t hairStyle = (ch->appearanceBytes >> 16) & 0xFF;
|
|
|
|
|
uint8_t hairColor = (ch->appearanceBytes >> 24) & 0xFF;
|
|
|
|
|
inventoryScreen.setPlayerAppearance(
|
|
|
|
|
ch->race, ch->gender, skin, face,
|
|
|
|
|
hairStyle, hairColor, ch->facialFeatures);
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
inventoryScreenCharGuid_ = activeGuid;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Set vendor mode before rendering inventory
|
|
|
|
|
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
|
|
|
|
|
2026-02-19 22:34:22 -08:00
|
|
|
// Auto-open bags once when vendor window first opens
|
2026-02-19 05:48:40 -08:00
|
|
|
if (gameHandler.isVendorWindowOpen()) {
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!windowManager_.vendorBagsOpened_) {
|
|
|
|
|
windowManager_.vendorBagsOpened_ = true;
|
2026-02-19 22:34:22 -08:00
|
|
|
if (inventoryScreen.isSeparateBags()) {
|
2026-02-19 05:48:40 -08:00
|
|
|
inventoryScreen.openAllBags();
|
2026-02-19 22:34:22 -08:00
|
|
|
} else if (!inventoryScreen.isOpen()) {
|
|
|
|
|
inventoryScreen.setOpen(true);
|
2026-02-19 05:48:40 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 22:34:22 -08:00
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.vendorBagsOpened_ = false;
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
inventoryScreen.setGameHandler(&gameHandler);
|
2026-02-05 14:01:26 -08:00
|
|
|
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-11 21:11:58 -07:00
|
|
|
// Character screen (C key toggle handled inside render())
|
|
|
|
|
inventoryScreen.renderCharacterScreen(gameHandler);
|
|
|
|
|
|
|
|
|
|
// Insert item link into chat if player shift-clicked any inventory/equipment slot
|
2026-03-11 21:09:42 -07:00
|
|
|
{
|
|
|
|
|
std::string pendingLink = inventoryScreen.getAndClearPendingChatLink();
|
|
|
|
|
if (!pendingLink.empty()) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.insertChatLink(pendingLink);
|
2026-03-11 21:09:42 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:13:42 -08:00
|
|
|
if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) {
|
2026-02-02 12:24:50 -08:00
|
|
|
updateCharacterGeosets(gameHandler.getInventory());
|
|
|
|
|
updateCharacterTextures(gameHandler.getInventory());
|
2026-04-01 20:38:37 +03:00
|
|
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons();
|
2026-02-06 14:24:38 -08:00
|
|
|
inventoryScreen.markPreviewDirty();
|
2026-02-06 15:41:29 -08:00
|
|
|
// Update renderer weapon type for animation selection
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* r = services_.renderer;
|
2026-02-06 15:41:29 -08:00
|
|
|
if (r) {
|
|
|
|
|
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
const auto& oh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::OFF_HAND);
|
|
|
|
|
if (mh.empty()) {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedWeaponType(0, false);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
} else {
|
|
|
|
|
// Polearms and staves use ATTACK_2H_LOOSE instead of ATTACK_2H
|
|
|
|
|
bool is2HLoose = (mh.item.subclassName == "Polearm" || mh.item.subclassName == "Staff");
|
|
|
|
|
bool isFist = (mh.item.subclassName == "Fist Weapon");
|
|
|
|
|
bool isDagger = (mh.item.subclassName == "Dagger");
|
|
|
|
|
bool hasOffHand = !oh.empty() &&
|
|
|
|
|
(oh.item.inventoryType == game::InvType::ONE_HAND ||
|
|
|
|
|
oh.item.subclassName == "Fist Weapon");
|
|
|
|
|
bool hasShield = !oh.empty() && oh.item.inventoryType == game::InvType::SHIELD;
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedWeaponType(mh.item.inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
}
|
|
|
|
|
// Detect ranged weapon type from RANGED slot
|
|
|
|
|
const auto& rangedSlot = gameHandler.getInventory().getEquipSlot(game::EquipSlot::RANGED);
|
|
|
|
|
if (rangedSlot.empty()) {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::NONE);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
} else if (rangedSlot.item.inventoryType == game::InvType::RANGED_BOW) {
|
|
|
|
|
// subclassName distinguishes Bow vs Crossbow
|
2026-04-05 19:30:44 +03:00
|
|
|
if (rangedSlot.item.subclassName == "Crossbow") {
|
|
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::CROSSBOW);
|
|
|
|
|
} else {
|
|
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::BOW);
|
|
|
|
|
}
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
} else if (rangedSlot.item.inventoryType == game::InvType::RANGED_GUN) {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::GUN);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
} else if (rangedSlot.item.inventoryType == game::InvType::THROWN) {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::THROWN);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
} else {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::NONE);
|
feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.
Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.
Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.
Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.
Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.
Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
|
|
|
}
|
2026-02-06 15:41:29 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Update renderer face-target position and selection circle
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-02 12:24:50 -08:00
|
|
|
if (renderer) {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = renderer->getAnimationController()) ac->setInCombat(gameHandler.isInCombat() &&
|
2026-03-14 08:27:32 -07:00
|
|
|
!gameHandler.isPlayerDead() &&
|
|
|
|
|
!gameHandler.isPlayerGhost());
|
2026-03-14 08:31:08 -07:00
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
uint32_t charInstId = renderer->getCharacterInstanceId();
|
|
|
|
|
if (charInstId != 0) {
|
|
|
|
|
const bool isGhost = gameHandler.isPlayerGhost();
|
|
|
|
|
if (!ghostOpacityStateKnown_ ||
|
|
|
|
|
ghostOpacityLastState_ != isGhost ||
|
|
|
|
|
ghostOpacityLastInstanceId_ != charInstId) {
|
|
|
|
|
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
|
|
|
|
|
ghostOpacityStateKnown_ = true;
|
|
|
|
|
ghostOpacityLastState_ = isGhost;
|
|
|
|
|
ghostOpacityLastInstanceId_ = charInstId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
static glm::vec3 targetGLPos;
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target) {
|
2026-03-10 06:33:44 -07:00
|
|
|
// Prefer the renderer's actual instance position so the selection
|
|
|
|
|
// circle tracks the rendered model (not a parallel entity-space
|
|
|
|
|
// interpolator that can drift from the visual position).
|
|
|
|
|
glm::vec3 instPos;
|
|
|
|
|
if (core::Application::getInstance().getRenderPositionForGuid(target->getGuid(), instPos)) {
|
|
|
|
|
targetGLPos = instPos;
|
|
|
|
|
// Override Z with foot position to sit the circle on the ground.
|
|
|
|
|
float footZ = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
|
|
|
targetGLPos.z = footZ;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: entity game-logic position (no CharacterRenderer instance yet)
|
|
|
|
|
targetGLPos = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(target->getX(), target->getY(), target->getZ()));
|
|
|
|
|
float footZ = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
|
|
|
targetGLPos.z = footZ;
|
|
|
|
|
}
|
2026-02-20 16:02:34 -08:00
|
|
|
}
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = renderer->getAnimationController()) ac->setTargetPosition(&targetGLPos);
|
2026-02-06 13:47:03 -08:00
|
|
|
|
2026-02-06 16:47:07 -08:00
|
|
|
// Selection circle color: WoW-canonical level-based colors
|
2026-02-21 02:52:05 -08:00
|
|
|
bool showSelectionCircle = false;
|
2026-02-06 13:47:03 -08:00
|
|
|
glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow
|
|
|
|
|
float circleRadius = 1.5f;
|
2026-02-06 18:34:45 -08:00
|
|
|
{
|
|
|
|
|
glm::vec3 boundsCenter;
|
|
|
|
|
float boundsRadius = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderBoundsForGuid(target->getGuid(), boundsCenter, boundsRadius)) {
|
|
|
|
|
float r = boundsRadius * 1.1f;
|
|
|
|
|
circleRadius = std::min(std::max(r, 0.8f), 8.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 13:47:03 -08:00
|
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
2026-02-21 02:52:05 -08:00
|
|
|
showSelectionCircle = true;
|
2026-02-06 13:47:03 -08:00
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
|
|
|
|
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
2026-02-06 18:34:45 -08:00
|
|
|
} else if (unit->isHostile() || gameHandler.isAggressiveTowardPlayer(target->getGuid())) {
|
2026-02-06 16:47:07 -08:00
|
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
|
|
|
uint32_t mobLv = unit->getLevel();
|
|
|
|
|
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
|
|
|
|
if (game::GameHandler::killXp(playerLv, mobLv) == 0) {
|
|
|
|
|
circleColor = glm::vec3(0.6f, 0.6f, 0.6f); // grey
|
|
|
|
|
} else if (diff >= 10) {
|
|
|
|
|
circleColor = glm::vec3(1.0f, 0.1f, 0.1f); // red
|
|
|
|
|
} else if (diff >= 5) {
|
|
|
|
|
circleColor = glm::vec3(1.0f, 0.5f, 0.1f); // orange
|
|
|
|
|
} else if (diff >= -2) {
|
|
|
|
|
circleColor = glm::vec3(1.0f, 1.0f, 0.1f); // yellow
|
|
|
|
|
} else {
|
|
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green
|
|
|
|
|
}
|
2026-02-06 14:24:38 -08:00
|
|
|
} else {
|
|
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
2026-02-21 02:52:05 -08:00
|
|
|
showSelectionCircle = true;
|
2026-02-06 13:47:03 -08:00
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
|
|
|
|
}
|
2026-02-21 02:52:05 -08:00
|
|
|
if (showSelectionCircle) {
|
|
|
|
|
renderer->setSelectionCircle(targetGLPos, circleRadius, circleColor);
|
|
|
|
|
} else {
|
|
|
|
|
renderer->clearSelectionCircle();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = renderer->getAnimationController()) ac->setTargetPosition(nullptr);
|
2026-02-06 13:47:03 -08:00
|
|
|
renderer->clearSelectionCircle();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-04-05 19:30:44 +03:00
|
|
|
if (auto* ac = renderer->getAnimationController()) ac->setTargetPosition(nullptr);
|
2026-02-06 13:47:03 -08:00
|
|
|
renderer->clearSelectionCircle();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 20:19:39 -08:00
|
|
|
|
2026-03-11 22:57:04 -07:00
|
|
|
// Screen edge damage flash — red vignette that fires on HP decrease
|
|
|
|
|
{
|
|
|
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
uint32_t currentHp = 0;
|
|
|
|
|
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER ||
|
|
|
|
|
playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
|
|
|
if (unit->getMaxHealth() > 0)
|
|
|
|
|
currentHp = unit->getHealth();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect HP drop (ignore transitions from 0 — entity just spawned or uninitialized)
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.damageFlashEnabled_ && lastPlayerHp_ > 0 && currentHp < lastPlayerHp_ && currentHp > 0)
|
2026-03-11 22:57:04 -07:00
|
|
|
damageFlashAlpha_ = 1.0f;
|
|
|
|
|
lastPlayerHp_ = currentHp;
|
|
|
|
|
|
|
|
|
|
// Fade out over ~0.5 seconds
|
|
|
|
|
if (damageFlashAlpha_ > 0.0f) {
|
|
|
|
|
damageFlashAlpha_ -= ImGui::GetIO().DeltaTime * 2.0f;
|
|
|
|
|
if (damageFlashAlpha_ < 0.0f) damageFlashAlpha_ = 0.0f;
|
2026-04-06 22:43:13 +03:00
|
|
|
drawScreenEdgeVignette(200, 0, 0,
|
|
|
|
|
static_cast<int>(damageFlashAlpha_ * 100.0f), 0.12f);
|
2026-03-11 22:57:04 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:07:46 -07:00
|
|
|
// Persistent low-health vignette — pulsing red edges when HP < 20%
|
|
|
|
|
{
|
|
|
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
bool isDead = gameHandler.isPlayerDead();
|
|
|
|
|
float hpPct = 1.0f;
|
|
|
|
|
if (!isDead && playerEntity &&
|
|
|
|
|
(playerEntity->getType() == game::ObjectType::PLAYER ||
|
|
|
|
|
playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
|
|
|
if (unit->getMaxHealth() > 0)
|
|
|
|
|
hpPct = static_cast<float>(unit->getHealth()) / static_cast<float>(unit->getMaxHealth());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only show when alive and below 20% HP; intensity increases as HP drops
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.lowHealthVignetteEnabled_ && !isDead && hpPct < 0.20f && hpPct > 0.0f) {
|
2026-03-12 07:07:46 -07:00
|
|
|
// Base intensity from HP deficit (0 at 20%, 1 at 0%); pulse at ~1.5 Hz
|
|
|
|
|
float danger = (0.20f - hpPct) / 0.20f;
|
|
|
|
|
float pulse = 0.55f + 0.45f * std::sin(static_cast<float>(ImGui::GetTime()) * 9.4f);
|
|
|
|
|
int alpha = static_cast<int>(danger * pulse * 90.0f); // max ~90 alpha, subtle
|
2026-04-06 22:43:13 +03:00
|
|
|
drawScreenEdgeVignette(200, 0, 0, alpha, 0.15f);
|
2026-03-12 07:07:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 23:10:21 -07:00
|
|
|
// Level-up golden burst overlay
|
2026-03-31 09:18:17 +03:00
|
|
|
if (toastManager_.levelUpFlashAlpha > 0.0f) {
|
|
|
|
|
toastManager_.levelUpFlashAlpha -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second
|
|
|
|
|
if (toastManager_.levelUpFlashAlpha < 0.0f) toastManager_.levelUpFlashAlpha = 0.0f;
|
2026-03-11 23:10:21 -07:00
|
|
|
|
2026-03-31 09:18:17 +03:00
|
|
|
const int alpha = static_cast<int>(toastManager_.levelUpFlashAlpha * 160.0f);
|
2026-04-06 22:43:13 +03:00
|
|
|
drawScreenEdgeVignette(255, 210, 50, alpha, 0.18f);
|
2026-03-11 23:10:21 -07:00
|
|
|
|
|
|
|
|
// "Level X!" text in the center during the first half of the animation
|
2026-03-31 09:18:17 +03:00
|
|
|
if (toastManager_.levelUpFlashAlpha > 0.5f && toastManager_.levelUpDisplayLevel > 0) {
|
2026-04-06 22:43:13 +03:00
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
const float W = ImGui::GetIO().DisplaySize.x;
|
|
|
|
|
const float H = ImGui::GetIO().DisplaySize.y;
|
2026-03-11 23:10:21 -07:00
|
|
|
char lvlText[32];
|
2026-03-31 09:18:17 +03:00
|
|
|
snprintf(lvlText, sizeof(lvlText), "Level %u!", toastManager_.levelUpDisplayLevel);
|
2026-03-11 23:10:21 -07:00
|
|
|
ImVec2 ts = ImGui::CalcTextSize(lvlText);
|
|
|
|
|
float tx = (W - ts.x) * 0.5f;
|
|
|
|
|
float ty = H * 0.35f;
|
|
|
|
|
// Large shadow + bright gold text
|
|
|
|
|
fg->AddText(nullptr, 28.0f, ImVec2(tx + 2, ty + 2), IM_COL32(0, 0, 0, alpha), lvlText);
|
|
|
|
|
fg->AddText(nullptr, 28.0f, ImVec2(tx, ty), IM_COL32(255, 230, 80, alpha), lvlText);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:19:39 -08:00
|
|
|
// Restore previous alpha
|
|
|
|
|
ImGui::GetStyle().Alpha = prevAlpha;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPlayerInfo(game::GameHandler& gameHandler) {
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 250), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10, 30), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::Begin("Player Info", &showPlayerInfo);
|
|
|
|
|
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Position & Movement");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Position
|
|
|
|
|
ImGui::Text("Position:");
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
ImGui::Text("X: %.2f", movement.x);
|
|
|
|
|
ImGui::Text("Y: %.2f", movement.y);
|
|
|
|
|
ImGui::Text("Z: %.2f", movement.z);
|
|
|
|
|
ImGui::Text("Orientation: %.2f rad (%.1f deg)", movement.orientation, movement.orientation * 180.0f / 3.14159f);
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Movement flags
|
|
|
|
|
ImGui::Text("Movement Flags: 0x%08X", movement.flags);
|
|
|
|
|
ImGui::Text("Time: %u ms", movement.time);
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Connection state
|
|
|
|
|
ImGui::Text("Connection State:");
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
auto state = gameHandler.getState();
|
|
|
|
|
switch (state) {
|
|
|
|
|
case game::WorldState::IN_WORLD:
|
2026-03-25 12:12:03 -07:00
|
|
|
ImGui::TextColored(kColorBrightGreen, "In World");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::WorldState::AUTHENTICATED:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorYellow, "Authenticated");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::WorldState::ENTERING_WORLD:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorYellow, "Entering World...");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
default:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorRed, "State: %d", static_cast<int>(state));
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderEntityList(game::GameHandler& gameHandler) {
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10, 290), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::Begin("Entities", &showEntityWindow);
|
|
|
|
|
|
|
|
|
|
const auto& entityManager = gameHandler.getEntityManager();
|
|
|
|
|
const auto& entities = entityManager.getEntities();
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Entities in View: %zu", entities.size());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
if (entities.empty()) {
|
|
|
|
|
ImGui::TextDisabled("No entities in view");
|
|
|
|
|
} else {
|
|
|
|
|
// Entity table
|
|
|
|
|
if (ImGui::BeginTable("EntitiesTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
ImGui::TableSetupColumn("GUID", ImGuiTableColumnFlags_WidthFixed, 140.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
ImGui::TableSetupColumn("Position", ImGuiTableColumnFlags_WidthFixed, 150.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Distance", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
const auto& playerMovement = gameHandler.getMovementInfo();
|
|
|
|
|
float playerX = playerMovement.x;
|
|
|
|
|
float playerY = playerMovement.y;
|
|
|
|
|
float playerZ = playerMovement.z;
|
|
|
|
|
|
|
|
|
|
for (const auto& [guid, entity] : entities) {
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
// GUID
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-04 11:31:08 -08:00
|
|
|
char guidStr[24];
|
|
|
|
|
snprintf(guidStr, sizeof(guidStr), "0x%016llX", (unsigned long long)guid);
|
|
|
|
|
ImGui::Text("%s", guidStr);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Type
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
switch (entity->getType()) {
|
|
|
|
|
case game::ObjectType::PLAYER:
|
2026-03-25 12:12:03 -07:00
|
|
|
ImGui::TextColored(kColorBrightGreen, "Player");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::ObjectType::UNIT:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorYellow, "Unit");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::ObjectType::GAMEOBJECT:
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 1.0f, 1.0f), "GameObject");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
ImGui::Text("Object");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Name (for players and units)
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
if (entity->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
auto player = std::static_pointer_cast<game::Player>(entity);
|
|
|
|
|
ImGui::Text("%s", player->getName().c_str());
|
|
|
|
|
} else if (entity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (!unit->getName().empty()) {
|
|
|
|
|
ImGui::Text("%s", unit->getName().c_str());
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Position
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
ImGui::Text("%.1f, %.1f, %.1f", entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
|
|
|
|
|
// Distance from player
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
float dx = entity->getX() - playerX;
|
|
|
|
|
float dy = entity->getY() - playerY;
|
|
|
|
|
float dz = entity->getZ() - playerZ;
|
|
|
|
|
float distance = std::sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
|
ImGui::Text("%.1f", distance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|
|
|
|
auto& io = ImGui::GetIO();
|
|
|
|
|
auto& input = core::Input::getInstance();
|
|
|
|
|
|
|
|
|
|
// If the user is typing (or about to focus chat this frame), do not allow
|
|
|
|
|
// A-Z or 1-0 shortcuts to fire.
|
|
|
|
|
if (!io.WantTextInput && !chatPanel_.isChatInputActive() && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
|
|
|
|
chatPanel_.activateSlashInput();
|
2026-02-06 18:34:45 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (!io.WantTextInput && !chatPanel_.isChatInputActive() &&
|
refactor(chat): decompose into modular architecture, add GM commands, fix protocol
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/
(ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser,
ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager,
ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter)
- Split 2700-line chat_panel_commands.cpp into 11 command modules
- Add GM command handling: 190-command data table, dot-prefix interception,
tab-completion, /gmhelp with category filter
- Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00);
values 0x00-0x1B shared across Vanilla/TBC/WotLK
- Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26
- Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat,
add 2-frame input cooldown)
- Add tests: chat_markup_parser, chat_tab_completer, gm_commands,
macro_evaluator
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 14:59:56 +03:00
|
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, false)) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.activateInput();
|
2026-02-07 21:12:54 -08:00
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
const bool textFocus = chatPanel_.isChatInputActive() || io.WantTextInput;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
refactor(chat): decompose into modular architecture, add GM commands, fix protocol
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/
(ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser,
ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager,
ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter)
- Split 2700-line chat_panel_commands.cpp into 11 command modules
- Add GM command handling: 190-command data table, dot-prefix interception,
tab-completion, /gmhelp with category filter
- Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00);
values 0x00-0x1B shared across Vanilla/TBC/WotLK
- Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26
- Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat,
add 2-frame input cooldown)
- Add tests: chat_markup_parser, chat_tab_completer, gm_commands,
macro_evaluator
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 14:59:56 +03:00
|
|
|
// Game hotkeys — gate on textFocus (chat/text-input active) rather than
|
|
|
|
|
// WantCaptureKeyboard so that toggle keys like M, C, I still work when an
|
|
|
|
|
// ImGui window (character panel, map, etc.) happens to have focus.
|
|
|
|
|
{
|
2026-03-31 08:53:14 +03:00
|
|
|
if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
2026-02-14 14:30:09 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) {
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.showSettingsWindow) {
|
|
|
|
|
settingsPanel_.showSettingsWindow = false;
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (windowManager_.showEscapeMenu) {
|
|
|
|
|
windowManager_.showEscapeMenu = false;
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.showEscapeSettingsNotice = false;
|
2026-03-31 08:53:14 +03:00
|
|
|
} else if (gameHandler.isCasting()) {
|
|
|
|
|
gameHandler.cancelCast();
|
|
|
|
|
} else if (gameHandler.isLootWindowOpen()) {
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
} else if (gameHandler.isGossipWindowOpen()) {
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
} else if (gameHandler.isVendorWindowOpen()) {
|
|
|
|
|
gameHandler.closeVendor();
|
|
|
|
|
} else if (gameHandler.isBarberShopOpen()) {
|
|
|
|
|
gameHandler.closeBarberShop();
|
|
|
|
|
} else if (gameHandler.isBankOpen()) {
|
|
|
|
|
gameHandler.closeBank();
|
|
|
|
|
} else if (gameHandler.isTrainerWindowOpen()) {
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
} else if (gameHandler.isMailboxOpen()) {
|
|
|
|
|
gameHandler.closeMailbox();
|
|
|
|
|
} else if (gameHandler.isAuctionHouseOpen()) {
|
|
|
|
|
gameHandler.closeAuctionHouse();
|
|
|
|
|
} else if (gameHandler.isQuestDetailsOpen()) {
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
} else if (gameHandler.isQuestOfferRewardOpen()) {
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
} else if (gameHandler.isQuestRequestItemsOpen()) {
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
} else if (gameHandler.isTradeOpen()) {
|
|
|
|
|
gameHandler.cancelTrade();
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (socialPanel_.showWhoWindow_) {
|
|
|
|
|
socialPanel_.showWhoWindow_ = false;
|
|
|
|
|
} else if (combatUI_.showCombatLog_) {
|
|
|
|
|
combatUI_.showCombatLog_ = false;
|
|
|
|
|
} else if (socialPanel_.showSocialFrame_) {
|
|
|
|
|
socialPanel_.showSocialFrame_ = false;
|
2026-03-31 08:53:14 +03:00
|
|
|
} else if (talentScreen.isOpen()) {
|
|
|
|
|
talentScreen.setOpen(false);
|
|
|
|
|
} else if (spellbookScreen.isOpen()) {
|
|
|
|
|
spellbookScreen.setOpen(false);
|
|
|
|
|
} else if (questLogScreen.isOpen()) {
|
|
|
|
|
questLogScreen.setOpen(false);
|
|
|
|
|
} else if (inventoryScreen.isCharacterOpen()) {
|
|
|
|
|
inventoryScreen.toggleCharacter();
|
|
|
|
|
} else if (inventoryScreen.isOpen()) {
|
|
|
|
|
inventoryScreen.setOpen(false);
|
|
|
|
|
} else if (showWorldMap_) {
|
|
|
|
|
showWorldMap_ = false;
|
|
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showEscapeMenu = true;
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (!textFocus) {
|
|
|
|
|
// Toggle character screen (C) and inventory/bags (I)
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
|
|
|
|
const bool wasOpen = inventoryScreen.isCharacterOpen();
|
|
|
|
|
inventoryScreen.toggleCharacter();
|
|
|
|
|
if (!wasOpen && gameHandler.isConnected()) {
|
|
|
|
|
gameHandler.requestPlayedTime();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
|
|
|
|
inventoryScreen.toggle();
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
|
|
|
|
if (ImGui::GetIO().KeyShift)
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.showFriendlyNameplates_ = !settingsPanel_.showFriendlyNameplates_;
|
2026-02-14 15:58:54 -08:00
|
|
|
else
|
2026-03-31 08:53:14 +03:00
|
|
|
showNameplates_ = !showNameplates_;
|
2026-02-14 15:58:54 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
|
|
|
|
showWorldMap_ = !showWorldMap_;
|
2026-02-18 03:46:03 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
|
|
|
|
showMinimap_ = !showMinimap_;
|
2026-03-17 14:42:00 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.showRaidFrames_ = !socialPanel_.showRaidFrames_;
|
2026-03-17 16:42:19 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showAchievementWindow_ = !windowManager_.showAchievementWindow_;
|
2026-03-17 16:42:19 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SKILLS)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showSkillsWindow_ = !windowManager_.showSkillsWindow_;
|
2026-03-17 16:43:57 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
|
refactor(chat): decompose into modular architecture, add GM commands, fix protocol
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/
(ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser,
ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager,
ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter)
- Split 2700-line chat_panel_commands.cpp into 11 command modules
- Add GM command handling: 190-command data table, dot-prefix interception,
tab-completion, /gmhelp with category filter
- Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00);
values 0x00-0x1B shared across Vanilla/TBC/WotLK
- Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26
- Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat,
add 2-frame input cooldown)
- Add tests: chat_markup_parser, chat_tab_completer, gm_commands,
macro_evaluator
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 14:59:56 +03:00
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_H)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showTitlesWindow_ = !windowManager_.showTitlesWindow_;
|
2026-03-17 16:47:33 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Screenshot (PrintScreen key)
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) {
|
|
|
|
|
takeScreenshot(gameHandler);
|
2026-03-17 16:47:33 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Action bar keys (1-9, 0, -, =)
|
|
|
|
|
static const SDL_Scancode actionBarKeys[] = {
|
|
|
|
|
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
|
|
|
|
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
|
|
|
|
|
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
|
2026-03-17 16:47:33 -07:00
|
|
|
};
|
2026-03-31 08:53:14 +03:00
|
|
|
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
|
|
|
|
const bool ctrlDown = input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL);
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
2026-02-19 01:50:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Ctrl+1..Ctrl+8 → switch stance/form/presence (WoW default bindings).
|
|
|
|
|
// Only fires for classes that use a stance bar; same slot ordering as
|
|
|
|
|
// renderStanceBar: Warrior, DK, Druid, Rogue, Priest.
|
|
|
|
|
if (ctrlDown) {
|
2026-04-06 22:43:13 +03:00
|
|
|
static constexpr uint32_t warriorStances[] = { 2457, 71, 2458 };
|
|
|
|
|
static constexpr uint32_t dkPresences[] = { 48266, 48263, 48265 };
|
|
|
|
|
static constexpr uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 };
|
|
|
|
|
static constexpr uint32_t rogueForms[] = { 1784 };
|
|
|
|
|
static constexpr uint32_t priestForms[] = { 15473 };
|
2026-03-31 08:53:14 +03:00
|
|
|
const uint32_t* stArr = nullptr; int stCnt = 0;
|
|
|
|
|
switch (gameHandler.getPlayerClass()) {
|
|
|
|
|
case 1: stArr = warriorStances; stCnt = 3; break;
|
|
|
|
|
case 6: stArr = dkPresences; stCnt = 3; break;
|
|
|
|
|
case 11: stArr = druidForms; stCnt = 9; break;
|
|
|
|
|
case 4: stArr = rogueForms; stCnt = 1; break;
|
|
|
|
|
case 5: stArr = priestForms; stCnt = 1; break;
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (stArr) {
|
|
|
|
|
const auto& known = gameHandler.getKnownSpells();
|
|
|
|
|
// Build available list (same order as UI)
|
|
|
|
|
std::vector<uint32_t> avail;
|
|
|
|
|
avail.reserve(stCnt);
|
|
|
|
|
for (int i = 0; i < stCnt; ++i)
|
|
|
|
|
if (known.count(stArr[i])) avail.push_back(stArr[i]);
|
|
|
|
|
// Ctrl+1 = first stance, Ctrl+2 = second, …
|
|
|
|
|
for (int i = 0; i < static_cast<int>(avail.size()) && i < 8; ++i) {
|
|
|
|
|
if (input.isKeyJustPressed(actionBarKeys[i]))
|
|
|
|
|
gameHandler.castSpell(avail[i]);
|
2026-03-17 14:50:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
|
|
|
if (!ctrlDown && input.isKeyJustPressed(actionBarKeys[i])) {
|
|
|
|
|
int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i;
|
|
|
|
|
if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) {
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
gameHandler.castSpell(bar[slotIdx].id, target);
|
|
|
|
|
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
|
|
|
|
|
gameHandler.useItemById(bar[slotIdx].id);
|
|
|
|
|
} else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) {
|
refactor(chat): decompose into modular architecture, add GM commands, fix protocol
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/
(ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser,
ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager,
ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter)
- Split 2700-line chat_panel_commands.cpp into 11 command modules
- Add GM command handling: 190-command data table, dot-prefix interception,
tab-completion, /gmhelp with category filter
- Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00);
values 0x00-0x1B shared across Vanilla/TBC/WotLK
- Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26
- Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat,
add 2-frame input cooldown)
- Add tests: chat_markup_parser, chat_tab_completer, gm_commands,
macro_evaluator
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 14:59:56 +03:00
|
|
|
chatPanel_.executeMacroText(gameHandler, gameHandler.getMacroText(bar[slotIdx].id));
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-03-11 21:03:51 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Cursor affordance: show hand cursor over interactable entities.
|
|
|
|
|
if (!io.WantCaptureMouse) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 08:53:14 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 08:53:14 +03:00
|
|
|
if (camera && window) {
|
|
|
|
|
glm::vec2 mousePos = input.getMousePosition();
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
|
|
|
|
float closestT = 1e30f;
|
|
|
|
|
bool hoverInteractable = false;
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
bool isGo = (entity->getType() == game::ObjectType::GAMEOBJECT);
|
|
|
|
|
bool isUnit = (entity->getType() == game::ObjectType::UNIT);
|
|
|
|
|
bool isPlayer = (entity->getType() == game::ObjectType::PLAYER);
|
|
|
|
|
if (!isGo && !isUnit && !isPlayer) continue;
|
|
|
|
|
if (guid == gameHandler.getPlayerGuid()) continue; // skip self
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
hitRadius = isGo ? 2.5f : 1.8f;
|
|
|
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
hitCenter.z += isGo ? 1.2f : 1.0f;
|
2026-02-14 16:42:47 -08:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.8f);
|
2026-02-14 15:58:54 -08:00
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
hoverInteractable = true;
|
2026-03-12 06:45:27 -07:00
|
|
|
}
|
2026-03-11 23:49:37 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (hoverInteractable) {
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
2026-03-11 22:00:30 -07:00
|
|
|
}
|
2026-03-11 23:24:27 -07:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Left-click targeting: only on mouse-up if the mouse didn't drag (camera rotate)
|
|
|
|
|
// Record press position on mouse-down
|
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) {
|
|
|
|
|
leftClickPressPos_ = input.getMousePosition();
|
|
|
|
|
leftClickWasPress_ = true;
|
2026-03-11 23:24:27 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// On mouse-up, check if it was a click (not a drag)
|
|
|
|
|
if (leftClickWasPress_ && input.isMouseButtonJustReleased(SDL_BUTTON_LEFT)) {
|
|
|
|
|
leftClickWasPress_ = false;
|
|
|
|
|
glm::vec2 releasePos = input.getMousePosition();
|
|
|
|
|
glm::vec2 dragDelta = releasePos - leftClickPressPos_;
|
|
|
|
|
float dragDistSq = glm::dot(dragDelta, dragDelta);
|
|
|
|
|
constexpr float CLICK_THRESHOLD = 5.0f; // pixels
|
2026-02-07 12:30:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (dragDistSq < CLICK_THRESHOLD * CLICK_THRESHOLD) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 08:53:14 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (camera && window) {
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
2026-03-12 11:16:42 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
rendering::Ray ray = camera->screenToWorldRay(leftClickPressPos_.x, leftClickPressPos_.y, screenW, screenH);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float closestT = 1e30f;
|
|
|
|
|
uint64_t closestGuid = 0;
|
|
|
|
|
float closestHostileUnitT = 1e30f;
|
|
|
|
|
uint64_t closestHostileUnitGuid = 0;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
auto t = entity->getType();
|
|
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT) continue;
|
|
|
|
|
if (guid == myGuid) continue; // Don't target self
|
2026-03-12 06:38:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
// Fallback hitbox based on entity type
|
|
|
|
|
float heightOffset = 1.5f;
|
|
|
|
|
hitRadius = 1.5f;
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
// Critters have very low max health (< 100)
|
|
|
|
|
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
|
|
|
|
hitRadius = 0.5f;
|
|
|
|
|
heightOffset = 0.3f;
|
|
|
|
|
}
|
|
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
heightOffset = 1.2f;
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
hitCenter.z += heightOffset;
|
|
|
|
|
} else {
|
|
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
bool hostileUnit = unit->isHostile() || gameHandler.isAggressiveTowardPlayer(guid);
|
|
|
|
|
if (hostileUnit && hitT < closestHostileUnitT) {
|
|
|
|
|
closestHostileUnitT = hitT;
|
|
|
|
|
closestHostileUnitGuid = guid;
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
closestGuid = guid;
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 06:38:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Prefer hostile monsters over nearby gameobjects/others when both are hittable.
|
|
|
|
|
if (closestHostileUnitGuid != 0) {
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
}
|
2026-03-11 23:06:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
} else {
|
|
|
|
|
// Clicked empty space — deselect current target
|
|
|
|
|
gameHandler.clearTarget();
|
2026-03-11 23:06:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 23:06:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
|
|
|
|
// Suppress when left button is held (both-button run)
|
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
|
|
|
|
// If a gameobject is already targeted, prioritize interacting with that target
|
|
|
|
|
// instead of re-picking under cursor (which can hit nearby decorative GOs).
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target && target->getType() == game::ObjectType::GAMEOBJECT) {
|
2026-04-05 20:12:17 -07:00
|
|
|
LOG_DEBUG("[GO-DIAG] Right-click: re-interacting with targeted GO 0x",
|
2026-03-31 19:51:37 +03:00
|
|
|
std::hex, target->getGuid(), std::dec);
|
2026-03-31 08:53:14 +03:00
|
|
|
gameHandler.setTarget(target->getGuid());
|
|
|
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
|
|
|
|
return;
|
2026-03-11 23:06:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// If no target or right-clicking in world, try to pick one under cursor
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 08:53:14 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 08:53:14 +03:00
|
|
|
if (camera && window) {
|
|
|
|
|
// If a quest objective gameobject is under the cursor, prefer it over
|
|
|
|
|
// hostile units so quest pickups (e.g. "Bundle of Wood") are reliable.
|
|
|
|
|
std::unordered_set<uint32_t> questObjectiveGoEntries;
|
|
|
|
|
{
|
|
|
|
|
const auto& ql = gameHandler.getQuestLog();
|
|
|
|
|
questObjectiveGoEntries.reserve(32);
|
|
|
|
|
for (const auto& q : ql) {
|
|
|
|
|
if (q.complete) continue;
|
|
|
|
|
for (const auto& obj : q.killObjectives) {
|
|
|
|
|
if (obj.npcOrGoId >= 0 || obj.required == 0) continue;
|
|
|
|
|
uint32_t entry = static_cast<uint32_t>(-obj.npcOrGoId);
|
|
|
|
|
uint32_t cur = 0;
|
|
|
|
|
auto it = q.killCounts.find(entry);
|
|
|
|
|
if (it != q.killCounts.end()) cur = it->second.first;
|
|
|
|
|
if (cur < obj.required) questObjectiveGoEntries.insert(entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 13:39:36 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec2 mousePos = input.getMousePosition();
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
|
|
|
|
float closestT = 1e30f;
|
|
|
|
|
uint64_t closestGuid = 0;
|
|
|
|
|
game::ObjectType closestType = game::ObjectType::OBJECT;
|
|
|
|
|
float closestHostileUnitT = 1e30f;
|
|
|
|
|
uint64_t closestHostileUnitGuid = 0;
|
|
|
|
|
float closestQuestGoT = 1e30f;
|
|
|
|
|
uint64_t closestQuestGoGuid = 0;
|
|
|
|
|
float closestGoT = 1e30f;
|
|
|
|
|
uint64_t closestGoGuid = 0;
|
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
auto t = entity->getType();
|
|
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT)
|
|
|
|
|
continue;
|
|
|
|
|
if (guid == myGuid) continue;
|
2026-03-12 13:39:36 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
float heightOffset = 1.5f;
|
|
|
|
|
hitRadius = 1.5f;
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
|
|
|
|
hitRadius = 0.5f;
|
|
|
|
|
heightOffset = 0.3f;
|
|
|
|
|
}
|
|
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
heightOffset = 1.2f;
|
|
|
|
|
}
|
|
|
|
|
hitCenter = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
hitCenter.z += heightOffset;
|
2026-03-31 19:51:37 +03:00
|
|
|
// Log each unique GO's raypick position once
|
|
|
|
|
if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
static std::unordered_set<uint64_t> goPickLog;
|
|
|
|
|
if (goPickLog.insert(guid).second) {
|
|
|
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
2026-04-05 20:12:17 -07:00
|
|
|
LOG_DEBUG("[GO-DIAG] Raypick GO: guid=0x", std::hex, guid, std::dec,
|
2026-03-31 19:51:37 +03:00
|
|
|
" entry=", go->getEntry(), " name='", go->getName(),
|
|
|
|
|
"' pos=(", entity->getX(), ",", entity->getY(), ",", entity->getZ(),
|
|
|
|
|
") center=(", hitCenter.x, ",", hitCenter.y, ",", hitCenter.z,
|
|
|
|
|
") r=", hitRadius);
|
|
|
|
|
}
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
} else {
|
|
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
|
|
|
|
}
|
2026-03-12 13:39:36 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
bool hostileUnit = unit->isHostile() || gameHandler.isAggressiveTowardPlayer(guid);
|
|
|
|
|
if (hostileUnit && hitT < closestHostileUnitT) {
|
|
|
|
|
closestHostileUnitT = hitT;
|
|
|
|
|
closestHostileUnitGuid = guid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
if (hitT < closestGoT) {
|
|
|
|
|
closestGoT = hitT;
|
|
|
|
|
closestGoGuid = guid;
|
|
|
|
|
}
|
|
|
|
|
if (!questObjectiveGoEntries.empty()) {
|
|
|
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
|
|
|
if (questObjectiveGoEntries.count(go->getEntry())) {
|
|
|
|
|
if (hitT < closestQuestGoT) {
|
|
|
|
|
closestQuestGoT = hitT;
|
|
|
|
|
closestQuestGoGuid = guid;
|
2026-03-12 13:39:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
closestGuid = guid;
|
|
|
|
|
closestType = t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 23:51:27 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Priority: quest GO > closer of (GO, hostile unit) > closest anything.
|
|
|
|
|
if (closestQuestGoGuid != 0) {
|
|
|
|
|
closestGuid = closestQuestGoGuid;
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
|
|
|
} else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) {
|
|
|
|
|
// Both a GO and hostile unit were hit — prefer whichever is closer.
|
|
|
|
|
if (closestGoT <= closestHostileUnitT) {
|
|
|
|
|
closestGuid = closestGoGuid;
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
|
|
|
} else {
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
closestType = game::ObjectType::UNIT;
|
|
|
|
|
}
|
|
|
|
|
} else if (closestGoGuid != 0) {
|
|
|
|
|
closestGuid = closestGoGuid;
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
|
|
|
} else if (closestHostileUnitGuid != 0) {
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
closestType = game::ObjectType::UNIT;
|
2026-03-18 11:48:22 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
if (closestType == game::ObjectType::GAMEOBJECT) {
|
2026-04-05 20:12:17 -07:00
|
|
|
LOG_DEBUG("[GO-DIAG] Right-click: raypick hit GO 0x",
|
2026-03-31 19:51:37 +03:00
|
|
|
std::hex, closestGuid, std::dec);
|
2026-03-31 08:53:14 +03:00
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
gameHandler.interactWithGameObject(closestGuid);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
2026-03-18 11:48:22 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target) {
|
|
|
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
// Check if unit is dead (health == 0) → loot, otherwise interact/attack
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
|
|
|
|
gameHandler.lootTarget(target->getGuid());
|
|
|
|
|
} else {
|
|
|
|
|
// Interact with service NPCs; otherwise treat non-interactable living units
|
|
|
|
|
// as attackable fallback (covers bad faction-template classification).
|
|
|
|
|
auto isSpiritNpc = [&]() -> bool {
|
|
|
|
|
constexpr uint32_t NPC_FLAG_SPIRIT_GUIDE = 0x00004000;
|
|
|
|
|
constexpr uint32_t NPC_FLAG_SPIRIT_HEALER = 0x00008000;
|
|
|
|
|
if (unit->getNpcFlags() & (NPC_FLAG_SPIRIT_GUIDE | NPC_FLAG_SPIRIT_HEALER)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
std::string name = unit->getName();
|
|
|
|
|
std::transform(name.begin(), name.end(), name.begin(),
|
|
|
|
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
return (name.find("spirit healer") != std::string::npos) ||
|
|
|
|
|
(name.find("spirit guide") != std::string::npos);
|
|
|
|
|
};
|
|
|
|
|
bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc();
|
|
|
|
|
bool canInteractNpc = unit->isInteractable() || allowSpiritInteract;
|
|
|
|
|
bool shouldAttackByFallback = !canInteractNpc;
|
|
|
|
|
if (!unit->isHostile() && canInteractNpc) {
|
|
|
|
|
gameHandler.interactWithNpc(target->getGuid());
|
|
|
|
|
} else if (unit->isHostile() || shouldAttackByFallback) {
|
|
|
|
|
gameHandler.startAutoAttack(target->getGuid());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (target->getType() == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
|
|
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
// Right-click another player could start attack in PvP context
|
|
|
|
|
}
|
2026-03-12 14:43:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 14:43:58 -07:00
|
|
|
|
2026-03-17 20:46:41 -07:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
}} // namespace wowee::ui
|