2026-02-02 12:24:50 -08:00
|
|
|
#include "ui/game_screen.hpp"
|
2026-02-06 14:24:38 -08:00
|
|
|
#include "rendering/character_preview.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/application.hpp"
|
2026-02-04 17:37:28 -08:00
|
|
|
#include "core/coordinates.hpp"
|
2026-02-04 18:27:52 -08:00
|
|
|
#include "core/spawn_presets.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/input.hpp"
|
|
|
|
|
#include "rendering/renderer.hpp"
|
2026-02-04 22:27:45 -08:00
|
|
|
#include "rendering/minimap.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"
|
2026-02-05 16:17:04 -08:00
|
|
|
#include "audio/music_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-06 14:30:54 -08:00
|
|
|
#include "pipeline/blp_loader.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <imgui.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>
|
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-02 12:24:50 -08:00
|
|
|
#include <unordered_set>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::render(game::GameHandler& gameHandler) {
|
2026-02-06 20:19:39 -08:00
|
|
|
// Apply UI transparency setting
|
|
|
|
|
float prevAlpha = ImGui::GetStyle().Alpha;
|
|
|
|
|
ImGui::GetStyle().Alpha = uiOpacity_;
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
// Apply initial minimap settings when renderer becomes available
|
|
|
|
|
if (!minimapSettingsApplied_) {
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->setRotateWithCamera(minimapRotate_);
|
|
|
|
|
minimap->setSquareShape(minimapSquare_);
|
|
|
|
|
minimapSettingsApplied_ = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Process targeting input before UI windows
|
|
|
|
|
processTargetInput(gameHandler);
|
|
|
|
|
|
|
|
|
|
// Player unit frame (top-left)
|
|
|
|
|
renderPlayerFrame(gameHandler);
|
|
|
|
|
|
|
|
|
|
// Target frame (only when we have a target)
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
renderTargetFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render windows
|
|
|
|
|
if (showPlayerInfo) {
|
|
|
|
|
renderPlayerInfo(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showEntityWindow) {
|
|
|
|
|
renderEntityList(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showChatWindow) {
|
|
|
|
|
renderChatWindow(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
|
|
|
// ---- New UI elements ----
|
|
|
|
|
renderActionBar(gameHandler);
|
2026-02-05 12:07:58 -08:00
|
|
|
renderXpBar(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
|
|
|
renderCastBar(gameHandler);
|
|
|
|
|
renderCombatText(gameHandler);
|
|
|
|
|
renderPartyFrames(gameHandler);
|
|
|
|
|
renderGroupInvitePopup(gameHandler);
|
|
|
|
|
renderBuffBar(gameHandler);
|
|
|
|
|
renderLootWindow(gameHandler);
|
|
|
|
|
renderGossipWindow(gameHandler);
|
2026-02-06 11:59:51 -08:00
|
|
|
renderQuestDetailsWindow(gameHandler);
|
2026-02-06 21:50:15 -08:00
|
|
|
renderQuestRequestItemsWindow(gameHandler);
|
|
|
|
|
renderQuestOfferRewardWindow(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
|
|
|
renderVendorWindow(gameHandler);
|
2026-02-08 14:33:39 -08:00
|
|
|
renderTrainerWindow(gameHandler);
|
2026-02-07 16:59:20 -08:00
|
|
|
renderTaxiWindow(gameHandler);
|
2026-02-09 23:41:38 -08:00
|
|
|
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
2026-02-06 20:10:10 -08:00
|
|
|
renderMinimapMarkers(gameHandler);
|
2026-02-06 17:27:20 -08:00
|
|
|
renderDeathScreen(gameHandler);
|
2026-02-07 23:12:24 -08:00
|
|
|
renderResurrectDialog(gameHandler);
|
2026-02-05 16:01:38 -08:00
|
|
|
renderEscapeMenu();
|
2026-02-05 16:11:00 -08:00
|
|
|
renderSettingsWindow();
|
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
|
|
|
// World map (M key toggle handled inside)
|
|
|
|
|
renderWorldMap(gameHandler);
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Quest Log (L key toggle handled inside)
|
|
|
|
|
questLogScreen.render(gameHandler);
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
// Spellbook (P key toggle handled inside)
|
|
|
|
|
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
|
|
|
|
|
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_) {
|
2026-02-06 14:24:38 -08:00
|
|
|
auto* am = core::Application::getInstance().getAssetManager();
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Auto-open bags when vendor window opens
|
|
|
|
|
if (gameHandler.isVendorWindowOpen() && !inventoryScreen.isOpen()) {
|
|
|
|
|
inventoryScreen.setOpen(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bags (B key toggle handled inside)
|
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-02-06 13:47:03 -08:00
|
|
|
// Character screen (C key toggle handled inside render())
|
2026-02-06 14:24:38 -08:00
|
|
|
inventoryScreen.renderCharacterScreen(gameHandler);
|
2026-02-06 13:47:03 -08: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());
|
|
|
|
|
core::Application::getInstance().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
|
|
|
|
|
auto* r = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (r) {
|
|
|
|
|
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
|
|
|
r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Update renderer face-target position and selection circle
|
2026-02-02 12:24:50 -08:00
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (renderer) {
|
2026-02-07 13:56:58 -08:00
|
|
|
renderer->setInCombat(gameHandler.isAutoAttacking());
|
2026-02-02 12:24:50 -08:00
|
|
|
static glm::vec3 targetGLPos;
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target) {
|
2026-02-04 17:37:28 -08:00
|
|
|
targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
2026-02-02 12:24:50 -08:00
|
|
|
renderer->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-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) {
|
|
|
|
|
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) {
|
|
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
|
|
|
|
}
|
|
|
|
|
renderer->setSelectionCircle(targetGLPos, circleRadius, circleColor);
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
|
|
|
|
renderer->setTargetPosition(nullptr);
|
2026-02-06 13:47:03 -08:00
|
|
|
renderer->clearSelectionCircle();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
renderer->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
|
|
|
|
|
|
|
|
// 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:
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "In World");
|
|
|
|
|
break;
|
|
|
|
|
case game::WorldState::AUTHENTICATED:
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Authenticated");
|
|
|
|
|
break;
|
|
|
|
|
case game::WorldState::ENTERING_WORLD:
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Entering World...");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "State: %d", static_cast<int>(state));
|
|
|
|
|
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:
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Player");
|
|
|
|
|
break;
|
|
|
|
|
case game::ObjectType::UNIT:
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Unit");
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
2026-02-04 11:31:08 -08:00
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
float chatW = std::min(500.0f, screenW * 0.4f);
|
|
|
|
|
float chatH = 220.0f;
|
|
|
|
|
float chatX = 8.0f;
|
|
|
|
|
float chatY = screenH - chatH - 80.0f; // Above action bar
|
2026-02-06 18:34:45 -08:00
|
|
|
if (!chatWindowPosInit_) {
|
|
|
|
|
chatWindowPos_ = ImVec2(chatX, chatY);
|
|
|
|
|
chatWindowPosInit_ = true;
|
|
|
|
|
}
|
|
|
|
|
if (chatWindowLocked) {
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_Always);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_FirstUseEver);
|
|
|
|
|
}
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
2026-02-07 21:12:54 -08:00
|
|
|
if (chatWindowLocked) {
|
|
|
|
|
flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
ImGui::Begin("Chat", nullptr, flags);
|
|
|
|
|
|
|
|
|
|
if (!chatWindowLocked) {
|
|
|
|
|
chatWindowPos_ = ImGui::GetWindowPos();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Chat history
|
|
|
|
|
const auto& chatHistory = gameHandler.getChatHistory();
|
|
|
|
|
|
|
|
|
|
ImGui::BeginChild("ChatHistory", ImVec2(0, -70), true, ImGuiWindowFlags_HorizontalScrollbar);
|
2026-02-07 21:12:54 -08:00
|
|
|
bool chatHistoryHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
// Helper: render text with clickable URLs
|
|
|
|
|
auto renderTextWithLinks = [](const std::string& text, const ImVec4& color) {
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while (pos < text.size()) {
|
|
|
|
|
// Find next URL (http:// or https://)
|
|
|
|
|
size_t urlStart = std::string::npos;
|
|
|
|
|
size_t httpPos = text.find("http://", pos);
|
|
|
|
|
size_t httpsPos = text.find("https://", pos);
|
|
|
|
|
if (httpPos != std::string::npos && (httpsPos == std::string::npos || httpPos < httpsPos))
|
|
|
|
|
urlStart = httpPos;
|
|
|
|
|
else if (httpsPos != std::string::npos)
|
|
|
|
|
urlStart = httpsPos;
|
|
|
|
|
|
|
|
|
|
if (urlStart == std::string::npos) {
|
|
|
|
|
// No more URLs, render remaining text
|
|
|
|
|
std::string remaining = text.substr(pos);
|
|
|
|
|
if (!remaining.empty()) {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
ImGui::TextWrapped("%s", remaining.c_str());
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render text before URL
|
|
|
|
|
if (urlStart > pos) {
|
|
|
|
|
std::string before = text.substr(pos, urlStart - pos);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
ImGui::TextWrapped("%s", before.c_str());
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find end of URL (space, newline, or end of string)
|
|
|
|
|
size_t urlEnd = text.find_first_of(" \t\n\r", urlStart);
|
|
|
|
|
if (urlEnd == std::string::npos) urlEnd = text.size();
|
|
|
|
|
std::string url = text.substr(urlStart, urlEnd - urlStart);
|
|
|
|
|
|
|
|
|
|
// Render URL as clickable link
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.7f, 1.0f, 1.0f));
|
|
|
|
|
ImGui::TextWrapped("%s", url.c_str());
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
ImGui::SetTooltip("Open: %s", url.c_str());
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
|
|
|
std::string cmd = "xdg-open '" + url + "' &";
|
|
|
|
|
system(cmd.c_str());
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
pos = urlEnd;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
for (const auto& msg : chatHistory) {
|
|
|
|
|
ImVec4 color = getChatTypeColor(msg.type);
|
|
|
|
|
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
if (msg.type == game::ChatType::SYSTEM) {
|
2026-02-07 23:32:27 -08:00
|
|
|
renderTextWithLinks(msg.message, color);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
} else if (msg.type == game::ChatType::TEXT_EMOTE) {
|
2026-02-07 23:32:27 -08:00
|
|
|
std::string full = "You " + msg.message;
|
|
|
|
|
renderTextWithLinks(full, color);
|
2026-02-04 11:31:08 -08:00
|
|
|
} else if (!msg.senderName.empty()) {
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
if (msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_YELL) {
|
2026-02-07 23:32:27 -08:00
|
|
|
std::string prefix = msg.senderName + " says: ";
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
ImGui::TextWrapped("%s", prefix.c_str());
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::SameLine(0, 0);
|
|
|
|
|
renderTextWithLinks(msg.message, color);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
} else {
|
2026-02-07 23:32:27 -08:00
|
|
|
std::string prefix = "[" + std::string(getChatTypeName(msg.type)) + "] " + msg.senderName + ": ";
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
ImGui::TextWrapped("%s", prefix.c_str());
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::SameLine(0, 0);
|
|
|
|
|
renderTextWithLinks(msg.message, color);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
2026-02-07 23:32:27 -08:00
|
|
|
std::string prefix = "[" + std::string(getChatTypeName(msg.type)) + "] ";
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
ImGui::TextWrapped("%s", prefix.c_str());
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::SameLine(0, 0);
|
|
|
|
|
renderTextWithLinks(msg.message, color);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom
|
|
|
|
|
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
|
|
|
|
ImGui::SetScrollHereY(1.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
2026-02-06 18:34:45 -08:00
|
|
|
// Lock toggle
|
|
|
|
|
ImGui::Checkbox("Lock", &chatWindowLocked);
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextDisabled(chatWindowLocked ? "(locked)" : "(movable)");
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Chat input
|
|
|
|
|
ImGui::Text("Type:");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::SetNextItemWidth(100);
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE" };
|
|
|
|
|
ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 10);
|
2026-02-07 12:30:36 -08:00
|
|
|
|
|
|
|
|
// Auto-fill whisper target when switching to WHISPER mode
|
|
|
|
|
if (selectedChatType == 4 && lastChatType != 4) {
|
|
|
|
|
// Just switched to WHISPER mode
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target && target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
auto player = std::static_pointer_cast<game::Player>(target);
|
|
|
|
|
if (!player->getName().empty()) {
|
|
|
|
|
strncpy(whisperTargetBuffer, player->getName().c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lastChatType = selectedChatType;
|
|
|
|
|
|
|
|
|
|
// Show whisper target field if WHISPER is selected
|
|
|
|
|
if (selectedChatType == 4) {
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::Text("To:");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::SetNextItemWidth(120);
|
|
|
|
|
ImGui::InputText("##WhisperTarget", whisperTargetBuffer, sizeof(whisperTargetBuffer));
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::Text("Message:");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
if (refocusChatInput) {
|
|
|
|
|
ImGui::SetKeyboardFocusHere();
|
|
|
|
|
refocusChatInput = false;
|
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
|
|
|
|
// Detect chat channel prefix as user types and switch the dropdown
|
|
|
|
|
{
|
|
|
|
|
std::string buf(chatInputBuffer);
|
|
|
|
|
if (buf.size() >= 2 && buf[0] == '/') {
|
|
|
|
|
// Find the command and check if there's a space after it
|
|
|
|
|
size_t sp = buf.find(' ', 1);
|
|
|
|
|
if (sp != std::string::npos) {
|
|
|
|
|
std::string cmd = buf.substr(1, sp - 1);
|
|
|
|
|
for (char& c : cmd) c = std::tolower(c);
|
|
|
|
|
int detected = -1;
|
|
|
|
|
if (cmd == "s" || cmd == "say") detected = 0;
|
|
|
|
|
else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1;
|
|
|
|
|
else if (cmd == "p" || cmd == "party") detected = 2;
|
|
|
|
|
else if (cmd == "g" || cmd == "guild") detected = 3;
|
|
|
|
|
else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4;
|
|
|
|
|
else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5;
|
|
|
|
|
else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6;
|
|
|
|
|
else if (cmd == "bg" || cmd == "battleground") detected = 7;
|
|
|
|
|
else if (cmd == "rw" || cmd == "raidwarning") detected = 8;
|
|
|
|
|
else if (cmd == "i" || cmd == "instance") detected = 9;
|
|
|
|
|
if (detected >= 0 && selectedChatType != detected) {
|
|
|
|
|
selectedChatType = detected;
|
|
|
|
|
// Strip the prefix, keep only the message part
|
|
|
|
|
std::string remaining = buf.substr(sp + 1);
|
|
|
|
|
// For whisper, first word after /w is the target
|
|
|
|
|
if (detected == 4) {
|
|
|
|
|
size_t msgStart = remaining.find(' ');
|
|
|
|
|
if (msgStart != std::string::npos) {
|
|
|
|
|
std::string wTarget = remaining.substr(0, msgStart);
|
|
|
|
|
strncpy(whisperTargetBuffer, wTarget.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
remaining = remaining.substr(msgStart + 1);
|
|
|
|
|
} else {
|
|
|
|
|
// Just the target name so far, no message yet
|
|
|
|
|
strncpy(whisperTargetBuffer, remaining.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
remaining = "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
strncpy(chatInputBuffer, remaining.c_str(), sizeof(chatInputBuffer) - 1);
|
|
|
|
|
chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0';
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color the input text based on current chat type
|
|
|
|
|
ImVec4 inputColor;
|
|
|
|
|
switch (selectedChatType) {
|
|
|
|
|
case 1: inputColor = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); break; // YELL - red
|
|
|
|
|
case 2: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // PARTY - blue
|
|
|
|
|
case 3: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // GUILD - green
|
|
|
|
|
case 4: inputColor = ImVec4(1.0f, 0.5f, 1.0f, 1.0f); break; // WHISPER - pink
|
|
|
|
|
case 5: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // RAID - orange
|
|
|
|
|
case 6: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // OFFICER - green
|
|
|
|
|
case 7: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // BG - orange
|
|
|
|
|
case 8: inputColor = ImVec4(1.0f, 0.3f, 0.0f, 1.0f); break; // RAID WARNING - red-orange
|
|
|
|
|
case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue
|
|
|
|
|
default: inputColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // SAY - white
|
|
|
|
|
}
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, inputColor);
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
auto inputCallback = [](ImGuiInputTextCallbackData* data) -> int {
|
|
|
|
|
auto* self = static_cast<GameScreen*>(data->UserData);
|
|
|
|
|
if (self && self->chatInputMoveCursorToEnd) {
|
|
|
|
|
int len = static_cast<int>(std::strlen(data->Buf));
|
|
|
|
|
data->CursorPos = len;
|
|
|
|
|
data->SelectionStart = len;
|
|
|
|
|
data->SelectionEnd = len;
|
|
|
|
|
self->chatInputMoveCursorToEnd = false;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways;
|
|
|
|
|
if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), inputFlags, inputCallback, this)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
sendChatMessage(gameHandler);
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
ImGui::PopStyleColor();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
if (ImGui::IsItemActive()) {
|
|
|
|
|
chatInputActive = true;
|
|
|
|
|
} else {
|
|
|
|
|
chatInputActive = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 21:12:54 -08:00
|
|
|
// Click in chat history area (received messages) → focus input.
|
2026-02-07 21:00:05 -08:00
|
|
|
{
|
2026-02-07 21:12:54 -08:00
|
|
|
if (chatHistoryHovered && ImGui::IsMouseClicked(0)) {
|
|
|
|
|
refocusChatInput = true;
|
2026-02-07 21:00:05 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|
|
|
|
auto& io = ImGui::GetIO();
|
|
|
|
|
auto& input = core::Input::getInstance();
|
|
|
|
|
|
|
|
|
|
// Tab targeting (when keyboard not captured by UI)
|
|
|
|
|
if (!io.WantCaptureKeyboard) {
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
2026-02-09 17:12:35 -08:00
|
|
|
if (showSettingsWindow) {
|
|
|
|
|
// Close settings window if open
|
|
|
|
|
showSettingsWindow = false;
|
|
|
|
|
} else if (showEscapeMenu) {
|
2026-02-05 16:01:38 -08:00
|
|
|
showEscapeMenu = false;
|
|
|
|
|
showEscapeSettingsNotice = false;
|
2026-02-04 18:27:52 -08:00
|
|
|
} else if (gameHandler.isCasting()) {
|
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
|
|
|
gameHandler.cancelCast();
|
|
|
|
|
} else if (gameHandler.isLootWindowOpen()) {
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
} else if (gameHandler.isGossipWindowOpen()) {
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
} else {
|
2026-02-05 16:01:38 -08:00
|
|
|
showEscapeMenu = true;
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
};
|
|
|
|
|
for (int i = 0; i < 12; ++i) {
|
|
|
|
|
if (input.isKeyJustPressed(actionBarKeys[i])) {
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
|
|
|
|
if (bar[i].type == game::ActionBarSlot::SPELL && bar[i].isReady()) {
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
gameHandler.castSpell(bar[i].id, target);
|
2026-02-06 19:17:35 -08:00
|
|
|
} else if (bar[i].type == game::ActionBarSlot::ITEM && bar[i].id != 0) {
|
|
|
|
|
gameHandler.useItemById(bar[i].id);
|
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-02 12:24:50 -08:00
|
|
|
}
|
2026-02-05 16:14:11 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
// Slash key: focus chat input
|
|
|
|
|
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
chatInputBuffer[0] = '/';
|
|
|
|
|
chatInputBuffer[1] = '\0';
|
2026-02-06 18:34:45 -08:00
|
|
|
chatInputMoveCursorToEnd = true;
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enter key: focus chat input (empty)
|
|
|
|
|
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:53:03 -08:00
|
|
|
// Left-click targeting: only on mouse-up if the mouse didn't drag (camera rotate)
|
|
|
|
|
// Record press position on mouse-down
|
2026-02-06 11:59:51 -08:00
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) {
|
2026-02-07 13:53:03 -08:00
|
|
|
leftClickPressPos_ = input.getMousePosition();
|
|
|
|
|
leftClickWasPress_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
float dragDist = glm::length(releasePos - leftClickPressPos_);
|
|
|
|
|
constexpr float CLICK_THRESHOLD = 5.0f; // pixels
|
|
|
|
|
|
|
|
|
|
if (dragDist < CLICK_THRESHOLD) {
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
|
|
|
|
if (camera && window) {
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
|
|
|
|
|
rendering::Ray ray = camera->screenToWorldRay(leftClickPressPos_.x, leftClickPressPos_.y, screenW, screenH);
|
|
|
|
|
|
|
|
|
|
float closestT = 1e30f;
|
|
|
|
|
uint64_t closestGuid = 0;
|
|
|
|
|
|
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
auto t = entity->getType();
|
2026-02-07 19:44:03 -08:00
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT) continue;
|
2026-02-07 13:53:03 -08:00
|
|
|
if (guid == myGuid) continue; // Don't target self
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-02-07 19:44:03 -08:00
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
hitRadius = 1.2f;
|
|
|
|
|
heightOffset = 0.8f;
|
2026-02-06 18:34:45 -08:00
|
|
|
}
|
2026-02-07 13:53:03 -08: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-02-06 13:47:03 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 13:53:03 -08:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
|
|
|
if (hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
closestGuid = guid;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:53:03 -08:00
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
} else {
|
|
|
|
|
// Clicked empty space — deselect current target
|
|
|
|
|
gameHandler.clearTarget();
|
|
|
|
|
}
|
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
|
|
|
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
2026-02-06 11:59:51 -08:00
|
|
|
// Suppress when left button is held (both-button run)
|
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
// If no target or right-clicking in world, try to pick one under cursor
|
|
|
|
|
{
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
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;
|
|
|
|
|
uint64_t closestGuid = 0;
|
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
auto t = entity->getType();
|
2026-02-07 19:44:03 -08:00
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT) continue;
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
if (guid == myGuid) continue;
|
2026-02-06 18:34:45 -08: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;
|
|
|
|
|
}
|
2026-02-07 19:44:03 -08:00
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
hitRadius = 1.2f;
|
|
|
|
|
heightOffset = 0.8f;
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
2026-02-06 18:34:45 -08: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);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
|
|
|
|
float hitT;
|
2026-02-06 18:34:45 -08:00
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
if (hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
closestGuid = guid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
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 {
|
2026-02-07 18:57:27 -08:00
|
|
|
// Interact with friendly NPCs; hostile units just get targeted
|
2026-02-07 21:00:05 -08:00
|
|
|
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);
|
|
|
|
|
};
|
2026-02-07 23:12:24 -08:00
|
|
|
bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc();
|
2026-02-07 21:00:05 -08:00
|
|
|
if (!unit->isHostile() && (unit->isInteractable() || allowSpiritInteract)) {
|
2026-02-06 03:11:43 -08:00
|
|
|
gameHandler.interactWithNpc(target->getGuid());
|
|
|
|
|
}
|
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-07 19:44:03 -08:00
|
|
|
} else if (target->getType() == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
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
|
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
// Right-click another player could start attack in PvP context
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
2026-02-06 18:34:45 -08:00
|
|
|
bool isDead = gameHandler.isPlayerDead();
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(250.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
2026-02-06 18:34:45 -08:00
|
|
|
ImVec4 playerBorder = isDead
|
|
|
|
|
? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
|
|
|
|
|
: (gameHandler.isAutoAttacking()
|
|
|
|
|
? ImVec4(1.0f, 0.2f, 0.2f, 1.0f)
|
|
|
|
|
: ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, playerBorder);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
if (ImGui::Begin("##PlayerFrame", nullptr, flags)) {
|
|
|
|
|
// Use selected character info if available, otherwise defaults
|
|
|
|
|
std::string playerName = "Adventurer";
|
|
|
|
|
uint32_t playerLevel = 1;
|
|
|
|
|
uint32_t playerHp = 100;
|
|
|
|
|
uint32_t playerMaxHp = 100;
|
|
|
|
|
|
|
|
|
|
const auto& characters = gameHandler.getCharacters();
|
2026-02-06 21:25:35 -08:00
|
|
|
uint64_t activeGuid = gameHandler.getActiveCharacterGuid();
|
|
|
|
|
const game::Character* activeChar = nullptr;
|
|
|
|
|
for (const auto& c : characters) {
|
|
|
|
|
if (c.guid == activeGuid) { activeChar = &c; break; }
|
|
|
|
|
}
|
|
|
|
|
if (!activeChar && !characters.empty()) activeChar = &characters[0];
|
|
|
|
|
if (activeChar) {
|
|
|
|
|
const auto& ch = *activeChar;
|
2026-02-02 12:24:50 -08:00
|
|
|
playerName = ch.name;
|
2026-02-06 13:47:03 -08:00
|
|
|
// Use live server level if available, otherwise character struct
|
|
|
|
|
playerLevel = gameHandler.getPlayerLevel();
|
|
|
|
|
if (playerLevel == 0) playerLevel = ch.level;
|
2026-02-02 12:24:50 -08:00
|
|
|
playerMaxHp = 20 + playerLevel * 10;
|
|
|
|
|
playerHp = playerMaxHp;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 17:27:20 -08:00
|
|
|
// Name in green (friendly player color) — clickable for self-target
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
|
|
|
|
if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) {
|
|
|
|
|
gameHandler.setTarget(gameHandler.getPlayerGuid());
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextDisabled("Lv %u", playerLevel);
|
2026-02-06 18:34:45 -08:00
|
|
|
if (isDead) {
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.2f, 0.2f, 1.0f), "DEAD");
|
|
|
|
|
}
|
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
|
|
|
// Try to get real HP/mana from the player entity
|
|
|
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
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) {
|
|
|
|
|
playerHp = unit->getHealth();
|
|
|
|
|
playerMaxHp = unit->getMaxHealth();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Health bar
|
|
|
|
|
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
2026-02-06 18:34:45 -08:00
|
|
|
ImVec4 hpColor = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpColor);
|
2026-02-02 12:24:50 -08:00
|
|
|
char overlay[64];
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp);
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
|
|
|
|
ImGui::PopStyleColor();
|
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
|
|
|
|
|
|
|
|
// Mana/Power bar (Phase 2)
|
|
|
|
|
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
|
|
|
uint32_t power = unit->getPower();
|
|
|
|
|
uint32_t maxPower = unit->getMaxPower();
|
|
|
|
|
if (maxPower > 0) {
|
|
|
|
|
float mpPct = static_cast<float>(power) / static_cast<float>(maxPower);
|
|
|
|
|
// Color by power type
|
|
|
|
|
ImVec4 powerColor;
|
|
|
|
|
switch (unit->getPowerType()) {
|
|
|
|
|
case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue)
|
|
|
|
|
case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red)
|
|
|
|
|
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
|
|
|
|
default: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
|
|
|
|
}
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
|
|
|
|
char mpOverlay[64];
|
|
|
|
|
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", power, maxPower);
|
|
|
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 14), mpOverlay);
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
float frameW = 250.0f;
|
|
|
|
|
float frameX = (screenW - frameW) / 2.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
2026-02-06 16:47:07 -08:00
|
|
|
// Determine hostility/level color for border and name (WoW-canonical)
|
2026-02-06 13:47:03 -08:00
|
|
|
ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
|
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
|
|
|
} else if (target->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto u = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
|
|
|
|
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
2026-02-06 14:24:38 -08:00
|
|
|
} else if (u->isHostile()) {
|
2026-02-06 16:47:07 -08:00
|
|
|
// WoW level-based color for hostile mobs
|
|
|
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
|
|
|
uint32_t mobLv = u->getLevel();
|
|
|
|
|
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
|
|
|
|
if (game::GameHandler::killXp(playerLv, mobLv) == 0) {
|
|
|
|
|
hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP
|
|
|
|
|
} else if (diff >= 10) {
|
|
|
|
|
hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard
|
|
|
|
|
} else if (diff >= 5) {
|
|
|
|
|
hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard
|
|
|
|
|
} else if (diff >= -2) {
|
|
|
|
|
hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even
|
|
|
|
|
} else {
|
|
|
|
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy
|
|
|
|
|
}
|
2026-02-06 14:24:38 -08:00
|
|
|
} else {
|
2026-02-06 16:47:07 -08:00
|
|
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
2026-02-07 21:00:05 -08:00
|
|
|
bool isHostileTarget = gameHandler.isHostileAttacker(target->getGuid());
|
|
|
|
|
if (!isHostileTarget && target->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto u = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
isHostileTarget = u->isHostile();
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
ImVec4 borderColor = ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f);
|
2026-02-07 21:00:05 -08:00
|
|
|
if (isHostileTarget) {
|
|
|
|
|
float t = ImGui::GetTime();
|
|
|
|
|
float pulse = (std::fmod(t, 0.6f) < 0.3f) ? 1.0f : 0.0f;
|
|
|
|
|
borderColor = ImVec4(1.0f, 0.1f, 0.1f, pulse);
|
|
|
|
|
} else if (gameHandler.isAutoAttacking()) {
|
2026-02-06 18:34:45 -08:00
|
|
|
borderColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
|
|
|
|
// Entity name and type
|
|
|
|
|
std::string name = getEntityName(target);
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
ImVec4 nameColor = hostileColor;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
ImGui::TextColored(nameColor, "%s", name.c_str());
|
|
|
|
|
|
2026-02-06 16:47:07 -08:00
|
|
|
// Level (for units/players) — colored by difficulty
|
2026-02-02 12:24:50 -08:00
|
|
|
if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
ImGui::SameLine();
|
2026-02-06 16:47:07 -08:00
|
|
|
// Level color matches the hostility/difficulty color
|
|
|
|
|
ImVec4 levelColor = hostileColor;
|
|
|
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextColored(levelColor, "Lv %u", unit->getLevel());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Health bar
|
|
|
|
|
uint32_t hp = unit->getHealth();
|
|
|
|
|
uint32_t maxHp = unit->getMaxHealth();
|
|
|
|
|
if (maxHp > 0) {
|
|
|
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
|
|
|
pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
char overlay[64];
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
|
|
|
|
ImGui::PopStyleColor();
|
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
|
|
|
// Target mana bar
|
|
|
|
|
uint32_t targetPower = unit->getPower();
|
|
|
|
|
uint32_t targetMaxPower = unit->getMaxPower();
|
|
|
|
|
if (targetMaxPower > 0) {
|
|
|
|
|
float mpPct = static_cast<float>(targetPower) / static_cast<float>(targetMaxPower);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.2f, 0.9f, 1.0f));
|
|
|
|
|
char mpOverlay[64];
|
|
|
|
|
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", targetPower, targetMaxPower);
|
|
|
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 14), mpOverlay);
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
|
|
|
|
ImGui::TextDisabled("No health data");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Distance
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
float dx = target->getX() - movement.x;
|
|
|
|
|
float dy = target->getY() - movement.y;
|
|
|
|
|
float dz = target->getZ() - movement.z;
|
|
|
|
|
float distance = std::sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
|
ImGui::TextDisabled("%.1f yd", distance);
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|
|
|
|
if (strlen(chatInputBuffer) > 0) {
|
|
|
|
|
std::string input(chatInputBuffer);
|
2026-02-07 12:30:36 -08:00
|
|
|
game::ChatType type;
|
|
|
|
|
std::string message = input;
|
|
|
|
|
std::string target;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
// Track if a channel shortcut should change the chat type dropdown
|
|
|
|
|
int switchChatType = -1;
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// Check for slash commands
|
2026-02-02 12:24:50 -08:00
|
|
|
if (input.size() > 1 && input[0] == '/') {
|
|
|
|
|
std::string command = input.substr(1);
|
2026-02-07 12:30:36 -08:00
|
|
|
size_t spacePos = command.find(' ');
|
|
|
|
|
std::string cmd = (spacePos != std::string::npos) ? command.substr(0, spacePos) : command;
|
|
|
|
|
|
|
|
|
|
// Convert command to lowercase for comparison
|
|
|
|
|
std::string cmdLower = cmd;
|
|
|
|
|
for (char& c : cmdLower) c = std::tolower(c);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// Special commands
|
|
|
|
|
if (cmdLower == "logout") {
|
2026-02-05 15:59:06 -08:00
|
|
|
core::Application::getInstance().logoutToLogin();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// /invite command
|
|
|
|
|
if (cmdLower == "invite" && spacePos != std::string::npos) {
|
|
|
|
|
std::string targetName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.inviteToGroup(targetName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 12:37:13 -08:00
|
|
|
// /inspect command
|
|
|
|
|
if (cmdLower == "inspect") {
|
|
|
|
|
gameHandler.inspectTarget();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
// /time command
|
|
|
|
|
if (cmdLower == "time") {
|
|
|
|
|
gameHandler.queryServerTime();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /played command
|
|
|
|
|
if (cmdLower == "played") {
|
|
|
|
|
gameHandler.requestPlayedTime();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /who command
|
|
|
|
|
if (cmdLower == "who") {
|
|
|
|
|
std::string playerName;
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
playerName = command.substr(spacePos + 1);
|
|
|
|
|
}
|
|
|
|
|
gameHandler.queryWho(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
// /roll command
|
|
|
|
|
if (cmdLower == "roll" || cmdLower == "random" || cmdLower == "rnd") {
|
|
|
|
|
uint32_t minRoll = 1;
|
|
|
|
|
uint32_t maxRoll = 100;
|
|
|
|
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string args = command.substr(spacePos + 1);
|
|
|
|
|
size_t dashPos = args.find('-');
|
|
|
|
|
size_t spacePos2 = args.find(' ');
|
|
|
|
|
|
|
|
|
|
if (dashPos != std::string::npos) {
|
|
|
|
|
// Format: /roll 1-100
|
|
|
|
|
try {
|
|
|
|
|
minRoll = std::stoul(args.substr(0, dashPos));
|
|
|
|
|
maxRoll = std::stoul(args.substr(dashPos + 1));
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
} else if (spacePos2 != std::string::npos) {
|
|
|
|
|
// Format: /roll 1 100
|
|
|
|
|
try {
|
|
|
|
|
minRoll = std::stoul(args.substr(0, spacePos2));
|
|
|
|
|
maxRoll = std::stoul(args.substr(spacePos2 + 1));
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
} else {
|
|
|
|
|
// Format: /roll 100 (means 1-100)
|
|
|
|
|
try {
|
|
|
|
|
maxRoll = std::stoul(args);
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gameHandler.randomRoll(minRoll, maxRoll);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /friend or /addfriend command
|
|
|
|
|
if (cmdLower == "friend" || cmdLower == "addfriend") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string args = command.substr(spacePos + 1);
|
|
|
|
|
size_t subCmdSpace = args.find(' ');
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "friend" && subCmdSpace != std::string::npos) {
|
|
|
|
|
std::string subCmd = args.substr(0, subCmdSpace);
|
|
|
|
|
std::transform(subCmd.begin(), subCmd.end(), subCmd.begin(), ::tolower);
|
|
|
|
|
|
|
|
|
|
if (subCmd == "add") {
|
|
|
|
|
std::string playerName = args.substr(subCmdSpace + 1);
|
|
|
|
|
gameHandler.addFriend(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
} else if (subCmd == "remove" || subCmd == "delete" || subCmd == "rem") {
|
|
|
|
|
std::string playerName = args.substr(subCmdSpace + 1);
|
|
|
|
|
gameHandler.removeFriend(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// /addfriend name or /friend name (assume add)
|
|
|
|
|
gameHandler.addFriend(args);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /friend add <name> or /friend remove <name>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /removefriend or /delfriend command
|
|
|
|
|
if (cmdLower == "removefriend" || cmdLower == "delfriend" || cmdLower == "remfriend") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.removeFriend(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /removefriend <name>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:58:11 -08:00
|
|
|
// /ignore command
|
|
|
|
|
if (cmdLower == "ignore") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.addIgnore(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /ignore <name>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /unignore command
|
|
|
|
|
if (cmdLower == "unignore") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.removeIgnore(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /unignore <name>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 17:59:40 -08:00
|
|
|
// /dismount command
|
|
|
|
|
if (cmdLower == "dismount") {
|
|
|
|
|
gameHandler.dismount();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:58:11 -08:00
|
|
|
// /sit command
|
|
|
|
|
if (cmdLower == "sit") {
|
|
|
|
|
gameHandler.setStandState(1); // 1 = sit
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /stand command
|
|
|
|
|
if (cmdLower == "stand") {
|
|
|
|
|
gameHandler.setStandState(0); // 0 = stand
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /kneel command
|
|
|
|
|
if (cmdLower == "kneel") {
|
|
|
|
|
gameHandler.setStandState(8); // 8 = kneel
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /logout command (already exists but using /logout instead of going to login)
|
|
|
|
|
if (cmdLower == "logout" || cmdLower == "camp") {
|
|
|
|
|
gameHandler.requestLogout();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /cancellogout command
|
|
|
|
|
if (cmdLower == "cancellogout") {
|
|
|
|
|
gameHandler.cancelLogout();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:03:21 -08:00
|
|
|
// /helm command
|
|
|
|
|
if (cmdLower == "helm" || cmdLower == "helmet" || cmdLower == "showhelm") {
|
|
|
|
|
gameHandler.toggleHelm();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /cloak command
|
|
|
|
|
if (cmdLower == "cloak" || cmdLower == "showcloak") {
|
|
|
|
|
gameHandler.toggleCloak();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /follow command
|
|
|
|
|
if (cmdLower == "follow" || cmdLower == "f") {
|
|
|
|
|
gameHandler.followTarget();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /assist command
|
|
|
|
|
if (cmdLower == "assist") {
|
|
|
|
|
gameHandler.assistTarget();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
// /pvp command
|
|
|
|
|
if (cmdLower == "pvp") {
|
|
|
|
|
gameHandler.togglePvp();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /ginfo command
|
|
|
|
|
if (cmdLower == "ginfo" || cmdLower == "guildinfo") {
|
|
|
|
|
gameHandler.requestGuildInfo();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /groster command
|
|
|
|
|
if (cmdLower == "groster" || cmdLower == "guildroster") {
|
|
|
|
|
gameHandler.requestGuildRoster();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /gmotd command
|
|
|
|
|
if (cmdLower == "gmotd" || cmdLower == "guildmotd") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string motd = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.setGuildMotd(motd);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /gmotd <message>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /gpromote command
|
|
|
|
|
if (cmdLower == "gpromote" || cmdLower == "guildpromote") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.promoteGuildMember(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /gpromote <player>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /gdemote command
|
|
|
|
|
if (cmdLower == "gdemote" || cmdLower == "guilddemote") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.demoteGuildMember(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /gdemote <player>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /gquit command
|
|
|
|
|
if (cmdLower == "gquit" || cmdLower == "guildquit" || cmdLower == "leaveguild") {
|
|
|
|
|
gameHandler.leaveGuild();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /ginvite command
|
|
|
|
|
if (cmdLower == "ginvite" || cmdLower == "guildinvite") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.inviteToGuild(playerName);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /ginvite <player>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /readycheck command
|
|
|
|
|
if (cmdLower == "readycheck" || cmdLower == "rc") {
|
|
|
|
|
gameHandler.initiateReadyCheck();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /ready command (respond yes to ready check)
|
|
|
|
|
if (cmdLower == "ready") {
|
|
|
|
|
gameHandler.respondToReadyCheck(true);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /notready command (respond no to ready check)
|
|
|
|
|
if (cmdLower == "notready" || cmdLower == "nr") {
|
|
|
|
|
gameHandler.respondToReadyCheck(false);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /yield or /forfeit command
|
|
|
|
|
if (cmdLower == "yield" || cmdLower == "forfeit" || cmdLower == "surrender") {
|
|
|
|
|
gameHandler.forfeitDuel();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:17:01 -08:00
|
|
|
// AFK command
|
|
|
|
|
if (cmdLower == "afk" || cmdLower == "away") {
|
|
|
|
|
std::string afkMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
gameHandler.toggleAfk(afkMsg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DND command
|
|
|
|
|
if (cmdLower == "dnd" || cmdLower == "busy") {
|
|
|
|
|
std::string dndMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
gameHandler.toggleDnd(dndMsg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reply command
|
|
|
|
|
if (cmdLower == "r" || cmdLower == "reply") {
|
|
|
|
|
std::string replyMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
gameHandler.replyToLastWhisper(replyMsg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:28:46 -08:00
|
|
|
// Party/Raid management commands
|
|
|
|
|
if (cmdLower == "uninvite" || cmdLower == "kick") {
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
gameHandler.uninvitePlayer(playerName);
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Usage: /uninvite <player name>";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "leave" || cmdLower == "leaveparty") {
|
|
|
|
|
gameHandler.leaveParty();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "maintank" || cmdLower == "mt") {
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
gameHandler.setMainTank(gameHandler.getTargetGuid());
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must target a player to set as main tank.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "mainassist" || cmdLower == "ma") {
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
gameHandler.setMainAssist(gameHandler.getTargetGuid());
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must target a player to set as main assist.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "clearmaintank") {
|
|
|
|
|
gameHandler.clearMainTank();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "clearmainassist") {
|
|
|
|
|
gameHandler.clearMainAssist();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "raidinfo") {
|
|
|
|
|
gameHandler.requestRaidInfo();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:36:50 -08:00
|
|
|
// Combat and Trade commands
|
|
|
|
|
if (cmdLower == "duel") {
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
gameHandler.proposeDuel(gameHandler.getTargetGuid());
|
|
|
|
|
} else if (spacePos != std::string::npos) {
|
|
|
|
|
// Target player by name (would need name-to-GUID lookup)
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must target a player to challenge to a duel.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must target a player to challenge to a duel.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "trade") {
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
gameHandler.initiateTrade(gameHandler.getTargetGuid());
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must target a player to trade with.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "startattack") {
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
gameHandler.startAutoAttack(gameHandler.getTargetGuid());
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You have no target.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "stopattack") {
|
|
|
|
|
gameHandler.stopAutoAttack();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "stopcasting") {
|
|
|
|
|
gameHandler.stopCasting();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 13:44:36 -08:00
|
|
|
// Targeting commands
|
|
|
|
|
if (cmdLower == "cleartarget") {
|
|
|
|
|
gameHandler.clearTarget();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetenemy") {
|
|
|
|
|
gameHandler.targetEnemy(false);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetfriend") {
|
|
|
|
|
gameHandler.targetFriend(false);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetlasttarget" || cmdLower == "targetlast") {
|
|
|
|
|
gameHandler.targetLastTarget();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetlastenemy") {
|
|
|
|
|
gameHandler.targetEnemy(true); // Reverse direction
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetlastfriend") {
|
|
|
|
|
gameHandler.targetFriend(true); // Reverse direction
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "focus") {
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
gameHandler.setFocus(gameHandler.getTargetGuid());
|
|
|
|
|
} else {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must target a unit to set as focus.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
}
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "clearfocus") {
|
|
|
|
|
gameHandler.clearFocus();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 16:59:20 -08:00
|
|
|
// /unstuck command — resets player position to floor height
|
|
|
|
|
if (cmdLower == "unstuck") {
|
|
|
|
|
gameHandler.unstuck();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-08 03:24:12 -08:00
|
|
|
// /unstuckgy command — move to nearest graveyard
|
|
|
|
|
if (cmdLower == "unstuckgy") {
|
|
|
|
|
gameHandler.unstuckGy();
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 16:59:20 -08:00
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// Chat channel slash commands
|
2026-02-07 23:32:27 -08:00
|
|
|
// If used without a message (e.g. just "/s"), switch the chat type dropdown
|
2026-02-07 12:30:36 -08:00
|
|
|
bool isChannelCommand = false;
|
|
|
|
|
if (cmdLower == "s" || cmdLower == "say") {
|
|
|
|
|
type = game::ChatType::SAY;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 0;
|
|
|
|
|
} else if (cmdLower == "y" || cmdLower == "yell" || cmdLower == "shout") {
|
2026-02-07 12:30:36 -08:00
|
|
|
type = game::ChatType::YELL;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 1;
|
2026-02-07 12:30:36 -08:00
|
|
|
} else if (cmdLower == "p" || cmdLower == "party") {
|
|
|
|
|
type = game::ChatType::PARTY;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 2;
|
2026-02-07 12:30:36 -08:00
|
|
|
} else if (cmdLower == "g" || cmdLower == "guild") {
|
|
|
|
|
type = game::ChatType::GUILD;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 3;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
} else if (cmdLower == "raid" || cmdLower == "rsay" || cmdLower == "ra") {
|
|
|
|
|
type = game::ChatType::RAID;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 5;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
} else if (cmdLower == "raidwarning" || cmdLower == "rw") {
|
|
|
|
|
type = game::ChatType::RAID_WARNING;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 8;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
} else if (cmdLower == "officer" || cmdLower == "o" || cmdLower == "osay") {
|
|
|
|
|
type = game::ChatType::OFFICER;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 6;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
} else if (cmdLower == "battleground" || cmdLower == "bg") {
|
|
|
|
|
type = game::ChatType::BATTLEGROUND;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 7;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
} else if (cmdLower == "instance" || cmdLower == "i") {
|
|
|
|
|
// Instance chat uses PARTY chat type
|
|
|
|
|
type = game::ChatType::PARTY;
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 9;
|
2026-02-07 12:30:36 -08:00
|
|
|
} else if (cmdLower == "w" || cmdLower == "whisper" || cmdLower == "tell" || cmdLower == "t") {
|
2026-02-07 23:32:27 -08:00
|
|
|
switchChatType = 4;
|
2026-02-07 12:30:36 -08:00
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
std::string rest = command.substr(spacePos + 1);
|
|
|
|
|
size_t msgStart = rest.find(' ');
|
|
|
|
|
if (msgStart != std::string::npos) {
|
2026-02-07 23:32:27 -08:00
|
|
|
// /w PlayerName message — send whisper immediately
|
2026-02-07 12:30:36 -08:00
|
|
|
target = rest.substr(0, msgStart);
|
|
|
|
|
message = rest.substr(msgStart + 1);
|
|
|
|
|
type = game::ChatType::WHISPER;
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
// Set whisper target for future messages
|
|
|
|
|
strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
2026-02-07 23:32:27 -08:00
|
|
|
// /w PlayerName — switch to whisper mode with target set
|
|
|
|
|
strncpy(whisperTargetBuffer, rest.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
message = "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-07 23:32:27 -08:00
|
|
|
// Just "/w" — switch to whisper mode
|
|
|
|
|
message = "";
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 12:30:36 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for emote commands
|
|
|
|
|
if (!isChannelCommand) {
|
2026-02-07 20:02:14 -08:00
|
|
|
std::string targetName;
|
|
|
|
|
const std::string* targetNamePtr = nullptr;
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto targetEntity = gameHandler.getTarget();
|
|
|
|
|
if (targetEntity) {
|
|
|
|
|
targetName = getEntityName(targetEntity);
|
|
|
|
|
if (!targetName.empty()) targetNamePtr = &targetName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string emoteText = rendering::Renderer::getEmoteText(cmdLower, targetNamePtr);
|
2026-02-07 12:30:36 -08:00
|
|
|
if (!emoteText.empty()) {
|
|
|
|
|
// Play the emote animation
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (renderer) {
|
|
|
|
|
renderer->playEmote(cmdLower);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add local chat message
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::TEXT_EMOTE;
|
|
|
|
|
msg.language = game::ChatLanguage::COMMON;
|
2026-02-07 20:02:14 -08:00
|
|
|
msg.message = emoteText;
|
2026-02-07 12:30:36 -08:00
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// Not a recognized command — fall through and send as normal chat
|
|
|
|
|
if (!isChannelCommand) {
|
|
|
|
|
message = input;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// If no valid command found and starts with /, just send as-is
|
|
|
|
|
if (!isChannelCommand && message == input) {
|
|
|
|
|
// Use the selected chat type from dropdown
|
|
|
|
|
switch (selectedChatType) {
|
|
|
|
|
case 0: type = game::ChatType::SAY; break;
|
|
|
|
|
case 1: type = game::ChatType::YELL; break;
|
|
|
|
|
case 2: type = game::ChatType::PARTY; break;
|
|
|
|
|
case 3: type = game::ChatType::GUILD; break;
|
|
|
|
|
case 4: type = game::ChatType::WHISPER; target = whisperTargetBuffer; break;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
case 5: type = game::ChatType::RAID; break;
|
|
|
|
|
case 6: type = game::ChatType::OFFICER; break;
|
|
|
|
|
case 7: type = game::ChatType::BATTLEGROUND; break;
|
|
|
|
|
case 8: type = game::ChatType::RAID_WARNING; break;
|
|
|
|
|
case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY
|
2026-02-07 12:30:36 -08:00
|
|
|
default: type = game::ChatType::SAY; break;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
} else {
|
|
|
|
|
// No slash command, use the selected chat type from dropdown
|
|
|
|
|
switch (selectedChatType) {
|
|
|
|
|
case 0: type = game::ChatType::SAY; break;
|
|
|
|
|
case 1: type = game::ChatType::YELL; break;
|
|
|
|
|
case 2: type = game::ChatType::PARTY; break;
|
|
|
|
|
case 3: type = game::ChatType::GUILD; break;
|
|
|
|
|
case 4: type = game::ChatType::WHISPER; target = whisperTargetBuffer; break;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
case 5: type = game::ChatType::RAID; break;
|
|
|
|
|
case 6: type = game::ChatType::OFFICER; break;
|
|
|
|
|
case 7: type = game::ChatType::BATTLEGROUND; break;
|
|
|
|
|
case 8: type = game::ChatType::RAID_WARNING; break;
|
|
|
|
|
case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY
|
2026-02-07 12:30:36 -08:00
|
|
|
default: type = game::ChatType::SAY; break;
|
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-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
// Validate whisper has a target
|
|
|
|
|
if (type == game::ChatType::WHISPER && target.empty()) {
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "You must specify a player name for whisper.";
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
return;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
// Don't send empty messages — but switch chat type if a channel shortcut was used
|
2026-02-07 12:30:36 -08:00
|
|
|
if (!message.empty()) {
|
|
|
|
|
gameHandler.sendChatMessage(type, message, target);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
// Switch chat type dropdown when channel shortcut used (with or without message)
|
|
|
|
|
if (switchChatType >= 0) {
|
|
|
|
|
selectedChatType = switchChatType;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Clear input
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* GameScreen::getChatTypeName(game::ChatType type) const {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case game::ChatType::SAY: return "SAY";
|
|
|
|
|
case game::ChatType::YELL: return "YELL";
|
|
|
|
|
case game::ChatType::EMOTE: return "EMOTE";
|
|
|
|
|
case game::ChatType::TEXT_EMOTE: return "EMOTE";
|
|
|
|
|
case game::ChatType::PARTY: return "PARTY";
|
|
|
|
|
case game::ChatType::GUILD: return "GUILD";
|
|
|
|
|
case game::ChatType::OFFICER: return "OFFICER";
|
|
|
|
|
case game::ChatType::RAID: return "RAID";
|
|
|
|
|
case game::ChatType::RAID_LEADER: return "RAID LEADER";
|
|
|
|
|
case game::ChatType::RAID_WARNING: return "RAID WARNING";
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
case game::ChatType::BATTLEGROUND: return "BATTLEGROUND";
|
|
|
|
|
case game::ChatType::BATTLEGROUND_LEADER: return "BG LEADER";
|
2026-02-02 12:24:50 -08:00
|
|
|
case game::ChatType::WHISPER: return "WHISPER";
|
|
|
|
|
case game::ChatType::WHISPER_INFORM: return "TO";
|
|
|
|
|
case game::ChatType::SYSTEM: return "SYSTEM";
|
|
|
|
|
case game::ChatType::CHANNEL: return "CHANNEL";
|
|
|
|
|
case game::ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
|
|
|
|
|
default: return "UNKNOWN";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case game::ChatType::SAY:
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
|
|
|
|
|
case game::ChatType::YELL:
|
|
|
|
|
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
|
|
|
|
|
case game::ChatType::EMOTE:
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
|
|
|
|
case game::ChatType::TEXT_EMOTE:
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
|
|
|
|
case game::ChatType::PARTY:
|
|
|
|
|
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
|
|
|
|
|
case game::ChatType::GUILD:
|
|
|
|
|
return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
|
|
|
|
|
case game::ChatType::OFFICER:
|
|
|
|
|
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
|
|
|
|
|
case game::ChatType::RAID:
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
case game::ChatType::RAID_LEADER:
|
|
|
|
|
return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange
|
|
|
|
|
case game::ChatType::RAID_WARNING:
|
|
|
|
|
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
|
|
|
|
|
case game::ChatType::BATTLEGROUND:
|
|
|
|
|
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold
|
|
|
|
|
case game::ChatType::BATTLEGROUND_LEADER:
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
2026-02-02 12:24:50 -08:00
|
|
|
case game::ChatType::WHISPER:
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
|
|
|
|
case game::ChatType::WHISPER_INFORM:
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
|
|
|
|
case game::ChatType::SYSTEM:
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
|
|
|
|
|
case game::ChatType::CHANNEL:
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
|
|
|
|
|
case game::ChatType::ACHIEVEMENT:
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
|
|
|
|
|
default:
|
|
|
|
|
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Gray
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::updateCharacterGeosets(game::Inventory& inventory) {
|
|
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
auto* renderer = app.getRenderer();
|
|
|
|
|
if (!renderer) return;
|
|
|
|
|
|
|
|
|
|
uint32_t instanceId = renderer->getCharacterInstanceId();
|
|
|
|
|
if (instanceId == 0) return;
|
|
|
|
|
|
|
|
|
|
auto* charRenderer = renderer->getCharacterRenderer();
|
|
|
|
|
if (!charRenderer) return;
|
|
|
|
|
|
|
|
|
|
auto* assetManager = app.getAssetManager();
|
|
|
|
|
|
|
|
|
|
// Load ItemDisplayInfo.dbc for geosetGroup lookup
|
|
|
|
|
std::shared_ptr<pipeline::DBCFile> displayInfoDbc;
|
|
|
|
|
if (assetManager) {
|
|
|
|
|
displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: get geosetGroup field for an equipped item's displayInfoId
|
|
|
|
|
// DBC binary fields: 7=geosetGroup_1, 8=geosetGroup_2, 9=geosetGroup_3
|
|
|
|
|
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
|
|
|
|
if (!displayInfoDbc || displayInfoId == 0) return 0;
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
|
|
|
if (recIdx < 0) return 0;
|
|
|
|
|
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper: find first equipped item matching inventoryType, return its displayInfoId
|
|
|
|
|
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (slot.item.inventoryType == t)
|
|
|
|
|
return slot.item.displayInfoId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper: check if any equipment slot has the given inventoryType
|
|
|
|
|
auto hasEquippedType = [&](std::initializer_list<uint8_t> types) -> bool {
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (slot.item.inventoryType == t) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Base geosets always present
|
|
|
|
|
std::unordered_set<uint16_t> geosets;
|
|
|
|
|
for (uint16_t i = 0; i <= 18; i++) {
|
|
|
|
|
geosets.insert(i);
|
|
|
|
|
}
|
|
|
|
|
geosets.insert(101); // Hair
|
|
|
|
|
geosets.insert(201); // Facial
|
|
|
|
|
geosets.insert(701); // Ears
|
|
|
|
|
|
|
|
|
|
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
|
|
|
|
|
// geosetGroup_1 > 0 → use mesh variant (502+), otherwise bare (501) + texture only
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : (did > 0 ? 501 : 501)));
|
|
|
|
|
// geosetGroup_3 > 0 on robes also shows kilt legs (1302)
|
|
|
|
|
uint32_t gg3 = getGeosetGroup(did, 2);
|
|
|
|
|
if (gg3 > 0) {
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Legs: inventoryType 7
|
|
|
|
|
// geosetGroup_1 > 0 → kilt/skirt mesh (1302+), otherwise bare legs (1301) + texture
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({7});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
// Only add leg geoset if robe hasn't already set a kilt geoset
|
|
|
|
|
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Feet/Boots: inventoryType 8
|
|
|
|
|
// geosetGroup_1 > 0 → boot mesh (402+), otherwise bare feet (401) + texture
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({8});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gloves/Hands: inventoryType 10
|
|
|
|
|
// geosetGroup_1 > 0 → glove mesh (302+), otherwise bare hands (301)
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({10});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Back/Cloak: inventoryType 16 — geoset only, no skin texture (cloaks are separate models)
|
|
|
|
|
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
|
|
|
|
|
|
|
|
|
// Tabard: inventoryType 19
|
|
|
|
|
if (hasEquippedType({19})) {
|
|
|
|
|
geosets.insert(1201);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
charRenderer->setActiveGeosets(instanceId, geosets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
|
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
auto* renderer = app.getRenderer();
|
|
|
|
|
if (!renderer) return;
|
|
|
|
|
|
|
|
|
|
auto* charRenderer = renderer->getCharacterRenderer();
|
|
|
|
|
if (!charRenderer) return;
|
|
|
|
|
|
|
|
|
|
auto* assetManager = app.getAssetManager();
|
|
|
|
|
if (!assetManager) return;
|
|
|
|
|
|
|
|
|
|
const auto& bodySkinPath = app.getBodySkinPath();
|
|
|
|
|
const auto& underwearPaths = app.getUnderwearPaths();
|
|
|
|
|
uint32_t skinSlot = app.getSkinTextureSlotIndex();
|
|
|
|
|
|
|
|
|
|
if (bodySkinPath.empty()) return;
|
|
|
|
|
|
|
|
|
|
// Component directory names indexed by region
|
|
|
|
|
static const char* componentDirs[] = {
|
|
|
|
|
"ArmUpperTexture", // 0
|
|
|
|
|
"ArmLowerTexture", // 1
|
|
|
|
|
"HandTexture", // 2
|
|
|
|
|
"TorsoUpperTexture", // 3
|
|
|
|
|
"TorsoLowerTexture", // 4
|
|
|
|
|
"LegUpperTexture", // 5
|
|
|
|
|
"LegLowerTexture", // 6
|
|
|
|
|
"FootTexture", // 7
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Load ItemDisplayInfo.dbc
|
|
|
|
|
auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
if (!displayInfoDbc) return;
|
|
|
|
|
|
|
|
|
|
// Collect equipment texture regions from all equipped items
|
|
|
|
|
std::vector<std::pair<int, std::string>> regionLayers;
|
|
|
|
|
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (slot.empty() || slot.item.displayInfoId == 0) continue;
|
|
|
|
|
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
|
|
|
|
|
if (recIdx < 0) continue;
|
|
|
|
|
|
|
|
|
|
// DBC fields 15-22 = texture_1 through texture_8 (regions 0-7)
|
|
|
|
|
// (binary DBC has inventoryIcon_2 at field 6, shifting fields +1 vs CSV)
|
|
|
|
|
for (int region = 0; region < 8; region++) {
|
|
|
|
|
uint32_t fieldIdx = 15 + region;
|
|
|
|
|
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
|
|
|
|
if (texName.empty()) continue;
|
|
|
|
|
|
|
|
|
|
// Actual MPQ files have a gender suffix: _M (male), _F (female), _U (unisex)
|
|
|
|
|
// Try gender-specific first, then unisex fallback
|
|
|
|
|
std::string base = "Item\\TextureComponents\\" +
|
|
|
|
|
std::string(componentDirs[region]) + "\\" + texName;
|
|
|
|
|
std::string malePath = base + "_M.blp";
|
|
|
|
|
std::string unisexPath = base + "_U.blp";
|
|
|
|
|
std::string fullPath;
|
|
|
|
|
if (assetManager->fileExists(malePath)) {
|
|
|
|
|
fullPath = malePath;
|
|
|
|
|
} else if (assetManager->fileExists(unisexPath)) {
|
|
|
|
|
fullPath = unisexPath;
|
|
|
|
|
} else {
|
|
|
|
|
// Last resort: try without suffix
|
|
|
|
|
fullPath = base + ".blp";
|
|
|
|
|
}
|
|
|
|
|
regionLayers.emplace_back(region, fullPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-composite: base skin + underwear + equipment regions
|
|
|
|
|
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
|
|
|
|
if (newTex != 0) {
|
|
|
|
|
charRenderer->setModelTexture(1, skinSlot, newTex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin)
|
|
|
|
|
uint32_t cloakSlot = app.getCloakTextureSlotIndex();
|
|
|
|
|
if (cloakSlot > 0) {
|
|
|
|
|
// Find equipped cloak (inventoryType 16)
|
|
|
|
|
uint32_t cloakDisplayId = 0;
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty() && slot.item.inventoryType == 16 && slot.item.displayInfoId != 0) {
|
|
|
|
|
cloakDisplayId = slot.item.displayInfoId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cloakDisplayId > 0) {
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId);
|
|
|
|
|
if (recIdx >= 0) {
|
|
|
|
|
// DBC field 3 = modelTexture_1 (cape texture name)
|
|
|
|
|
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
|
|
|
|
|
if (!capeName.empty()) {
|
|
|
|
|
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
|
|
|
|
|
GLuint capeTex = charRenderer->loadTexture(capePath);
|
|
|
|
|
if (capeTex != 0) {
|
|
|
|
|
charRenderer->setModelTexture(1, cloakSlot, capeTex);
|
|
|
|
|
LOG_INFO("Cloak texture applied: ", capePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No cloak equipped — reset to white fallback
|
|
|
|
|
charRenderer->resetModelTexture(1, cloakSlot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 22:27:45 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// World Map
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderWorldMap(game::GameHandler& /* gameHandler */) {
|
|
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
auto* renderer = app.getRenderer();
|
|
|
|
|
auto* assetMgr = app.getAssetManager();
|
|
|
|
|
if (!renderer || !assetMgr) return;
|
|
|
|
|
|
|
|
|
|
worldMap.initialize(assetMgr);
|
|
|
|
|
|
|
|
|
|
// Keep map name in sync with minimap's map name
|
|
|
|
|
auto* minimap = renderer->getMinimap();
|
|
|
|
|
if (minimap) {
|
|
|
|
|
worldMap.setMapName(minimap->getMapName());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glm::vec3 playerPos = renderer->getCharacterPosition();
|
|
|
|
|
auto* window = app.getWindow();
|
|
|
|
|
int screenW = window ? window->getWidth() : 1280;
|
|
|
|
|
int screenH = window ? window->getHeight() : 720;
|
|
|
|
|
worldMap.render(playerPos, screenW, screenH);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// ============================================================
|
|
|
|
|
// Action Bar (Phase 3)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-06 14:30:54 -08:00
|
|
|
GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
|
|
|
|
|
if (spellId == 0 || !am) return 0;
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
auto cit = spellIconCache_.find(spellId);
|
|
|
|
|
if (cit != spellIconCache_.end()) return cit->second;
|
|
|
|
|
|
|
|
|
|
// Lazy-load SpellIcon.dbc and Spell.dbc icon IDs
|
|
|
|
|
if (!spellIconDbLoaded_) {
|
|
|
|
|
spellIconDbLoaded_ = true;
|
|
|
|
|
|
|
|
|
|
// Load SpellIcon.dbc: field 0 = ID, field 1 = icon path
|
|
|
|
|
auto iconDbc = am->loadDBC("SpellIcon.dbc");
|
|
|
|
|
if (iconDbc && iconDbc->isLoaded()) {
|
|
|
|
|
for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) {
|
|
|
|
|
uint32_t id = iconDbc->getUInt32(i, 0);
|
|
|
|
|
std::string path = iconDbc->getString(i, 1);
|
|
|
|
|
if (!path.empty() && id > 0) {
|
|
|
|
|
spellIconPaths_[id] = path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load Spell.dbc: field 133 = SpellIconID
|
|
|
|
|
auto spellDbc = am->loadDBC("Spell.dbc");
|
|
|
|
|
if (spellDbc && spellDbc->isLoaded() && spellDbc->getFieldCount() > 133) {
|
|
|
|
|
for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) {
|
|
|
|
|
uint32_t id = spellDbc->getUInt32(i, 0);
|
|
|
|
|
uint32_t iconId = spellDbc->getUInt32(i, 133);
|
|
|
|
|
if (id > 0 && iconId > 0) {
|
|
|
|
|
spellIconIds_[id] = iconId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Look up spellId -> SpellIconID -> icon path
|
|
|
|
|
auto iit = spellIconIds_.find(spellId);
|
|
|
|
|
if (iit == spellIconIds_.end()) {
|
|
|
|
|
spellIconCache_[spellId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto pit = spellIconPaths_.find(iit->second);
|
|
|
|
|
if (pit == spellIconPaths_.end()) {
|
|
|
|
|
spellIconCache_[spellId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Path from DBC has no extension — append .blp
|
|
|
|
|
std::string iconPath = pit->second + ".blp";
|
|
|
|
|
auto blpData = am->readFile(iconPath);
|
|
|
|
|
if (blpData.empty()) {
|
|
|
|
|
spellIconCache_[spellId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto image = pipeline::BLPLoader::load(blpData);
|
|
|
|
|
if (!image.isValid()) {
|
|
|
|
|
spellIconCache_[spellId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GLuint texId = 0;
|
|
|
|
|
glGenTextures(1, &texId);
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, texId);
|
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
|
|
|
|
|
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
|
|
|
|
|
|
spellIconCache_[spellId] = texId;
|
|
|
|
|
return texId;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-02-05 15:07:13 -08:00
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
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
|
|
|
|
|
|
|
|
float slotSize = 48.0f;
|
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
float padding = 8.0f;
|
|
|
|
|
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
|
|
|
|
float barH = slotSize + 24.0f;
|
|
|
|
|
float barX = (screenW - barW) / 2.0f;
|
|
|
|
|
float barY = screenH - barH;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
2026-02-09 01:40:29 -08:00
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
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
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##ActionBar", nullptr, flags)) {
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
|
|
|
|
static const char* keyLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 12; ++i) {
|
|
|
|
|
if (i > 0) ImGui::SameLine(0, spacing);
|
|
|
|
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
|
|
|
|
const auto& slot = bar[i];
|
|
|
|
|
bool onCooldown = !slot.isReady();
|
|
|
|
|
|
2026-02-05 15:07:13 -08:00
|
|
|
auto getSpellName = [&](uint32_t spellId) -> std::string {
|
|
|
|
|
if (!actionSpellDbAttempted) {
|
|
|
|
|
actionSpellDbAttempted = true;
|
|
|
|
|
if (assetMgr && assetMgr->isInitialized()) {
|
|
|
|
|
auto dbc = assetMgr->loadDBC("Spell.dbc");
|
|
|
|
|
if (dbc && dbc->isLoaded()) {
|
|
|
|
|
uint32_t fieldCount = dbc->getFieldCount();
|
|
|
|
|
uint32_t nameField = 136;
|
|
|
|
|
if (fieldCount < 137) {
|
|
|
|
|
if (fieldCount > 10) {
|
|
|
|
|
nameField = fieldCount > 140 ? 136 : 1;
|
|
|
|
|
} else {
|
|
|
|
|
nameField = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
uint32_t count = dbc->getRecordCount();
|
|
|
|
|
actionSpellNames.reserve(count);
|
2026-02-06 14:30:54 -08:00
|
|
|
for (uint32_t r = 0; r < count; ++r) {
|
|
|
|
|
uint32_t id = dbc->getUInt32(r, 0);
|
|
|
|
|
std::string name = dbc->getString(r, nameField);
|
2026-02-05 15:07:13 -08:00
|
|
|
if (!name.empty() && id > 0) {
|
|
|
|
|
actionSpellNames[id] = name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
actionSpellDbLoaded = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
auto it = actionSpellNames.find(spellId);
|
|
|
|
|
if (it != actionSpellNames.end()) return it->second;
|
|
|
|
|
return "Spell #" + std::to_string(spellId);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-06 14:30:54 -08:00
|
|
|
// Try to get icon texture for this slot
|
|
|
|
|
GLuint iconTex = 0;
|
2026-02-06 19:17:35 -08:00
|
|
|
const game::ItemDef* barItemDef = nullptr;
|
2026-02-06 14:30:54 -08:00
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) {
|
|
|
|
|
iconTex = getSpellIcon(slot.id, assetMgr);
|
2026-02-06 19:17:35 -08:00
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
// Look up item in inventory for icon and name
|
|
|
|
|
auto& inv = gameHandler.getInventory();
|
|
|
|
|
for (int bi = 0; bi < inv.getBackpackSize(); bi++) {
|
|
|
|
|
const auto& bs = inv.getBackpackSlot(bi);
|
|
|
|
|
if (!bs.empty() && bs.item.itemId == slot.id) {
|
|
|
|
|
barItemDef = &bs.item;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (barItemDef && barItemDef->displayInfoId != 0) {
|
|
|
|
|
iconTex = inventoryScreen.getItemIcon(barItemDef->displayInfoId);
|
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool clicked = false;
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
// Render icon-based button
|
|
|
|
|
ImVec4 tintColor(1, 1, 1, 1);
|
|
|
|
|
ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f);
|
|
|
|
|
if (onCooldown) {
|
|
|
|
|
tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f);
|
|
|
|
|
bgColor = ImVec4(0.1f, 0.1f, 0.1f, 0.8f);
|
2026-02-05 15:07:13 -08:00
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
clicked = ImGui::ImageButton("##icon",
|
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
ImVec2(slotSize - 4, slotSize - 4),
|
|
|
|
|
ImVec2(0, 0), ImVec2(1, 1),
|
|
|
|
|
bgColor, tintColor);
|
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
|
|
|
} else {
|
2026-02-06 14:30:54 -08:00
|
|
|
// Fallback to text button
|
|
|
|
|
if (onCooldown) {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f));
|
|
|
|
|
} else if (slot.isEmpty()) {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char label[32];
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL) {
|
|
|
|
|
std::string spellName = getSpellName(slot.id);
|
|
|
|
|
if (spellName.size() > 6) spellName = spellName.substr(0, 6);
|
|
|
|
|
snprintf(label, sizeof(label), "%s", spellName.c_str());
|
2026-02-06 19:17:35 -08:00
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && barItemDef) {
|
|
|
|
|
std::string itemName = barItemDef->name;
|
|
|
|
|
if (itemName.size() > 6) itemName = itemName.substr(0, 6);
|
|
|
|
|
snprintf(label, sizeof(label), "%s", itemName.c_str());
|
2026-02-06 14:30:54 -08:00
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM) {
|
|
|
|
|
snprintf(label, sizeof(label), "Item");
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
|
|
|
|
snprintf(label, sizeof(label), "Macro");
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(label, sizeof(label), "--");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clicked = ImGui::Button(label, ImVec2(slotSize, slotSize));
|
|
|
|
|
ImGui::PopStyleColor();
|
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-06 20:27:01 -08:00
|
|
|
bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
2026-02-06 21:25:35 -08:00
|
|
|
bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
2026-02-06 20:36:29 -08:00
|
|
|
ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
2026-02-06 20:27:01 -08:00
|
|
|
|
2026-02-06 20:36:29 -08:00
|
|
|
// Drop dragged spell from spellbook onto this slot
|
|
|
|
|
// (mouse release over slot — button click won't fire since press was in spellbook)
|
|
|
|
|
if (hoveredOnRelease && spellbookScreen.isDraggingSpell()) {
|
2026-02-06 20:27:01 -08:00
|
|
|
gameHandler.setActionBarSlot(i, game::ActionBarSlot::SPELL,
|
|
|
|
|
spellbookScreen.getDragSpellId());
|
|
|
|
|
spellbookScreen.consumeDragSpell();
|
2026-02-06 20:36:29 -08:00
|
|
|
} else if (clicked && inventoryScreen.isHoldingItem()) {
|
|
|
|
|
// Drop held item from inventory onto action bar
|
|
|
|
|
const auto& held = inventoryScreen.getHeldItem();
|
|
|
|
|
gameHandler.setActionBarSlot(i, game::ActionBarSlot::ITEM, held.itemId);
|
|
|
|
|
inventoryScreen.returnHeldItem(gameHandler.getInventory());
|
2026-02-06 19:24:44 -08:00
|
|
|
} else if (clicked && actionBarDragSlot_ >= 0) {
|
|
|
|
|
// Dropping a dragged action bar slot onto another slot - swap or place
|
|
|
|
|
if (i != actionBarDragSlot_) {
|
|
|
|
|
const auto& dragSrc = bar[actionBarDragSlot_];
|
|
|
|
|
auto srcType = dragSrc.type;
|
|
|
|
|
auto srcId = dragSrc.id;
|
|
|
|
|
gameHandler.setActionBarSlot(actionBarDragSlot_, slot.type, slot.id);
|
|
|
|
|
gameHandler.setActionBarSlot(i, srcType, srcId);
|
|
|
|
|
}
|
|
|
|
|
actionBarDragSlot_ = -1;
|
|
|
|
|
actionBarDragIcon_ = 0;
|
|
|
|
|
} else if (clicked && !slot.isEmpty()) {
|
2026-02-06 20:27:01 -08:00
|
|
|
// Left-click on non-empty slot: cast spell or use item
|
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
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) {
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
gameHandler.castSpell(slot.id, target);
|
2026-02-06 19:17:35 -08:00
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
gameHandler.useItemById(slot.id);
|
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-06 20:27:01 -08:00
|
|
|
} else if (rightClicked && !slot.isEmpty()) {
|
|
|
|
|
// Right-click on non-empty slot: pick up for dragging
|
|
|
|
|
actionBarDragSlot_ = i;
|
|
|
|
|
actionBarDragIcon_ = iconTex;
|
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-06 19:17:35 -08:00
|
|
|
// Tooltip
|
2026-02-05 15:07:13 -08:00
|
|
|
if (ImGui::IsItemHovered() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0) {
|
|
|
|
|
std::string fullName = getSpellName(slot.id);
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::Text("%s", fullName.c_str());
|
|
|
|
|
ImGui::TextDisabled("Spell ID: %u", slot.id);
|
|
|
|
|
ImGui::EndTooltip();
|
2026-02-06 19:17:35 -08:00
|
|
|
} else if (ImGui::IsItemHovered() && slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
if (barItemDef) {
|
|
|
|
|
ImGui::Text("%s", barItemDef->name.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::Text("Item #%u", slot.id);
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndTooltip();
|
2026-02-05 15:07:13 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:30:54 -08:00
|
|
|
// Cooldown overlay
|
|
|
|
|
if (onCooldown && iconTex) {
|
|
|
|
|
// Draw cooldown text centered over the icon
|
|
|
|
|
ImVec2 btnMin = ImGui::GetItemRectMin();
|
|
|
|
|
ImVec2 btnMax = ImGui::GetItemRectMax();
|
|
|
|
|
char cdText[16];
|
|
|
|
|
snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining);
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(cdText);
|
|
|
|
|
float cx = btnMin.x + (btnMax.x - btnMin.x - textSize.x) * 0.5f;
|
|
|
|
|
float cy = btnMin.y + (btnMax.y - btnMin.y - textSize.y) * 0.5f;
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
|
|
|
|
IM_COL32(255, 255, 0, 255), cdText);
|
|
|
|
|
} else if (onCooldown) {
|
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
|
|
|
char cdText[16];
|
|
|
|
|
snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining);
|
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() - slotSize / 2 - 8);
|
|
|
|
|
ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", cdText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Key label below
|
|
|
|
|
ImGui::TextDisabled("%s", keyLabels[i]);
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-02-06 19:24:44 -08:00
|
|
|
|
|
|
|
|
// Handle action bar drag: render icon at cursor and detect drop outside
|
|
|
|
|
if (actionBarDragSlot_ >= 0) {
|
|
|
|
|
ImVec2 mousePos = ImGui::GetMousePos();
|
|
|
|
|
|
|
|
|
|
// Draw dragged icon at cursor
|
|
|
|
|
if (actionBarDragIcon_) {
|
|
|
|
|
ImGui::GetForegroundDrawList()->AddImage(
|
|
|
|
|
(ImTextureID)(uintptr_t)actionBarDragIcon_,
|
|
|
|
|
ImVec2(mousePos.x - 20, mousePos.y - 20),
|
|
|
|
|
ImVec2(mousePos.x + 20, mousePos.y + 20));
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::GetForegroundDrawList()->AddRectFilled(
|
|
|
|
|
ImVec2(mousePos.x - 20, mousePos.y - 20),
|
|
|
|
|
ImVec2(mousePos.x + 20, mousePos.y + 20),
|
|
|
|
|
IM_COL32(80, 80, 120, 180));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:27:01 -08:00
|
|
|
// On right mouse release, check if outside the action bar area
|
|
|
|
|
if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
|
2026-02-06 19:24:44 -08:00
|
|
|
bool insideBar = (mousePos.x >= barX && mousePos.x <= barX + barW &&
|
|
|
|
|
mousePos.y >= barY && mousePos.y <= barY + barH);
|
|
|
|
|
if (!insideBar) {
|
|
|
|
|
// Dropped outside - clear the slot
|
|
|
|
|
gameHandler.setActionBarSlot(actionBarDragSlot_, game::ActionBarSlot::EMPTY, 0);
|
|
|
|
|
}
|
|
|
|
|
actionBarDragSlot_ = -1;
|
|
|
|
|
actionBarDragIcon_ = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
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-05 12:07:58 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// XP Bar
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
|
|
|
|
|
uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp();
|
|
|
|
|
if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized)
|
|
|
|
|
|
|
|
|
|
uint32_t currentXp = gameHandler.getPlayerXp();
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
// Position just above the action bar
|
|
|
|
|
float slotSize = 48.0f;
|
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
float padding = 8.0f;
|
|
|
|
|
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
|
|
|
|
float barH = slotSize + 24.0f;
|
|
|
|
|
float actionBarY = screenH - barH;
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
float xpBarH = 20.0f;
|
2026-02-05 12:07:58 -08:00
|
|
|
float xpBarW = barW;
|
|
|
|
|
float xpBarX = (screenW - xpBarW) / 2.0f;
|
|
|
|
|
float xpBarY = actionBarY - xpBarH - 2.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(xpBarX, xpBarY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(xpBarW, xpBarH + 4.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 2.0f);
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 2.0f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##XpBar", nullptr, flags)) {
|
|
|
|
|
float pct = static_cast<float>(currentXp) / static_cast<float>(nextLevelXp);
|
|
|
|
|
if (pct > 1.0f) pct = 1.0f;
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
// Custom segmented XP bar (20 bubbles)
|
|
|
|
|
ImVec2 barMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, xpBarH - 4.0f);
|
|
|
|
|
ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y);
|
|
|
|
|
auto* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
ImU32 bg = IM_COL32(15, 15, 20, 220);
|
|
|
|
|
ImU32 fg = IM_COL32(148, 51, 238, 255);
|
|
|
|
|
ImU32 seg = IM_COL32(35, 35, 45, 255);
|
|
|
|
|
drawList->AddRectFilled(barMin, barMax, bg, 2.0f);
|
|
|
|
|
drawList->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f);
|
|
|
|
|
|
|
|
|
|
float fillW = barSize.x * pct;
|
|
|
|
|
if (fillW > 0.0f) {
|
|
|
|
|
drawList->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), fg, 2.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int segments = 20;
|
|
|
|
|
float segW = barSize.x / static_cast<float>(segments);
|
|
|
|
|
for (int i = 1; i < segments; ++i) {
|
|
|
|
|
float x = barMin.x + segW * i;
|
|
|
|
|
drawList->AddLine(ImVec2(x, barMin.y + 1.0f), ImVec2(x, barMax.y - 1.0f), seg, 1.0f);
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
|
|
|
|
char overlay[96];
|
2026-02-06 18:34:45 -08:00
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u XP", currentXp, nextLevelXp);
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(overlay);
|
|
|
|
|
float tx = barMin.x + (barSize.x - textSize.x) * 0.5f;
|
|
|
|
|
float ty = barMin.y + (barSize.y - textSize.y) * 0.5f;
|
|
|
|
|
drawList->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), overlay);
|
2026-02-05 12:07:58 -08:00
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
ImGui::Dummy(barSize);
|
2026-02-05 12:07:58 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// ============================================================
|
|
|
|
|
// Cast Bar (Phase 3)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderCastBar(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isCasting()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
float barW = 300.0f;
|
|
|
|
|
float barX = (screenW - barW) / 2.0f;
|
|
|
|
|
float barY = screenH - 120.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, 40), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.9f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##CastBar", nullptr, flags)) {
|
|
|
|
|
float progress = gameHandler.getCastProgress();
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.6f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
char overlay[64];
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "Spell %u (%.1fs)",
|
|
|
|
|
gameHandler.getCurrentCastSpellId(), gameHandler.getCastTimeRemaining());
|
|
|
|
|
ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay);
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Floating Combat Text (Phase 2)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& entries = gameHandler.getCombatText();
|
|
|
|
|
if (entries.empty()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
// Render combat text entries overlaid on screen
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(screenW, 400));
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration |
|
|
|
|
|
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##CombatText", nullptr, flags)) {
|
|
|
|
|
float centerX = screenW / 2.0f;
|
|
|
|
|
int index = 0;
|
|
|
|
|
for (const auto& entry : entries) {
|
|
|
|
|
float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME);
|
|
|
|
|
float yOffset = 200.0f - entry.age * 60.0f;
|
|
|
|
|
|
|
|
|
|
ImVec4 color;
|
|
|
|
|
char text[64];
|
|
|
|
|
switch (entry.type) {
|
|
|
|
|
case game::CombatTextEntry::MELEE_DAMAGE:
|
|
|
|
|
case game::CombatTextEntry::SPELL_DAMAGE:
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
|
|
|
|
color = entry.isPlayerSource ?
|
|
|
|
|
ImVec4(1.0f, 1.0f, 0.3f, alpha) : // Outgoing = yellow
|
|
|
|
|
ImVec4(1.0f, 0.3f, 0.3f, alpha); // Incoming = red
|
|
|
|
|
break;
|
|
|
|
|
case game::CombatTextEntry::CRIT_DAMAGE:
|
|
|
|
|
snprintf(text, sizeof(text), "-%d!", entry.amount);
|
|
|
|
|
color = ImVec4(1.0f, 0.5f, 0.0f, alpha); // Orange for crit
|
|
|
|
|
break;
|
|
|
|
|
case game::CombatTextEntry::HEAL:
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
|
|
|
|
color = ImVec4(0.3f, 1.0f, 0.3f, alpha);
|
|
|
|
|
break;
|
|
|
|
|
case game::CombatTextEntry::CRIT_HEAL:
|
|
|
|
|
snprintf(text, sizeof(text), "+%d!", entry.amount);
|
|
|
|
|
color = ImVec4(0.3f, 1.0f, 0.3f, alpha);
|
|
|
|
|
break;
|
|
|
|
|
case game::CombatTextEntry::MISS:
|
|
|
|
|
snprintf(text, sizeof(text), "Miss");
|
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
|
|
|
|
break;
|
|
|
|
|
case game::CombatTextEntry::DODGE:
|
|
|
|
|
snprintf(text, sizeof(text), "Dodge");
|
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
|
|
|
|
break;
|
|
|
|
|
case game::CombatTextEntry::PARRY:
|
|
|
|
|
snprintf(text, sizeof(text), "Parry");
|
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
snprintf(text, sizeof(text), "%d", entry.amount);
|
|
|
|
|
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stagger entries horizontally
|
|
|
|
|
float xOffset = centerX + (index % 3 - 1) * 80.0f;
|
|
|
|
|
ImGui::SetCursorPos(ImVec2(xOffset, yOffset));
|
|
|
|
|
ImGui::TextColored(color, "%s", text);
|
|
|
|
|
index++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Party Frames (Phase 4)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isInGroup()) return;
|
|
|
|
|
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
float frameY = 120.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, frameY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.8f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##PartyFrames", nullptr, flags)) {
|
|
|
|
|
for (const auto& member : partyData.members) {
|
|
|
|
|
ImGui::PushID(static_cast<int>(member.guid));
|
|
|
|
|
|
|
|
|
|
// Clickable name to target
|
|
|
|
|
if (ImGui::Selectable(member.name.c_str(), gameHandler.getTargetGuid() == member.guid)) {
|
|
|
|
|
gameHandler.setTarget(member.guid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to show health from entity
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(member.guid);
|
|
|
|
|
if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
uint32_t hp = unit->getHealth();
|
|
|
|
|
uint32_t maxHp = unit->getMaxHealth();
|
|
|
|
|
if (maxHp > 0) {
|
|
|
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
|
|
|
pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 12), "");
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Group Invite Popup (Phase 4)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.hasPendingGroupInvite()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Group Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
ImGui::Text("%s has invited you to a group.", gameHandler.getPendingInviterName().c_str());
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
|
|
|
|
gameHandler.acceptGroupInvite();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
|
|
|
|
gameHandler.declineGroupInvite();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Buff/Debuff Bar (Phase 3)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& auras = gameHandler.getPlayerAuras();
|
|
|
|
|
if (auras.empty()) return;
|
|
|
|
|
|
|
|
|
|
// Count non-empty auras
|
|
|
|
|
int activeCount = 0;
|
|
|
|
|
for (const auto& a : auras) {
|
|
|
|
|
if (!a.isEmpty()) activeCount++;
|
|
|
|
|
}
|
|
|
|
|
if (activeCount == 0) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-02-07 23:47:43 -08:00
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
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-07 23:47:43 -08:00
|
|
|
// Position below the minimap (minimap is 200px + 10px margin from top-right)
|
|
|
|
|
constexpr float ICON_SIZE = 32.0f;
|
|
|
|
|
constexpr int ICONS_PER_ROW = 8;
|
|
|
|
|
float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW - barW - 10.0f, 220.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, 0), ImGuiCond_Always);
|
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
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
2026-02-07 23:47:43 -08:00
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar;
|
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
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
2026-02-07 23:47:43 -08:00
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f));
|
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
|
|
|
|
|
|
|
|
if (ImGui::Begin("##BuffBar", nullptr, flags)) {
|
|
|
|
|
int shown = 0;
|
|
|
|
|
for (size_t i = 0; i < auras.size() && shown < 16; ++i) {
|
|
|
|
|
const auto& aura = auras[i];
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
2026-02-07 23:47:43 -08:00
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
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
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
|
|
2026-02-07 23:47:43 -08:00
|
|
|
bool isBuff = (aura.flags & 0x02) != 0;
|
|
|
|
|
ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f);
|
|
|
|
|
|
|
|
|
|
// Try to get spell icon
|
|
|
|
|
GLuint iconTex = 0;
|
|
|
|
|
if (assetMgr) {
|
|
|
|
|
iconTex = getSpellIcon(aura.spellId, assetMgr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
2026-02-08 00:00:12 -08:00
|
|
|
ImGui::ImageButton("##aura",
|
2026-02-07 23:47:43 -08:00
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
ImVec2(ICON_SIZE - 4, ICON_SIZE - 4));
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
|
|
|
|
char label[8];
|
|
|
|
|
snprintf(label, sizeof(label), "%u", aura.spellId);
|
2026-02-08 00:00:12 -08:00
|
|
|
ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE));
|
2026-02-07 23:47:43 -08:00
|
|
|
ImGui::PopStyleColor();
|
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-08 00:00:12 -08:00
|
|
|
// Right-click to cancel buffs / dismount
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && isBuff) {
|
|
|
|
|
if (gameHandler.isMounted()) {
|
|
|
|
|
gameHandler.dismount();
|
|
|
|
|
} else {
|
|
|
|
|
gameHandler.cancelAura(aura.spellId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 23:47:43 -08:00
|
|
|
// Tooltip with spell name and duration
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
std::string name;
|
|
|
|
|
auto it = actionSpellNames.find(aura.spellId);
|
|
|
|
|
if (it != actionSpellNames.end()) {
|
|
|
|
|
name = it->second;
|
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
|
|
|
} else {
|
2026-02-07 23:47:43 -08:00
|
|
|
name = "Spell #" + std::to_string(aura.spellId);
|
|
|
|
|
}
|
|
|
|
|
if (aura.durationMs > 0) {
|
|
|
|
|
int seconds = aura.durationMs / 1000;
|
|
|
|
|
if (seconds < 60) {
|
|
|
|
|
ImGui::SetTooltip("%s (%ds)", name.c_str(), seconds);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::SetTooltip("%s (%dm %ds)", name.c_str(), seconds / 60, seconds % 60);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::SetTooltip("%s", name.c_str());
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
shown++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
2026-02-07 23:47:43 -08:00
|
|
|
ImGui::PopStyleVar();
|
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
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Loot Window (Phase 5)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isLootWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Appearing);
|
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
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
const auto& loot = gameHandler.getCurrentLoot();
|
|
|
|
|
|
|
|
|
|
// Gold
|
|
|
|
|
if (loot.gold > 0) {
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%ug %us %uc",
|
|
|
|
|
loot.getGold(), loot.getSilver(), loot.getCopper());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 15:41:29 -08:00
|
|
|
// Items with icons and labels
|
|
|
|
|
constexpr float iconSize = 32.0f;
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
int lootSlotClicked = -1; // defer loot pickup to avoid iterator invalidation
|
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
|
|
|
for (const auto& item : loot.items) {
|
|
|
|
|
ImGui::PushID(item.slotIndex);
|
2026-02-06 15:41:29 -08:00
|
|
|
|
|
|
|
|
// Get item info for name and quality
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(item.itemId);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
std::string itemName;
|
|
|
|
|
game::ItemQuality quality = game::ItemQuality::COMMON;
|
|
|
|
|
if (info && !info->name.empty()) {
|
|
|
|
|
itemName = info->name;
|
|
|
|
|
quality = static_cast<game::ItemQuality>(info->quality);
|
|
|
|
|
} else {
|
2026-02-07 00:00:06 -08:00
|
|
|
itemName = "Item #" + std::to_string(item.itemId);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
2026-02-06 15:41:29 -08:00
|
|
|
ImVec4 qColor = InventoryScreen::getQualityColor(quality);
|
|
|
|
|
|
|
|
|
|
// Get item icon
|
|
|
|
|
uint32_t displayId = item.displayInfoId;
|
|
|
|
|
if (displayId == 0 && info) displayId = info->displayInfoId;
|
|
|
|
|
GLuint iconTex = inventoryScreen.getItemIcon(displayId);
|
|
|
|
|
|
|
|
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
|
|
|
float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f);
|
|
|
|
|
|
|
|
|
|
// Invisible selectable for click handling
|
|
|
|
|
if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) {
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
lootSlotClicked = item.slotIndex;
|
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-06 18:34:45 -08:00
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
lootSlotClicked = item.slotIndex;
|
|
|
|
|
}
|
2026-02-06 15:41:29 -08:00
|
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
// Draw hover highlight
|
|
|
|
|
if (hovered) {
|
|
|
|
|
drawList->AddRectFilled(cursor,
|
|
|
|
|
ImVec2(cursor.x + ImGui::GetContentRegionAvail().x + iconSize + 8.0f,
|
|
|
|
|
cursor.y + rowH),
|
|
|
|
|
IM_COL32(255, 255, 255, 30));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw icon
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
drawList->AddImage((ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize));
|
|
|
|
|
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qColor));
|
|
|
|
|
} else {
|
|
|
|
|
drawList->AddRectFilled(cursor,
|
|
|
|
|
ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
|
|
|
|
IM_COL32(40, 40, 50, 200));
|
|
|
|
|
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
|
|
|
|
IM_COL32(80, 80, 80, 200));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw item name
|
|
|
|
|
float textX = cursor.x + iconSize + 6.0f;
|
|
|
|
|
float textY = cursor.y + 2.0f;
|
|
|
|
|
drawList->AddText(ImVec2(textX, textY),
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str());
|
|
|
|
|
|
|
|
|
|
// Draw count if > 1
|
|
|
|
|
if (item.count > 1) {
|
|
|
|
|
char countStr[32];
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "x%u", item.count);
|
|
|
|
|
float countY = textY + ImGui::GetTextLineHeight();
|
|
|
|
|
drawList->AddText(ImVec2(textX, countY), IM_COL32(200, 200, 200, 220), countStr);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
// Process deferred loot pickup (after loop to avoid iterator invalidation)
|
|
|
|
|
if (lootSlotClicked >= 0) {
|
|
|
|
|
gameHandler.lootItem(static_cast<uint8_t>(lootSlotClicked));
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
if (loot.items.empty() && loot.gold == 0) {
|
2026-02-06 18:34:45 -08:00
|
|
|
gameHandler.closeLoot();
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Gossip Window (Phase 5)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isGossipWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
|
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
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
if (ImGui::Begin("NPC Dialog", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
const auto& gossip = gameHandler.getCurrentGossip();
|
|
|
|
|
|
|
|
|
|
// NPC name (from creature cache)
|
|
|
|
|
auto npcEntity = gameHandler.getEntityManager().getEntity(gossip.npcGuid);
|
|
|
|
|
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
|
|
|
|
if (!unit->getName().empty()) {
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Gossip options
|
|
|
|
|
static const char* gossipIcons[] = {"[Chat]", "[Vendor]", "[Taxi]", "[Trainer]", "[Spiritguide]",
|
|
|
|
|
"[Tabardvendor]", "[Battlemaster]", "[Banker]", "[Petitioner]",
|
|
|
|
|
"[Tabarddesigner]", "[Auctioneer]"};
|
|
|
|
|
|
|
|
|
|
for (const auto& opt : gossip.options) {
|
|
|
|
|
ImGui::PushID(static_cast<int>(opt.id));
|
|
|
|
|
const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedText = replaceGenderPlaceholders(opt.text, 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
|
|
|
char label[256];
|
2026-02-09 17:39:21 -08:00
|
|
|
snprintf(label, sizeof(label), "%s %s", icon, processedText.c_str());
|
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
|
|
|
if (ImGui::Selectable(label)) {
|
2026-02-07 23:12:24 -08:00
|
|
|
gameHandler.selectGossipOption(opt.id);
|
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
|
|
|
}
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 21:00:05 -08:00
|
|
|
// Fallback: some spirit healers don't send gossip options.
|
2026-02-07 23:12:24 -08:00
|
|
|
if (gossip.options.empty() && gameHandler.isPlayerGhost()) {
|
2026-02-07 21:00:05 -08:00
|
|
|
bool isSpirit = false;
|
|
|
|
|
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
|
|
|
|
std::string name = unit->getName();
|
|
|
|
|
std::transform(name.begin(), name.end(), name.begin(),
|
|
|
|
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
if (name.find("spirit healer") != std::string::npos ||
|
|
|
|
|
name.find("spirit guide") != std::string::npos) {
|
|
|
|
|
isSpirit = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (isSpirit) {
|
|
|
|
|
if (ImGui::Selectable("[Spiritguide] Return to Graveyard")) {
|
|
|
|
|
gameHandler.activateSpiritHealer(gossip.npcGuid);
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Quest items
|
|
|
|
|
if (!gossip.quests.empty()) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:");
|
2026-02-06 12:08:47 -08:00
|
|
|
for (size_t qi = 0; qi < gossip.quests.size(); qi++) {
|
|
|
|
|
const auto& quest = gossip.quests[qi];
|
|
|
|
|
ImGui::PushID(static_cast<int>(qi));
|
2026-02-06 11:45:35 -08:00
|
|
|
char qlabel[256];
|
|
|
|
|
snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str());
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.3f, 1.0f));
|
|
|
|
|
if (ImGui::Selectable(qlabel)) {
|
|
|
|
|
gameHandler.selectGossipQuest(quest.questId);
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::PopID();
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Quest Details Window
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isQuestDetailsOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
const auto& quest = gameHandler.getQuestDetails();
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open)) {
|
2026-02-06 11:59:51 -08:00
|
|
|
// Quest description
|
|
|
|
|
if (!quest.details.empty()) {
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedDetails = replaceGenderPlaceholders(quest.details, gameHandler);
|
|
|
|
|
ImGui::TextWrapped("%s", processedDetails.c_str());
|
2026-02-06 11:59:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Objectives
|
|
|
|
|
if (!quest.objectives.empty()) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Objectives:");
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedObjectives = replaceGenderPlaceholders(quest.objectives, gameHandler);
|
|
|
|
|
ImGui::TextWrapped("%s", processedObjectives.c_str());
|
2026-02-06 11:59:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rewards
|
|
|
|
|
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:");
|
|
|
|
|
if (quest.rewardXp > 0) {
|
|
|
|
|
ImGui::Text(" %u experience", quest.rewardXp);
|
|
|
|
|
}
|
|
|
|
|
if (quest.rewardMoney > 0) {
|
|
|
|
|
uint32_t gold = quest.rewardMoney / 10000;
|
|
|
|
|
uint32_t silver = (quest.rewardMoney % 10000) / 100;
|
|
|
|
|
uint32_t copper = quest.rewardMoney % 100;
|
|
|
|
|
if (gold > 0) ImGui::Text(" %ug %us %uc", gold, silver, copper);
|
|
|
|
|
else if (silver > 0) ImGui::Text(" %us %uc", silver, copper);
|
|
|
|
|
else ImGui::Text(" %uc", copper);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (quest.suggestedPlayers > 1) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
|
|
|
|
|
"Suggested players: %u", quest.suggestedPlayers);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Accept / Decline buttons
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(buttonW, 0))) {
|
|
|
|
|
gameHandler.acceptQuest();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(buttonW, 0))) {
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:50:15 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Quest Request Items Window (turn-in progress check)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isQuestRequestItemsOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 350), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
const auto& quest = gameHandler.getQuestRequestItems();
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
2026-02-06 21:50:15 -08:00
|
|
|
if (!quest.completionText.empty()) {
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedCompletionText = replaceGenderPlaceholders(quest.completionText, gameHandler);
|
|
|
|
|
ImGui::TextWrapped("%s", processedCompletionText.c_str());
|
2026-02-06 21:50:15 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Required items
|
|
|
|
|
if (!quest.requiredItems.empty()) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:");
|
|
|
|
|
for (const auto& item : quest.requiredItems) {
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
if (info && info->valid)
|
|
|
|
|
ImGui::Text(" %s x%u", info->name.c_str(), item.count);
|
|
|
|
|
else
|
|
|
|
|
ImGui::Text(" Item %u x%u", item.itemId, item.count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (quest.requiredMoney > 0) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
uint32_t g = quest.requiredMoney / 10000;
|
|
|
|
|
uint32_t s = (quest.requiredMoney % 10000) / 100;
|
|
|
|
|
uint32_t c = quest.requiredMoney % 100;
|
|
|
|
|
ImGui::Text("Required money: %ug %us %uc", g, s, c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Complete / Cancel buttons
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
if (quest.isCompletable()) {
|
|
|
|
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
|
|
|
|
gameHandler.completeQuest();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::BeginDisabled();
|
|
|
|
|
ImGui::Button("Incomplete", ImVec2(buttonW, 0));
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Quest Offer Reward Window (choose reward)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isQuestOfferRewardOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
const auto& quest = gameHandler.getQuestOfferReward();
|
|
|
|
|
static int selectedChoice = -1;
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
2026-02-06 21:50:15 -08:00
|
|
|
if (!quest.rewardText.empty()) {
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string processedRewardText = replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
|
|
|
|
ImGui::TextWrapped("%s", processedRewardText.c_str());
|
2026-02-06 21:50:15 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Choice rewards (pick one)
|
|
|
|
|
if (!quest.choiceRewards.empty()) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Choose a reward:");
|
|
|
|
|
for (size_t i = 0; i < quest.choiceRewards.size(); ++i) {
|
|
|
|
|
const auto& item = quest.choiceRewards[i];
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
char label[256];
|
|
|
|
|
if (info && info->valid)
|
|
|
|
|
snprintf(label, sizeof(label), "%s x%u", info->name.c_str(), item.count);
|
|
|
|
|
else
|
|
|
|
|
snprintf(label, sizeof(label), "Item %u x%u", item.itemId, item.count);
|
|
|
|
|
|
|
|
|
|
bool selected = (selectedChoice == static_cast<int>(i));
|
|
|
|
|
if (ImGui::Selectable(label, selected)) {
|
|
|
|
|
selectedChoice = static_cast<int>(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fixed rewards (always given)
|
|
|
|
|
if (!quest.fixedRewards.empty()) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "You will also receive:");
|
|
|
|
|
for (const auto& item : quest.fixedRewards) {
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
if (info && info->valid)
|
|
|
|
|
ImGui::Text(" %s x%u", info->name.c_str(), item.count);
|
|
|
|
|
else
|
|
|
|
|
ImGui::Text(" Item %u x%u", item.itemId, item.count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Money / XP rewards
|
|
|
|
|
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:");
|
|
|
|
|
if (quest.rewardXp > 0)
|
|
|
|
|
ImGui::Text(" %u experience", quest.rewardXp);
|
|
|
|
|
if (quest.rewardMoney > 0) {
|
|
|
|
|
uint32_t g = quest.rewardMoney / 10000;
|
|
|
|
|
uint32_t s = (quest.rewardMoney % 10000) / 100;
|
|
|
|
|
uint32_t c = quest.rewardMoney % 100;
|
|
|
|
|
if (g > 0) ImGui::Text(" %ug %us %uc", g, s, c);
|
|
|
|
|
else if (s > 0) ImGui::Text(" %us %uc", s, c);
|
|
|
|
|
else ImGui::Text(" %uc", c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Complete button
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
|
|
|
|
|
bool canComplete = quest.choiceRewards.empty() || selectedChoice >= 0;
|
|
|
|
|
if (!canComplete) ImGui::BeginDisabled();
|
|
|
|
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
|
|
|
|
uint32_t rewardIdx = quest.choiceRewards.empty() ? 0 : static_cast<uint32_t>(selectedChoice);
|
|
|
|
|
gameHandler.chooseQuestReward(rewardIdx);
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
}
|
|
|
|
|
if (!canComplete) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// ============================================================
|
|
|
|
|
// Vendor Window (Phase 5)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isVendorWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
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
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
if (ImGui::Begin("Vendor", &open)) {
|
|
|
|
|
const auto& vendor = gameHandler.getVendorItems();
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
// Show player money
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
|
|
|
|
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
|
|
|
|
uint32_t mc = static_cast<uint32_t>(money % 100);
|
|
|
|
|
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
2026-02-06 15:41:29 -08:00
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
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
|
|
|
if (vendor.items.empty()) {
|
|
|
|
|
ImGui::TextDisabled("This vendor has nothing for sale.");
|
|
|
|
|
} else {
|
|
|
|
|
if (ImGui::BeginTable("VendorTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Stock", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
// Quality colors (matching WoW)
|
|
|
|
|
static const ImVec4 qualityColors[] = {
|
|
|
|
|
ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0 Poor (gray)
|
|
|
|
|
ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1 Common (white)
|
|
|
|
|
ImVec4(0.12f, 1.0f, 0.0f, 1.0f), // 2 Uncommon (green)
|
|
|
|
|
ImVec4(0.0f, 0.44f, 0.87f, 1.0f), // 3 Rare (blue)
|
|
|
|
|
ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4 Epic (purple)
|
|
|
|
|
ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5 Legendary (orange)
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
for (const auto& item : vendor.items) {
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
ImGui::PushID(static_cast<int>(item.slot));
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-06 11:59:51 -08:00
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
if (info && info->valid) {
|
|
|
|
|
uint32_t q = info->quality < 6 ? info->quality : 1;
|
|
|
|
|
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
|
|
|
|
// Tooltip with stats on hover
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
|
|
|
|
if (info->armor > 0) ImGui::Text("Armor: %d", info->armor);
|
|
|
|
|
if (info->stamina > 0) ImGui::Text("+%d Stamina", info->stamina);
|
|
|
|
|
if (info->strength > 0) ImGui::Text("+%d Strength", info->strength);
|
|
|
|
|
if (info->agility > 0) ImGui::Text("+%d Agility", info->agility);
|
|
|
|
|
if (info->intellect > 0) ImGui::Text("+%d Intellect", info->intellect);
|
|
|
|
|
if (info->spirit > 0) ImGui::Text("+%d Spirit", info->spirit);
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::Text("Item %u", item.itemId);
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
uint32_t g = item.buyPrice / 10000;
|
|
|
|
|
uint32_t s = (item.buyPrice / 100) % 100;
|
|
|
|
|
uint32_t c = item.buyPrice % 100;
|
2026-02-06 11:59:51 -08:00
|
|
|
bool canAfford = money >= item.buyPrice;
|
|
|
|
|
if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
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
|
|
|
ImGui::Text("%ug %us %uc", g, s, c);
|
2026-02-06 11:59:51 -08:00
|
|
|
if (!canAfford) ImGui::PopStyleColor();
|
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
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
if (item.maxCount < 0) {
|
|
|
|
|
ImGui::Text("Inf");
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::Text("%d", item.maxCount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
if (ImGui::SmallButton("Buy")) {
|
|
|
|
|
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
2026-02-06 11:59:51 -08:00
|
|
|
gameHandler.closeVendor();
|
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-08 14:33:39 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Trainer
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isTrainerWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
if (ImGui::Begin("Trainer", &open)) {
|
|
|
|
|
const auto& trainer = gameHandler.getTrainerSpells();
|
|
|
|
|
|
|
|
|
|
// NPC name
|
|
|
|
|
auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid);
|
|
|
|
|
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
|
|
|
|
if (!unit->getName().empty()) {
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Greeting
|
|
|
|
|
if (!trainer.greeting.empty()) {
|
|
|
|
|
ImGui::TextWrapped("%s", trainer.greeting.c_str());
|
|
|
|
|
}
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
// Player money
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
|
|
|
|
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
|
|
|
|
uint32_t mc = static_cast<uint32_t>(money % 100);
|
|
|
|
|
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
if (trainer.spells.empty()) {
|
|
|
|
|
ImGui::TextDisabled("This trainer has nothing to teach you.");
|
|
|
|
|
} else {
|
|
|
|
|
// Known spells for checking
|
|
|
|
|
const auto& knownSpells = gameHandler.getKnownSpells();
|
|
|
|
|
auto isKnown = [&](uint32_t id) {
|
2026-02-08 15:03:43 -08:00
|
|
|
if (id == 0) return true;
|
2026-02-09 22:13:31 -08:00
|
|
|
bool found = std::find(knownSpells.begin(), knownSpells.end(), id) != knownSpells.end();
|
|
|
|
|
static int debugCount = 0;
|
|
|
|
|
if (debugCount < 5 && !found && id != 0) {
|
|
|
|
|
LOG_INFO("isKnown(", id, ") = false, knownSpells.size()=", knownSpells.size());
|
|
|
|
|
debugCount++;
|
|
|
|
|
}
|
|
|
|
|
return found;
|
2026-02-08 14:33:39 -08:00
|
|
|
};
|
2026-02-08 15:03:43 -08:00
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
2026-02-08 14:33:39 -08:00
|
|
|
|
2026-02-08 14:46:01 -08:00
|
|
|
// Renders spell rows into the current table
|
|
|
|
|
auto renderSpellRows = [&](const std::vector<const game::TrainerSpell*>& spells) {
|
|
|
|
|
for (const auto* spell : spells) {
|
2026-02-08 14:33:39 -08:00
|
|
|
ImGui::TableNextRow();
|
2026-02-08 14:46:01 -08:00
|
|
|
ImGui::PushID(static_cast<int>(spell->spellId));
|
2026-02-08 14:33:39 -08:00
|
|
|
|
2026-02-08 15:03:43 -08:00
|
|
|
// Check prerequisites client-side
|
2026-02-09 22:13:31 -08:00
|
|
|
bool prereq1Met = isKnown(spell->chainNode1);
|
|
|
|
|
bool prereq2Met = isKnown(spell->chainNode2);
|
|
|
|
|
bool prereq3Met = isKnown(spell->chainNode3);
|
|
|
|
|
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
|
2026-02-08 15:03:43 -08:00
|
|
|
bool levelMet = (spell->reqLevel == 0 || playerLevel >= spell->reqLevel);
|
2026-02-09 22:13:31 -08:00
|
|
|
bool alreadyKnown = isKnown(spell->spellId);
|
2026-02-08 15:03:43 -08:00
|
|
|
|
2026-02-08 14:33:39 -08:00
|
|
|
ImVec4 color;
|
|
|
|
|
const char* statusLabel;
|
2026-02-08 15:03:43 -08:00
|
|
|
if (alreadyKnown) {
|
2026-02-08 14:33:39 -08:00
|
|
|
color = ImVec4(0.3f, 0.9f, 0.3f, 1.0f);
|
|
|
|
|
statusLabel = "Known";
|
2026-02-08 15:03:43 -08:00
|
|
|
} else if (spell->state == 1 && prereqsMet && levelMet) {
|
2026-02-08 14:33:39 -08:00
|
|
|
color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
statusLabel = "Available";
|
|
|
|
|
} else {
|
|
|
|
|
color = ImVec4(0.6f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
statusLabel = "Unavailable";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spell name
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-08 14:46:01 -08:00
|
|
|
const std::string& name = gameHandler.getSpellName(spell->spellId);
|
|
|
|
|
const std::string& rank = gameHandler.getSpellRank(spell->spellId);
|
2026-02-08 14:33:39 -08:00
|
|
|
if (!name.empty()) {
|
2026-02-08 14:46:01 -08:00
|
|
|
if (!rank.empty())
|
2026-02-08 14:33:39 -08:00
|
|
|
ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str());
|
2026-02-08 14:46:01 -08:00
|
|
|
else
|
2026-02-08 14:33:39 -08:00
|
|
|
ImGui::TextColored(color, "%s", name.c_str());
|
|
|
|
|
} else {
|
2026-02-08 14:46:01 -08:00
|
|
|
ImGui::TextColored(color, "Spell #%u", spell->spellId);
|
2026-02-08 14:33:39 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
if (!name.empty()) {
|
|
|
|
|
ImGui::Text("%s", name.c_str());
|
|
|
|
|
if (!rank.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", rank.c_str());
|
|
|
|
|
}
|
|
|
|
|
ImGui::Text("Status: %s", statusLabel);
|
2026-02-08 15:03:43 -08:00
|
|
|
if (spell->reqLevel > 0) {
|
|
|
|
|
ImVec4 lvlColor = levelMet ? ImVec4(0.7f, 0.7f, 0.7f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
ImGui::TextColored(lvlColor, "Required Level: %u", spell->reqLevel);
|
2026-02-08 14:33:39 -08:00
|
|
|
}
|
2026-02-08 15:03:43 -08:00
|
|
|
if (spell->reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell->reqSkill, spell->reqSkillValue);
|
|
|
|
|
auto showPrereq = [&](uint32_t node) {
|
|
|
|
|
if (node == 0) return;
|
|
|
|
|
bool met = isKnown(node);
|
|
|
|
|
const std::string& pname = gameHandler.getSpellName(node);
|
|
|
|
|
ImVec4 pcolor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
if (!pname.empty())
|
|
|
|
|
ImGui::TextColored(pcolor, "Requires: %s%s", pname.c_str(), met ? " (known)" : "");
|
|
|
|
|
else
|
|
|
|
|
ImGui::TextColored(pcolor, "Requires: Spell #%u%s", node, met ? " (known)" : "");
|
|
|
|
|
};
|
|
|
|
|
showPrereq(spell->chainNode1);
|
|
|
|
|
showPrereq(spell->chainNode2);
|
|
|
|
|
showPrereq(spell->chainNode3);
|
2026-02-08 14:33:39 -08:00
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Level
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
2026-02-08 14:46:01 -08:00
|
|
|
ImGui::TextColored(color, "%u", spell->reqLevel);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
|
|
|
// Cost
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
2026-02-08 14:46:01 -08:00
|
|
|
if (spell->spellCost > 0) {
|
|
|
|
|
uint32_t g = spell->spellCost / 10000;
|
|
|
|
|
uint32_t s = (spell->spellCost / 100) % 100;
|
|
|
|
|
uint32_t c = spell->spellCost % 100;
|
|
|
|
|
bool canAfford = money >= spell->spellCost;
|
2026-02-08 14:33:39 -08:00
|
|
|
ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
ImGui::TextColored(costColor, "%ug %us %uc", g, s, c);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(color, "Free");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 15:03:43 -08:00
|
|
|
// Train button - only enabled if available, affordable, prereqs met
|
2026-02-08 14:33:39 -08:00
|
|
|
ImGui::TableSetColumnIndex(3);
|
2026-02-08 15:03:43 -08:00
|
|
|
bool canTrain = !alreadyKnown && spell->state == 1
|
|
|
|
|
&& prereqsMet && levelMet
|
|
|
|
|
&& (money >= spell->spellCost);
|
2026-02-09 21:59:00 -08:00
|
|
|
|
2026-02-09 22:13:31 -08:00
|
|
|
// Debug logging for first 3 spells to see why buttons are disabled
|
|
|
|
|
static int logCount = 0;
|
|
|
|
|
static uint64_t lastTrainerGuid = 0;
|
|
|
|
|
if (trainer.trainerGuid != lastTrainerGuid) {
|
|
|
|
|
logCount = 0;
|
|
|
|
|
lastTrainerGuid = trainer.trainerGuid;
|
|
|
|
|
}
|
|
|
|
|
if (logCount < 3) {
|
2026-02-09 21:59:00 -08:00
|
|
|
LOG_INFO("Trainer button debug: spellId=", spell->spellId,
|
|
|
|
|
" alreadyKnown=", alreadyKnown, " state=", (int)spell->state,
|
2026-02-09 22:13:31 -08:00
|
|
|
" prereqsMet=", prereqsMet, " (", prereq1Met, ",", prereq2Met, ",", prereq3Met, ")",
|
|
|
|
|
" levelMet=", levelMet,
|
|
|
|
|
" reqLevel=", spell->reqLevel, " playerLevel=", playerLevel,
|
|
|
|
|
" chain1=", spell->chainNode1, " chain2=", spell->chainNode2, " chain3=", spell->chainNode3,
|
2026-02-09 21:59:00 -08:00
|
|
|
" canAfford=", (money >= spell->spellCost),
|
|
|
|
|
" canTrain=", canTrain);
|
2026-02-09 22:13:31 -08:00
|
|
|
logCount++;
|
2026-02-09 21:59:00 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 14:46:01 -08:00
|
|
|
if (!canTrain) ImGui::BeginDisabled();
|
2026-02-08 14:33:39 -08:00
|
|
|
if (ImGui::SmallButton("Train")) {
|
2026-02-08 14:46:01 -08:00
|
|
|
gameHandler.trainSpell(spell->spellId);
|
2026-02-08 14:33:39 -08:00
|
|
|
}
|
2026-02-08 14:46:01 -08:00
|
|
|
if (!canTrain) ImGui::EndDisabled();
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
2026-02-08 14:46:01 -08:00
|
|
|
};
|
2026-02-08 14:33:39 -08:00
|
|
|
|
2026-02-08 14:46:01 -08:00
|
|
|
auto renderSpellTable = [&](const char* tableId, const std::vector<const game::TrainerSpell*>& spells) {
|
|
|
|
|
if (ImGui::BeginTable(tableId, 4,
|
|
|
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
ImGui::TableSetupColumn("Spell", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
renderSpellRows(spells);
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const auto& tabs = gameHandler.getTrainerTabs();
|
|
|
|
|
if (tabs.size() > 1) {
|
|
|
|
|
// Multiple tabs - show tab bar
|
|
|
|
|
if (ImGui::BeginTabBar("TrainerTabs")) {
|
|
|
|
|
for (size_t i = 0; i < tabs.size(); i++) {
|
|
|
|
|
char tabLabel[64];
|
|
|
|
|
snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)",
|
|
|
|
|
tabs[i].name.c_str(), tabs[i].spells.size());
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginTabItem(tabLabel)) {
|
|
|
|
|
char tableId[32];
|
|
|
|
|
snprintf(tableId, sizeof(tableId), "TT%zu", i);
|
|
|
|
|
renderSpellTable(tableId, tabs[i].spells);
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndTabBar();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Single tab or no categorization - flat list
|
|
|
|
|
std::vector<const game::TrainerSpell*> allSpells;
|
|
|
|
|
for (const auto& spell : trainer.spells) {
|
|
|
|
|
allSpells.push_back(&spell);
|
|
|
|
|
}
|
|
|
|
|
renderSpellTable("TrainerTable", allSpells);
|
2026-02-08 14:33:39 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 18:27:52 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Teleporter Panel
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-05 16:01:38 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Escape Menu
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderEscapeMenu() {
|
|
|
|
|
if (!showEscapeMenu) return;
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
|
|
|
|
ImVec2 size(260.0f, 220.0f);
|
|
|
|
|
ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f);
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##EscapeMenu", nullptr, flags)) {
|
|
|
|
|
ImGui::Text("Game Menu");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Logout", ImVec2(-1, 0))) {
|
|
|
|
|
core::Application::getInstance().logoutToLogin();
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
showEscapeSettingsNotice = false;
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::Button("Quit", ImVec2(-1, 0))) {
|
2026-02-05 16:17:04 -08:00
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
|
|
|
|
music->stopMusic(0.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 16:01:38 -08:00
|
|
|
core::Application::getInstance().shutdown();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::Button("Settings", ImVec2(-1, 0))) {
|
2026-02-05 16:11:00 -08:00
|
|
|
showEscapeSettingsNotice = false;
|
|
|
|
|
showSettingsWindow = true;
|
|
|
|
|
settingsInit = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
showEscapeMenu = false;
|
2026-02-05 16:01:38 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 16:21:17 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
|
|
|
|
if (ImGui::Button("Back to Game", ImVec2(-1, 0))) {
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
showEscapeSettingsNotice = false;
|
2026-02-05 16:01:38 -08:00
|
|
|
}
|
2026-02-05 16:21:17 -08:00
|
|
|
ImGui::PopStyleVar();
|
2026-02-05 16:01:38 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 16:59:20 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Taxi Window
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.isTaxiWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
if (ImGui::Begin("Flight Master", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
const auto& taxiData = gameHandler.getTaxiData();
|
|
|
|
|
const auto& nodes = gameHandler.getTaxiNodes();
|
|
|
|
|
uint32_t currentNode = gameHandler.getTaxiCurrentNode();
|
|
|
|
|
|
|
|
|
|
// Get current node's map to filter destinations
|
|
|
|
|
uint32_t currentMapId = 0;
|
|
|
|
|
auto curIt = nodes.find(currentNode);
|
|
|
|
|
if (curIt != nodes.end()) {
|
|
|
|
|
currentMapId = curIt->second.mapId;
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Current: %s", curIt->second.name.c_str());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Select a destination:");
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
2026-02-07 20:02:14 -08:00
|
|
|
static uint32_t selectedNodeId = 0;
|
2026-02-07 16:59:20 -08:00
|
|
|
int destCount = 0;
|
2026-02-07 20:02:14 -08:00
|
|
|
if (ImGui::BeginTable("TaxiNodes", 3, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
ImGui::TableSetupColumn("Destination", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
for (const auto& [nodeId, node] : nodes) {
|
|
|
|
|
if (nodeId == currentNode) continue;
|
|
|
|
|
if (node.mapId != currentMapId) continue;
|
|
|
|
|
if (!taxiData.isNodeKnown(nodeId)) continue;
|
|
|
|
|
|
|
|
|
|
uint32_t costCopper = gameHandler.getTaxiCostTo(nodeId);
|
|
|
|
|
uint32_t gold = costCopper / 10000;
|
|
|
|
|
uint32_t silver = (costCopper / 100) % 100;
|
|
|
|
|
uint32_t copper = costCopper % 100;
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(nodeId));
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
bool isSelected = (selectedNodeId == nodeId);
|
2026-02-08 03:05:38 -08:00
|
|
|
if (ImGui::Selectable(node.name.c_str(), isSelected,
|
|
|
|
|
ImGuiSelectableFlags_SpanAllColumns |
|
|
|
|
|
ImGuiSelectableFlags_AllowDoubleClick)) {
|
2026-02-07 20:02:14 -08:00
|
|
|
selectedNodeId = nodeId;
|
2026-02-08 03:05:38 -08:00
|
|
|
LOG_INFO("Taxi UI: Selected dest=", nodeId);
|
|
|
|
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
LOG_INFO("Taxi UI: Double-click activate dest=", nodeId);
|
|
|
|
|
gameHandler.activateTaxi(nodeId);
|
|
|
|
|
}
|
2026-02-07 20:02:14 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
if (gold > 0) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper);
|
|
|
|
|
} else if (silver > 0) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%us %uc", silver, copper);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.2f, 1.0f), "%uc", copper);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
if (ImGui::SmallButton("Fly")) {
|
|
|
|
|
selectedNodeId = nodeId;
|
|
|
|
|
LOG_INFO("Taxi UI: Fly clicked dest=", nodeId);
|
|
|
|
|
gameHandler.activateTaxi(nodeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
destCount++;
|
2026-02-07 16:59:20 -08:00
|
|
|
}
|
2026-02-07 20:02:14 -08:00
|
|
|
ImGui::EndTable();
|
2026-02-07 16:59:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (destCount == 0) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No destinations available.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
2026-02-08 03:05:38 -08:00
|
|
|
if (selectedNodeId != 0 && ImGui::Button("Fly Selected", ImVec2(-1, 0))) {
|
|
|
|
|
LOG_INFO("Taxi UI: Fly Selected dest=", selectedNodeId);
|
|
|
|
|
gameHandler.activateTaxi(selectedNodeId);
|
|
|
|
|
}
|
2026-02-07 16:59:20 -08:00
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
gameHandler.closeTaxi();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
gameHandler.closeTaxi();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 17:27:20 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Death Screen
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
|
2026-02-07 23:12:24 -08:00
|
|
|
if (!gameHandler.showDeathDialog()) return;
|
2026-02-06 17:27:20 -08:00
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
// Dark red overlay covering the whole screen
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(screenW, screenH));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.0f, 0.0f, 0.45f));
|
|
|
|
|
ImGui::Begin("##DeathOverlay", nullptr,
|
|
|
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs |
|
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing);
|
|
|
|
|
ImGui::End();
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
// "Release Spirit" dialog centered on screen
|
|
|
|
|
float dlgW = 280.0f;
|
|
|
|
|
float dlgH = 100.0f;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.0f, 0.0f, 0.9f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.6f, 0.1f, 0.1f, 1.0f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##DeathDialog", nullptr,
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
// Center "You are dead." text
|
|
|
|
|
const char* deathText = "You are dead.";
|
|
|
|
|
float textW = ImGui::CalcTextSize(deathText).x;
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - textW) / 2);
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText);
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Center the Release Spirit button
|
|
|
|
|
float btnW = 180.0f;
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW) / 2);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
if (ImGui::Button("Release Spirit", ImVec2(btnW, 30))) {
|
|
|
|
|
gameHandler.releaseSpirit();
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-02-07 23:12:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) {
|
|
|
|
|
if (!gameHandler.showResurrectDialog()) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
float dlgW = 300.0f;
|
|
|
|
|
float dlgH = 110.0f;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.8f, 1.0f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##ResurrectDialog", nullptr,
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
const char* text = "Return to life?";
|
|
|
|
|
float textW = ImGui::CalcTextSize(text).x;
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - textW) / 2);
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text);
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
float btnW = 100.0f;
|
|
|
|
|
float spacing = 20.0f;
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2);
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(btnW, 30))) {
|
|
|
|
|
gameHandler.acceptResurrect();
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(0, spacing);
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(btnW, 30))) {
|
|
|
|
|
gameHandler.declineResurrect();
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-02-06 17:27:20 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 16:11:00 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Settings Window
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderSettingsWindow() {
|
|
|
|
|
if (!showSettingsWindow) return;
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-02-05 16:14:11 -08:00
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
2026-02-05 16:11:00 -08:00
|
|
|
if (!window) return;
|
|
|
|
|
|
|
|
|
|
static const int kResolutions[][2] = {
|
|
|
|
|
{1280, 720},
|
|
|
|
|
{1600, 900},
|
|
|
|
|
{1920, 1080},
|
|
|
|
|
{2560, 1440},
|
|
|
|
|
{3840, 2160},
|
|
|
|
|
};
|
|
|
|
|
static const int kResCount = sizeof(kResolutions) / sizeof(kResolutions[0]);
|
2026-02-05 17:40:15 -08:00
|
|
|
constexpr int kDefaultResW = 1920;
|
|
|
|
|
constexpr int kDefaultResH = 1080;
|
|
|
|
|
constexpr bool kDefaultFullscreen = false;
|
|
|
|
|
constexpr bool kDefaultVsync = true;
|
2026-02-07 16:00:57 -08:00
|
|
|
constexpr bool kDefaultShadows = false;
|
2026-02-05 17:40:15 -08:00
|
|
|
constexpr int kDefaultMusicVolume = 30;
|
|
|
|
|
constexpr int kDefaultSfxVolume = 100;
|
|
|
|
|
constexpr float kDefaultMouseSensitivity = 0.2f;
|
|
|
|
|
constexpr bool kDefaultInvertMouse = false;
|
|
|
|
|
|
|
|
|
|
int defaultResIndex = 0;
|
|
|
|
|
for (int i = 0; i < kResCount; i++) {
|
|
|
|
|
if (kResolutions[i][0] == kDefaultResW && kResolutions[i][1] == kDefaultResH) {
|
|
|
|
|
defaultResIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
|
|
|
if (!settingsInit) {
|
|
|
|
|
pendingFullscreen = window->isFullscreen();
|
|
|
|
|
pendingVsync = window->isVsyncEnabled();
|
2026-02-05 16:14:11 -08:00
|
|
|
pendingShadows = renderer ? renderer->areShadowsEnabled() : true;
|
2026-02-05 17:32:21 -08:00
|
|
|
if (renderer) {
|
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
|
|
|
// Load volumes from all audio managers
|
2026-02-05 17:32:21 -08:00
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
|
|
|
|
pendingMusicVolume = music->getVolume();
|
|
|
|
|
}
|
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
|
|
|
if (auto* ambient = renderer->getAmbientSoundManager()) {
|
|
|
|
|
pendingAmbientVolume = static_cast<int>(ambient->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* ui = renderer->getUiSoundManager()) {
|
|
|
|
|
pendingUiVolume = static_cast<int>(ui->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* combat = renderer->getCombatSoundManager()) {
|
|
|
|
|
pendingCombatVolume = static_cast<int>(combat->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* spell = renderer->getSpellSoundManager()) {
|
|
|
|
|
pendingSpellVolume = static_cast<int>(spell->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* movement = renderer->getMovementSoundManager()) {
|
|
|
|
|
pendingMovementVolume = static_cast<int>(movement->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
2026-02-05 17:32:21 -08:00
|
|
|
if (auto* footstep = renderer->getFootstepManager()) {
|
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
|
|
|
pendingFootstepVolume = static_cast<int>(footstep->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* npcVoice = renderer->getNpcVoiceManager()) {
|
|
|
|
|
pendingNpcVoiceVolume = static_cast<int>(npcVoice->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* mount = renderer->getMountSoundManager()) {
|
|
|
|
|
pendingMountVolume = static_cast<int>(mount->getVolumeScale() * 100.0f + 0.5f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* activity = renderer->getActivitySoundManager()) {
|
|
|
|
|
pendingActivityVolume = static_cast<int>(activity->getVolumeScale() * 100.0f + 0.5f);
|
2026-02-05 17:32:21 -08:00
|
|
|
}
|
2026-02-05 17:40:15 -08:00
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
pendingMouseSensitivity = cameraController->getMouseSensitivity();
|
|
|
|
|
pendingInvertMouse = cameraController->isInvertMouse();
|
|
|
|
|
}
|
2026-02-05 17:32:21 -08:00
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
pendingResIndex = 0;
|
|
|
|
|
int curW = window->getWidth();
|
|
|
|
|
int curH = window->getHeight();
|
|
|
|
|
for (int i = 0; i < kResCount; i++) {
|
|
|
|
|
if (kResolutions[i][0] == curW && kResolutions[i][1] == curH) {
|
|
|
|
|
pendingResIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 20:19:39 -08:00
|
|
|
pendingUiOpacity = static_cast<int>(uiOpacity_ * 100.0f + 0.5f);
|
2026-02-07 20:51:53 -08:00
|
|
|
pendingMinimapRotate = minimapRotate_;
|
2026-02-09 17:39:21 -08:00
|
|
|
pendingMinimapSquare = minimapSquare_;
|
2026-02-07 20:51:53 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->setRotateWithCamera(minimapRotate_);
|
2026-02-09 17:39:21 -08:00
|
|
|
minimap->setSquareShape(minimapSquare_);
|
2026-02-07 20:51:53 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
settingsInit = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
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
|
|
|
ImVec2 size(520.0f, std::min(screenH * 0.9f, 720.0f));
|
2026-02-05 16:11:00 -08:00
|
|
|
ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f);
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##SettingsWindow", nullptr, flags)) {
|
|
|
|
|
ImGui::Text("Settings");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
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
|
|
|
if (ImGui::BeginTabBar("SettingsTabs", ImGuiTabBarFlags_None)) {
|
|
|
|
|
// ============================================================
|
|
|
|
|
// VIDEO TAB
|
|
|
|
|
// ============================================================
|
|
|
|
|
if (ImGui::BeginTabItem("Video")) {
|
|
|
|
|
ImGui::Spacing();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
|
|
|
|
if (ImGui::Checkbox("Fullscreen", &pendingFullscreen)) {
|
|
|
|
|
window->setFullscreen(pendingFullscreen);
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::Checkbox("VSync", &pendingVsync)) {
|
|
|
|
|
window->setVsync(pendingVsync);
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::Checkbox("Shadows", &pendingShadows)) {
|
|
|
|
|
if (renderer) renderer->setShadowsEnabled(pendingShadows);
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
const char* resLabel = "Resolution";
|
|
|
|
|
const char* resItems[kResCount];
|
|
|
|
|
char resBuf[kResCount][16];
|
|
|
|
|
for (int i = 0; i < kResCount; i++) {
|
|
|
|
|
snprintf(resBuf[i], sizeof(resBuf[i]), "%dx%d", kResolutions[i][0], kResolutions[i][1]);
|
|
|
|
|
resItems[i] = resBuf[i];
|
|
|
|
|
}
|
2026-02-09 17:12:35 -08:00
|
|
|
if (ImGui::Combo(resLabel, &pendingResIndex, resItems, kResCount)) {
|
|
|
|
|
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
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
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
2026-02-05 16:11:00 -08:00
|
|
|
|
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
|
|
|
if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
pendingFullscreen = kDefaultFullscreen;
|
|
|
|
|
pendingVsync = kDefaultVsync;
|
|
|
|
|
pendingShadows = kDefaultShadows;
|
|
|
|
|
pendingResIndex = defaultResIndex;
|
2026-02-09 17:12:35 -08:00
|
|
|
window->setFullscreen(pendingFullscreen);
|
|
|
|
|
window->setVsync(pendingVsync);
|
|
|
|
|
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
|
|
|
|
if (renderer) renderer->setShadowsEnabled(pendingShadows);
|
|
|
|
|
saveSettings();
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// AUDIO TAB
|
|
|
|
|
// ============================================================
|
|
|
|
|
if (ImGui::BeginTabItem("Audio")) {
|
|
|
|
|
ImGui::Spacing();
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true);
|
|
|
|
|
|
|
|
|
|
// Helper lambda to apply audio settings
|
|
|
|
|
auto applyAudioSettings = [&]() {
|
|
|
|
|
if (!renderer) return;
|
|
|
|
|
float masterScale = static_cast<float>(pendingMasterVolume) / 100.0f;
|
|
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
|
|
|
|
music->setVolume(static_cast<int>(pendingMusicVolume * masterScale));
|
|
|
|
|
}
|
|
|
|
|
if (auto* ambient = renderer->getAmbientSoundManager()) {
|
|
|
|
|
ambient->setVolumeScale(pendingAmbientVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* ui = renderer->getUiSoundManager()) {
|
|
|
|
|
ui->setVolumeScale(pendingUiVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* combat = renderer->getCombatSoundManager()) {
|
|
|
|
|
combat->setVolumeScale(pendingCombatVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* spell = renderer->getSpellSoundManager()) {
|
|
|
|
|
spell->setVolumeScale(pendingSpellVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* movement = renderer->getMovementSoundManager()) {
|
|
|
|
|
movement->setVolumeScale(pendingMovementVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* footstep = renderer->getFootstepManager()) {
|
|
|
|
|
footstep->setVolumeScale(pendingFootstepVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* npcVoice = renderer->getNpcVoiceManager()) {
|
|
|
|
|
npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* mount = renderer->getMountSoundManager()) {
|
|
|
|
|
mount->setVolumeScale(pendingMountVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
if (auto* activity = renderer->getActivitySoundManager()) {
|
|
|
|
|
activity->setVolumeScale(pendingActivityVolume / 100.0f * masterScale);
|
|
|
|
|
}
|
|
|
|
|
saveSettings();
|
|
|
|
|
};
|
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
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Text("Master Volume");
|
|
|
|
|
if (ImGui::SliderInt("##MasterVolume", &pendingMasterVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::Separator();
|
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
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Text("Music");
|
|
|
|
|
if (ImGui::SliderInt("##MusicVolume", &pendingMusicVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
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
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Ambient Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##AmbientVolume", &pendingAmbientVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextWrapped("Weather, zones, cities, emitters");
|
2026-02-05 17:32:21 -08:00
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("UI Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##UiVolume", &pendingUiVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextWrapped("Buttons, loot, quest complete");
|
2026-02-05 17:40:15 -08:00
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Combat Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##CombatVolume", &pendingCombatVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextWrapped("Weapon swings, impacts, grunts");
|
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
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Spell Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##SpellVolume", &pendingSpellVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextWrapped("Magic casting and impacts");
|
2026-02-05 17:40:15 -08:00
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Movement Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##MovementVolume", &pendingMovementVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextWrapped("Water splashes, jump/land");
|
2026-02-05 17:32:21 -08:00
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Footsteps");
|
|
|
|
|
if (ImGui::SliderInt("##FootstepVolume", &pendingFootstepVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
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
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("NPC Voices");
|
|
|
|
|
if (ImGui::SliderInt("##NpcVoiceVolume", &pendingNpcVoiceVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
2026-02-05 17:51:14 -08:00
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Mount Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##MountVolume", &pendingMountVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
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
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Text("Activity Sounds");
|
|
|
|
|
if (ImGui::SliderInt("##ActivityVolume", &pendingActivityVolume, 0, 100, "%d%%")) {
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextWrapped("Swimming, eating, drinking");
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Restore Audio Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
pendingMasterVolume = 100;
|
|
|
|
|
pendingMusicVolume = kDefaultMusicVolume;
|
|
|
|
|
pendingAmbientVolume = 100;
|
|
|
|
|
pendingUiVolume = 100;
|
|
|
|
|
pendingCombatVolume = 100;
|
|
|
|
|
pendingSpellVolume = 100;
|
|
|
|
|
pendingMovementVolume = 100;
|
|
|
|
|
pendingFootstepVolume = 100;
|
|
|
|
|
pendingNpcVoiceVolume = 100;
|
|
|
|
|
pendingMountVolume = 100;
|
|
|
|
|
pendingActivityVolume = 100;
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// GAMEPLAY TAB
|
|
|
|
|
// ============================================================
|
|
|
|
|
if (ImGui::BeginTabItem("Gameplay")) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Controls");
|
|
|
|
|
ImGui::Separator();
|
2026-02-09 17:12:35 -08:00
|
|
|
if (ImGui::SliderFloat("Mouse Sensitivity", &pendingMouseSensitivity, 0.05f, 1.0f, "%.2f")) {
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
cameraController->setMouseSensitivity(pendingMouseSensitivity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::Checkbox("Invert Mouse", &pendingInvertMouse)) {
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
cameraController->setInvertMouse(pendingInvertMouse);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Interface");
|
|
|
|
|
ImGui::Separator();
|
2026-02-09 17:12:35 -08:00
|
|
|
if (ImGui::SliderInt("UI Opacity", &pendingUiOpacity, 20, 100, "%d%%")) {
|
|
|
|
|
uiOpacity_ = static_cast<float>(pendingUiOpacity) / 100.0f;
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) {
|
|
|
|
|
minimapRotate_ = pendingMinimapRotate;
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->setRotateWithCamera(minimapRotate_);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
if (ImGui::Checkbox("Square Minimap", &pendingMinimapSquare)) {
|
|
|
|
|
minimapSquare_ = pendingMinimapSquare;
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->setSquareShape(minimapSquare_);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
// Zoom controls
|
|
|
|
|
ImGui::Text("Minimap Zoom:");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button(" - ")) {
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->zoomOut();
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button(" + ")) {
|
|
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->zoomIn();
|
|
|
|
|
saveSettings();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Restore Gameplay Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
pendingMouseSensitivity = kDefaultMouseSensitivity;
|
|
|
|
|
pendingInvertMouse = kDefaultInvertMouse;
|
|
|
|
|
pendingUiOpacity = 65;
|
|
|
|
|
pendingMinimapRotate = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
pendingMinimapSquare = false;
|
2026-02-09 17:12:35 -08:00
|
|
|
uiOpacity_ = 0.65f;
|
|
|
|
|
minimapRotate_ = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
minimapSquare_ = false;
|
2026-02-09 17:12:35 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
cameraController->setMouseSensitivity(pendingMouseSensitivity);
|
|
|
|
|
cameraController->setInvertMouse(pendingInvertMouse);
|
|
|
|
|
}
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
minimap->setRotateWithCamera(minimapRotate_);
|
2026-02-09 17:39:21 -08:00
|
|
|
minimap->setSquareShape(minimapSquare_);
|
2026-02-09 17:12:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
saveSettings();
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabBar();
|
2026-02-06 20:19:39 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 16:16:03 -08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
|
|
|
|
if (ImGui::Button("Back to Game", ImVec2(-1, 0))) {
|
2026-02-05 16:11:00 -08:00
|
|
|
showSettingsWindow = false;
|
|
|
|
|
}
|
2026-02-05 16:16:03 -08:00
|
|
|
ImGui::PopStyleVar();
|
2026-02-05 16:11:00 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:10:10 -08:00
|
|
|
void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
|
|
|
|
if (statuses.empty()) return;
|
|
|
|
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
if (!camera || !window) return;
|
|
|
|
|
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
glm::mat4 viewProj = camera->getViewProjectionMatrix();
|
|
|
|
|
auto* drawList = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
for (const auto& [guid, status] : statuses) {
|
|
|
|
|
// Only show markers for available (!) and reward/completable (?)
|
|
|
|
|
const char* marker = nullptr;
|
|
|
|
|
ImU32 color = IM_COL32(255, 210, 0, 255); // yellow
|
|
|
|
|
if (status == game::QuestGiverStatus::AVAILABLE) {
|
|
|
|
|
marker = "!";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::AVAILABLE_LOW) {
|
|
|
|
|
marker = "!";
|
|
|
|
|
color = IM_COL32(160, 160, 160, 255); // gray
|
|
|
|
|
} else if (status == game::QuestGiverStatus::REWARD) {
|
|
|
|
|
marker = "?";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
|
|
|
|
marker = "?";
|
|
|
|
|
color = IM_COL32(160, 160, 160, 255); // gray
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get entity position (canonical coords)
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
|
|
|
|
if (!entity) continue;
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
|
|
|
|
|
|
|
|
|
// Get model height for offset
|
|
|
|
|
float heightOffset = 3.0f;
|
|
|
|
|
glm::vec3 boundsCenter;
|
|
|
|
|
float boundsRadius = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) {
|
|
|
|
|
heightOffset = boundsRadius * 2.0f + 1.0f;
|
|
|
|
|
}
|
|
|
|
|
renderPos.z += heightOffset;
|
|
|
|
|
|
|
|
|
|
// Project to screen
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
if (clipPos.w <= 0.0f) continue;
|
|
|
|
|
|
|
|
|
|
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
|
|
|
|
float sx = (ndc.x + 1.0f) * 0.5f * screenW;
|
|
|
|
|
float sy = (1.0f - ndc.y) * 0.5f * screenH;
|
|
|
|
|
|
|
|
|
|
// Skip if off-screen
|
|
|
|
|
if (sx < -50 || sx > screenW + 50 || sy < -50 || sy > screenH + 50) continue;
|
|
|
|
|
|
|
|
|
|
// Scale text size based on distance
|
|
|
|
|
float dist = clipPos.w;
|
|
|
|
|
float fontSize = std::clamp(800.0f / dist, 14.0f, 48.0f);
|
|
|
|
|
|
|
|
|
|
// Draw outlined text: 4 shadow copies then main text
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
ImU32 outlineColor = IM_COL32(0, 0, 0, 220);
|
|
|
|
|
float off = std::max(1.0f, fontSize * 0.06f);
|
|
|
|
|
ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, marker);
|
|
|
|
|
float tx = sx - textSize.x * 0.5f;
|
|
|
|
|
float ty = sy - textSize.y * 0.5f;
|
|
|
|
|
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx - off, ty), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx + off, ty), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty - off), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty + off), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty), color, marker);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
auto* minimap = renderer ? renderer->getMinimap() : nullptr;
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
if (!camera || !minimap || !window) return;
|
|
|
|
|
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
|
|
|
|
|
// Minimap parameters (matching minimap.cpp)
|
|
|
|
|
float mapSize = 200.0f;
|
|
|
|
|
float margin = 10.0f;
|
|
|
|
|
float mapRadius = mapSize * 0.5f;
|
|
|
|
|
float centerX = screenW - margin - mapRadius;
|
|
|
|
|
float centerY = margin + mapRadius;
|
|
|
|
|
float viewRadius = 400.0f;
|
|
|
|
|
|
|
|
|
|
// Player position in render coords
|
|
|
|
|
auto& mi = gameHandler.getMovementInfo();
|
|
|
|
|
glm::vec3 playerRender = core::coords::canonicalToRender(glm::vec3(mi.x, mi.y, mi.z));
|
|
|
|
|
|
|
|
|
|
// Camera bearing for minimap rotation
|
2026-02-07 20:51:53 -08:00
|
|
|
float bearing = 0.0f;
|
|
|
|
|
float cosB = 1.0f;
|
|
|
|
|
float sinB = 0.0f;
|
|
|
|
|
if (minimapRotate_) {
|
|
|
|
|
glm::vec3 fwd = camera->getForward();
|
|
|
|
|
bearing = std::atan2(-fwd.x, fwd.y);
|
|
|
|
|
cosB = std::cos(bearing);
|
|
|
|
|
sinB = std::sin(bearing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (statuses.empty()) return;
|
2026-02-06 20:10:10 -08:00
|
|
|
|
|
|
|
|
auto* drawList = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
for (const auto& [guid, status] : statuses) {
|
|
|
|
|
ImU32 dotColor;
|
|
|
|
|
const char* marker = nullptr;
|
|
|
|
|
if (status == game::QuestGiverStatus::AVAILABLE) {
|
|
|
|
|
dotColor = IM_COL32(255, 210, 0, 255);
|
|
|
|
|
marker = "!";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::AVAILABLE_LOW) {
|
|
|
|
|
dotColor = IM_COL32(160, 160, 160, 255);
|
|
|
|
|
marker = "!";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::REWARD) {
|
|
|
|
|
dotColor = IM_COL32(255, 210, 0, 255);
|
|
|
|
|
marker = "?";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
|
|
|
|
dotColor = IM_COL32(160, 160, 160, 255);
|
|
|
|
|
marker = "?";
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
|
|
|
|
if (!entity) continue;
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
glm::vec3 npcRender = core::coords::canonicalToRender(canonical);
|
|
|
|
|
|
|
|
|
|
// Offset from player in render coords
|
|
|
|
|
float dx = npcRender.x - playerRender.x;
|
|
|
|
|
float dy = npcRender.y - playerRender.y;
|
|
|
|
|
|
|
|
|
|
// Rotate by camera bearing (minimap north-up rotation)
|
|
|
|
|
float rx = dx * cosB - dy * sinB;
|
|
|
|
|
float ry = dx * sinB + dy * cosB;
|
|
|
|
|
|
|
|
|
|
// Scale to minimap pixels
|
|
|
|
|
float px = rx / viewRadius * mapRadius;
|
|
|
|
|
float py = -ry / viewRadius * mapRadius; // screen Y is inverted
|
|
|
|
|
|
|
|
|
|
// Clamp to circle
|
|
|
|
|
float distFromCenter = std::sqrt(px * px + py * py);
|
|
|
|
|
if (distFromCenter > mapRadius - 4.0f) {
|
|
|
|
|
float scale = (mapRadius - 4.0f) / distFromCenter;
|
|
|
|
|
px *= scale;
|
|
|
|
|
py *= scale;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float sx = centerX + px;
|
|
|
|
|
float sy = centerY + py;
|
|
|
|
|
|
|
|
|
|
// Draw dot with marker text
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, dotColor);
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
ImVec2 textSize = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, marker);
|
|
|
|
|
drawList->AddText(font, 11.0f,
|
|
|
|
|
ImVec2(sx - textSize.x * 0.5f, sy - textSize.y * 0.5f),
|
|
|
|
|
IM_COL32(0, 0, 0, 255), marker);
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
|
|
|
|
|
// Add zoom buttons at the bottom edge of the minimap
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(centerX - 30, centerY + mapRadius - 30), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(60, 24), ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags zoomFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoBackground;
|
|
|
|
|
if (ImGui::Begin("##MinimapZoom", nullptr, zoomFlags)) {
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0));
|
|
|
|
|
if (ImGui::SmallButton("-")) {
|
|
|
|
|
if (minimap) minimap->zoomOut();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::SmallButton("+")) {
|
|
|
|
|
if (minimap) minimap->zoomIn();
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
2026-02-06 20:10:10 -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
|
|
|
std::string GameScreen::getSettingsPath() {
|
|
|
|
|
std::string dir;
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
const char* appdata = std::getenv("APPDATA");
|
|
|
|
|
dir = appdata ? std::string(appdata) + "\\wowee" : ".";
|
|
|
|
|
#else
|
|
|
|
|
const char* home = std::getenv("HOME");
|
|
|
|
|
dir = home ? std::string(home) + "/.wowee" : ".";
|
|
|
|
|
#endif
|
|
|
|
|
return dir + "/settings.cfg";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
2026-02-09 17:46:45 -08:00
|
|
|
// Get player gender, pronouns, and name
|
2026-02-09 17:39:21 -08:00
|
|
|
game::Gender gender = game::Gender::NONBINARY;
|
2026-02-09 17:46:45 -08:00
|
|
|
std::string playerName = "Adventurer";
|
2026-02-09 17:39:21 -08:00
|
|
|
const auto* character = gameHandler.getActiveCharacter();
|
|
|
|
|
if (character) {
|
|
|
|
|
gender = character->gender;
|
2026-02-09 17:46:45 -08:00
|
|
|
if (!character->name.empty()) {
|
|
|
|
|
playerName = character->name;
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
}
|
|
|
|
|
game::Pronouns pronouns = game::Pronouns::forGender(gender);
|
|
|
|
|
|
|
|
|
|
std::string result = text;
|
|
|
|
|
|
|
|
|
|
// Helper to trim whitespace
|
|
|
|
|
auto trim = [](std::string& s) {
|
|
|
|
|
s.erase(0, s.find_first_not_of(" \t\n\r"));
|
|
|
|
|
s.erase(s.find_last_not_of(" \t\n\r") + 1);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-09 17:46:45 -08:00
|
|
|
// Replace simple placeholders first
|
|
|
|
|
// $n = player name
|
2026-02-09 17:39:21 -08:00
|
|
|
// $p = subject pronoun (he/she/they)
|
|
|
|
|
// $o = object pronoun (him/her/them)
|
|
|
|
|
// $s = possessive adjective (his/her/their)
|
|
|
|
|
// $S = possessive pronoun (his/hers/theirs)
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while ((pos = result.find('$', pos)) != std::string::npos) {
|
|
|
|
|
if (pos + 1 >= result.length()) break;
|
|
|
|
|
|
|
|
|
|
char code = result[pos + 1];
|
|
|
|
|
std::string replacement;
|
|
|
|
|
|
|
|
|
|
switch (code) {
|
2026-02-09 17:46:45 -08:00
|
|
|
case 'n': replacement = playerName; break;
|
2026-02-09 17:39:21 -08:00
|
|
|
case 'p': replacement = pronouns.subject; break;
|
|
|
|
|
case 'o': replacement = pronouns.object; break;
|
|
|
|
|
case 's': replacement = pronouns.possessive; break;
|
|
|
|
|
case 'S': replacement = pronouns.possessiveP; break;
|
|
|
|
|
case 'g':
|
|
|
|
|
// Handle $g separately below
|
|
|
|
|
pos++;
|
|
|
|
|
continue;
|
|
|
|
|
default:
|
|
|
|
|
pos++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 17:46:45 -08:00
|
|
|
// Replace the placeholder
|
2026-02-09 17:39:21 -08:00
|
|
|
result.replace(pos, 2, replacement);
|
|
|
|
|
pos += replacement.length();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find and replace all $g placeholders (gender-specific text)
|
|
|
|
|
// Format: $g<male>:<female>; or $g<male>:<female>:<nonbinary>;
|
|
|
|
|
pos = 0;
|
|
|
|
|
while ((pos = result.find("$g", pos)) != std::string::npos) {
|
|
|
|
|
size_t endPos = result.find(';', pos);
|
|
|
|
|
if (endPos == std::string::npos) break;
|
|
|
|
|
|
|
|
|
|
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
|
|
|
|
|
|
|
|
|
// Split by colons
|
|
|
|
|
std::vector<std::string> parts;
|
|
|
|
|
size_t start = 0;
|
|
|
|
|
size_t colonPos;
|
|
|
|
|
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
|
|
|
|
|
std::string part = placeholder.substr(start, colonPos - start);
|
|
|
|
|
trim(part);
|
|
|
|
|
parts.push_back(part);
|
|
|
|
|
start = colonPos + 1;
|
|
|
|
|
}
|
|
|
|
|
// Add the last part
|
|
|
|
|
std::string lastPart = placeholder.substr(start);
|
|
|
|
|
trim(lastPart);
|
|
|
|
|
parts.push_back(lastPart);
|
|
|
|
|
|
|
|
|
|
// Select appropriate text based on gender
|
|
|
|
|
std::string replacement;
|
|
|
|
|
if (parts.size() >= 3) {
|
|
|
|
|
// Three options: male, female, nonbinary
|
|
|
|
|
switch (gender) {
|
|
|
|
|
case game::Gender::MALE:
|
|
|
|
|
replacement = parts[0];
|
|
|
|
|
break;
|
|
|
|
|
case game::Gender::FEMALE:
|
|
|
|
|
replacement = parts[1];
|
|
|
|
|
break;
|
|
|
|
|
case game::Gender::NONBINARY:
|
|
|
|
|
replacement = parts[2];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else if (parts.size() >= 2) {
|
|
|
|
|
// Two options: male, female (use first for nonbinary)
|
|
|
|
|
switch (gender) {
|
|
|
|
|
case game::Gender::MALE:
|
|
|
|
|
replacement = parts[0];
|
|
|
|
|
break;
|
|
|
|
|
case game::Gender::FEMALE:
|
|
|
|
|
replacement = parts[1];
|
|
|
|
|
break;
|
|
|
|
|
case game::Gender::NONBINARY:
|
|
|
|
|
// Default to gender-neutral: use the shorter/simpler option
|
|
|
|
|
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Malformed placeholder
|
|
|
|
|
pos = endPos + 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.replace(pos, endPos - pos + 1, replacement);
|
|
|
|
|
pos += replacement.length();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
void GameScreen::saveSettings() {
|
|
|
|
|
std::string path = getSettingsPath();
|
|
|
|
|
std::filesystem::path dir = std::filesystem::path(path).parent_path();
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
std::filesystem::create_directories(dir, ec);
|
|
|
|
|
|
|
|
|
|
std::ofstream out(path);
|
|
|
|
|
if (!out.is_open()) {
|
|
|
|
|
LOG_WARNING("Could not save settings to ", path);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
// Interface
|
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
|
|
|
out << "ui_opacity=" << pendingUiOpacity << "\n";
|
2026-02-07 20:51:53 -08:00
|
|
|
out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n";
|
2026-02-09 17:39:21 -08:00
|
|
|
out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n";
|
|
|
|
|
|
|
|
|
|
// Audio
|
|
|
|
|
out << "master_volume=" << pendingMasterVolume << "\n";
|
|
|
|
|
out << "music_volume=" << pendingMusicVolume << "\n";
|
|
|
|
|
out << "ambient_volume=" << pendingAmbientVolume << "\n";
|
|
|
|
|
out << "ui_volume=" << pendingUiVolume << "\n";
|
|
|
|
|
out << "combat_volume=" << pendingCombatVolume << "\n";
|
|
|
|
|
out << "spell_volume=" << pendingSpellVolume << "\n";
|
|
|
|
|
out << "movement_volume=" << pendingMovementVolume << "\n";
|
|
|
|
|
out << "footstep_volume=" << pendingFootstepVolume << "\n";
|
|
|
|
|
out << "npc_voice_volume=" << pendingNpcVoiceVolume << "\n";
|
|
|
|
|
out << "mount_volume=" << pendingMountVolume << "\n";
|
|
|
|
|
out << "activity_volume=" << pendingActivityVolume << "\n";
|
|
|
|
|
|
|
|
|
|
// Controls
|
|
|
|
|
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
|
|
|
|
|
out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n";
|
|
|
|
|
|
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
|
|
|
LOG_INFO("Settings saved to ", path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::loadSettings() {
|
|
|
|
|
std::string path = getSettingsPath();
|
|
|
|
|
std::ifstream in(path);
|
|
|
|
|
if (!in.is_open()) return;
|
|
|
|
|
|
|
|
|
|
std::string line;
|
|
|
|
|
while (std::getline(in, line)) {
|
|
|
|
|
size_t eq = line.find('=');
|
|
|
|
|
if (eq == std::string::npos) continue;
|
|
|
|
|
std::string key = line.substr(0, eq);
|
|
|
|
|
std::string val = line.substr(eq + 1);
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
try {
|
|
|
|
|
// Interface
|
|
|
|
|
if (key == "ui_opacity") {
|
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
|
|
|
int v = std::stoi(val);
|
|
|
|
|
if (v >= 20 && v <= 100) {
|
|
|
|
|
pendingUiOpacity = v;
|
|
|
|
|
uiOpacity_ = static_cast<float>(v) / 100.0f;
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
} else if (key == "minimap_rotate") {
|
2026-02-07 20:51:53 -08:00
|
|
|
int v = std::stoi(val);
|
|
|
|
|
minimapRotate_ = (v != 0);
|
|
|
|
|
pendingMinimapRotate = minimapRotate_;
|
2026-02-09 17:39:21 -08:00
|
|
|
} else if (key == "minimap_square") {
|
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
minimapSquare_ = (v != 0);
|
|
|
|
|
pendingMinimapSquare = minimapSquare_;
|
|
|
|
|
}
|
|
|
|
|
// Audio
|
|
|
|
|
else if (key == "master_volume") pendingMasterVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "music_volume") pendingMusicVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "ambient_volume") pendingAmbientVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "ui_volume") pendingUiVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "combat_volume") pendingCombatVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "spell_volume") pendingSpellVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "movement_volume") pendingMovementVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "footstep_volume") pendingFootstepVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "npc_voice_volume") pendingNpcVoiceVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "mount_volume") pendingMountVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "activity_volume") pendingActivityVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
// Controls
|
|
|
|
|
else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
|
|
|
|
|
else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);
|
|
|
|
|
} catch (...) {}
|
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
|
|
|
}
|
|
|
|
|
LOG_INFO("Settings loaded from ", path);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
}} // namespace wowee::ui
|