mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 00:20:16 +00:00
Remove single-player mode to focus on multiplayer
Removed all single-player/offline mode functionality: - Removed ~2,200 lines of SQLite database code - Removed 11 public SP methods from GameHandler - Removed SP member variables and state flags - Removed SP UI elements (auth screen button, game settings) - Removed SQLite3 build dependency - Deleted docs/single-player.md - Updated documentation (README, FEATURES, CHANGELOG) Files modified: - src/game/game_handler.cpp: 2,852 lines (down from 4,921) - include/game/game_handler.hpp: Removed SP API - src/core/application.cpp/hpp: Removed startSinglePlayer() - src/ui/*: Removed SP UI logic - CMakeLists.txt: Removed SQLite3 All online multiplayer features preserved and tested.
This commit is contained in:
parent
82afb83591
commit
fb2e9bfb3d
15 changed files with 4959 additions and 3536 deletions
|
|
@ -4,6 +4,9 @@ All notable changes to the Wowee project are documented here.
|
||||||
|
|
||||||
## Recent Development (2024-2026)
|
## Recent Development (2024-2026)
|
||||||
|
|
||||||
|
### Architecture Changes
|
||||||
|
- **Removed single-player mode**: Removed offline/single-player functionality to focus exclusively on multiplayer. This includes removal of SQLite persistence, local combat simulation, and all single-player UI elements.
|
||||||
|
|
||||||
### Quest System
|
### Quest System
|
||||||
- **Quest markers**: Added ! (quest available) and ? (quest complete) markers above NPCs
|
- **Quest markers**: Added ! (quest available) and ? (quest complete) markers above NPCs
|
||||||
- **Minimap integration**: Quest markers now appear on minimap for easy navigation
|
- **Minimap integration**: Quest markers now appear on minimap for easy navigation
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ find_package(OpenGL REQUIRED)
|
||||||
find_package(GLEW REQUIRED)
|
find_package(GLEW REQUIRED)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
find_package(SQLite3 REQUIRED)
|
|
||||||
find_package(ZLIB REQUIRED)
|
find_package(ZLIB REQUIRED)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libswscale libavutil)
|
pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libswscale libavutil)
|
||||||
|
|
@ -267,14 +266,6 @@ target_link_libraries(wowee PRIVATE
|
||||||
ZLIB::ZLIB
|
ZLIB::ZLIB
|
||||||
)
|
)
|
||||||
|
|
||||||
# SQLite
|
|
||||||
if (TARGET SQLite::SQLite3)
|
|
||||||
target_link_libraries(wowee PRIVATE SQLite::SQLite3)
|
|
||||||
else()
|
|
||||||
target_include_directories(wowee PRIVATE ${SQLite3_INCLUDE_DIRS})
|
|
||||||
target_link_libraries(wowee PRIVATE ${SQLite3_LIBRARIES})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
|
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
|
||||||
if (FFMPEG_LIBRARY_DIRS)
|
if (FFMPEG_LIBRARY_DIRS)
|
||||||
target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS})
|
target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS})
|
||||||
|
|
|
||||||
28
FEATURES.md
28
FEATURES.md
|
|
@ -9,7 +9,6 @@ A comprehensive overview of all implemented features in Wowee, the native C++ Wo
|
||||||
- [Network & Authentication](#network--authentication)
|
- [Network & Authentication](#network--authentication)
|
||||||
- [Asset Pipeline](#asset-pipeline)
|
- [Asset Pipeline](#asset-pipeline)
|
||||||
- [Audio Features](#audio-features)
|
- [Audio Features](#audio-features)
|
||||||
- [Single-Player Mode](#single-player-mode)
|
|
||||||
- [Developer Tools](#developer-tools)
|
- [Developer Tools](#developer-tools)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -233,7 +232,6 @@ A comprehensive overview of all implemented features in Wowee, the native C++ Wo
|
||||||
- ✅ Password input field (masked)
|
- ✅ Password input field (masked)
|
||||||
- ✅ Server address input
|
- ✅ Server address input
|
||||||
- ✅ Login button
|
- ✅ Login button
|
||||||
- ✅ Single Player button (offline mode)
|
|
||||||
- ✅ Connection status display
|
- ✅ Connection status display
|
||||||
- ✅ Error message display
|
- ✅ Error message display
|
||||||
|
|
||||||
|
|
@ -519,32 +517,6 @@ A comprehensive overview of all implemented features in Wowee, the native C++ Wo
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Single-Player Mode
|
|
||||||
|
|
||||||
### Offline Gameplay
|
|
||||||
- ✅ Play without server connection
|
|
||||||
- ✅ Local character creation
|
|
||||||
- ✅ Local character selection
|
|
||||||
- ✅ Character persistence (SQLite)
|
|
||||||
- ✅ Settings persistence
|
|
||||||
|
|
||||||
### Simulated Systems
|
|
||||||
- ✅ Simulated combat
|
|
||||||
- ✅ Simulated XP gain
|
|
||||||
- ✅ Local inventory management
|
|
||||||
- ✅ Local spell casting
|
|
||||||
- ✅ Simulated loot (TODO)
|
|
||||||
- ✅ Simulated quests (TODO)
|
|
||||||
|
|
||||||
### Local Storage
|
|
||||||
- ✅ SQLite database for character data
|
|
||||||
- ✅ Character appearance and stats
|
|
||||||
- ✅ Inventory and equipment
|
|
||||||
- ✅ Known spells
|
|
||||||
- ✅ Settings and preferences
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Developer Tools
|
## Developer Tools
|
||||||
|
|
||||||
### Debug HUD (F1)
|
### Debug HUD (F1)
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,6 @@ A native C++ client for World of Warcraft 3.3.5a (Wrath of the Lich King) with a
|
||||||
- **Party** -- Group invites, party list
|
- **Party** -- Group invites, party list
|
||||||
- **UI** -- Loading screens with progress bar, settings window with opacity slider
|
- **UI** -- Loading screens with progress bar, settings window with opacity slider
|
||||||
|
|
||||||
### Single-Player Mode
|
|
||||||
- Offline play without server connection
|
|
||||||
- Local character persistence (SQLite)
|
|
||||||
- Simulated XP and combat
|
|
||||||
- Local item management
|
|
||||||
- Settings persistence
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
@ -161,7 +154,7 @@ make -j$(nproc)
|
||||||
- **Graphics**: OpenGL 3.3 Core, GLSL 330, forward rendering with post-processing
|
- **Graphics**: OpenGL 3.3 Core, GLSL 330, forward rendering with post-processing
|
||||||
- **Performance**: 60 FPS (vsync), ~50k triangles/frame, ~30 draw calls, <10% GPU
|
- **Performance**: 60 FPS (vsync), ~50k triangles/frame, ~30 draw calls, <10% GPU
|
||||||
- **Platform**: Linux (primary), C++17, CMake 3.15+
|
- **Platform**: Linux (primary), C++17, CMake 3.15+
|
||||||
- **Dependencies**: SDL2, OpenGL/GLEW, GLM, OpenSSL, StormLib, ImGui, SQLite3, FFmpeg
|
- **Dependencies**: SDL2, OpenGL/GLEW, GLM, OpenSSL, StormLib, ImGui, FFmpeg
|
||||||
- **Architecture**: Modular design with clear separation (core, rendering, networking, game logic, asset pipeline, UI, audio)
|
- **Architecture**: Modular design with clear separation (core, rendering, networking, game logic, asset pipeline, UI, audio)
|
||||||
- **Networking**: Non-blocking TCP, SRP6a authentication, RC4 encryption, WoW 3.3.5a protocol
|
- **Networking**: Non-blocking TCP, SRP6a authentication, RC4 encryption, WoW 3.3.5a protocol
|
||||||
- **Asset Loading**: Async terrain streaming, lazy loading, MPQ archive support
|
- **Asset Loading**: Async terrain streaming, lazy loading, MPQ archive support
|
||||||
|
|
|
||||||
|
|
@ -1,575 +0,0 @@
|
||||||
# Single-Player Mode Guide
|
|
||||||
|
|
||||||
**Date**: 2026-01-27
|
|
||||||
**Purpose**: Play wowee without a server connection
|
|
||||||
**Status**: ✅ Fully Functional
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Single-player mode allows you to explore the rendering engine without setting up a server. It bypasses authentication and loads the game world directly with all atmospheric effects and test objects.
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
### 1. Start the Client
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/k/Desktop/wowee/wowee
|
|
||||||
./build/bin/wowee
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Click "Start Single Player"
|
|
||||||
|
|
||||||
On the authentication screen, you'll see:
|
|
||||||
- **Server connection** section (hostname, username, password)
|
|
||||||
- **Single-Player Mode** section with a large blue button
|
|
||||||
|
|
||||||
Click the **"Start Single Player"** button to bypass authentication and go directly to the game world.
|
|
||||||
|
|
||||||
### 3. Explore the World
|
|
||||||
|
|
||||||
You'll immediately enter the game with:
|
|
||||||
- ✨ Full atmospheric rendering (sky, stars, clouds, sun/moon)
|
|
||||||
- 🎮 Full camera controls (WASD, mouse)
|
|
||||||
- 🌦️ Weather effects (W key to cycle)
|
|
||||||
- 🏗️ Ability to spawn test objects (K, O keys)
|
|
||||||
- 📊 Performance HUD (F1 to toggle)
|
|
||||||
|
|
||||||
## Features Available
|
|
||||||
|
|
||||||
### Atmospheric Rendering ✅
|
|
||||||
- **Skybox** - Dynamic day/night gradient
|
|
||||||
- **Stars** - 1000+ stars visible at night
|
|
||||||
- **Celestial** - Sun and moon with orbital movement
|
|
||||||
- **Clouds** - Animated volumetric clouds
|
|
||||||
- **Lens Flare** - Sun bloom effects
|
|
||||||
- **Weather** - Rain and snow particle systems
|
|
||||||
|
|
||||||
### Camera & Movement ✅
|
|
||||||
- **WASD** - Free-fly camera movement
|
|
||||||
- **Mouse** - Look around (360° rotation)
|
|
||||||
- **Shift** - Move faster (sprint)
|
|
||||||
- Full 3D navigation with no collisions
|
|
||||||
|
|
||||||
### Test Objects ✅
|
|
||||||
- **K key** - Spawn test character (animated cube)
|
|
||||||
- **O key** - Spawn procedural WMO building (5×5×5 cube)
|
|
||||||
- **Shift+O** - Load real WMO from MPQ (if WOW_DATA_PATH set)
|
|
||||||
- **P key** - Clear all WMO buildings
|
|
||||||
- **J key** - Clear characters
|
|
||||||
|
|
||||||
### Rendering Controls ✅
|
|
||||||
- **F1** - Toggle performance HUD
|
|
||||||
- **F2** - Wireframe mode
|
|
||||||
- **F8** - Toggle water rendering
|
|
||||||
- **F9** - Toggle time progression
|
|
||||||
- **F10** - Toggle sun/moon
|
|
||||||
- **F11** - Toggle stars
|
|
||||||
- **F12** - Toggle fog
|
|
||||||
- **+/-** - Manual time of day adjustment
|
|
||||||
|
|
||||||
### Effects Controls ✅
|
|
||||||
- **C** - Toggle clouds
|
|
||||||
- **[/]** - Adjust cloud density
|
|
||||||
- **L** - Toggle lens flare
|
|
||||||
- **,/.** - Adjust lens flare intensity
|
|
||||||
- **M** - Toggle moon phase cycling
|
|
||||||
- **;/'** - Manual moon phase control
|
|
||||||
- **W** - Cycle weather (None → Rain → Snow)
|
|
||||||
- **</>** - Adjust weather intensity
|
|
||||||
|
|
||||||
## Loading Terrain (Optional)
|
|
||||||
|
|
||||||
Single-player mode can load real terrain if you have WoW data files.
|
|
||||||
|
|
||||||
### Setup WOW_DATA_PATH
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux/Mac
|
|
||||||
export WOW_DATA_PATH="/path/to/WoW-3.3.5a/Data"
|
|
||||||
|
|
||||||
# Or add to ~/.bashrc for persistence
|
|
||||||
echo 'export WOW_DATA_PATH="/path/to/WoW-3.3.5a/Data"' >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Run client
|
|
||||||
cd /home/k/Desktop/wowee/wowee
|
|
||||||
./build/bin/wowee
|
|
||||||
```
|
|
||||||
|
|
||||||
### What Gets Loaded
|
|
||||||
|
|
||||||
With `WOW_DATA_PATH` set, single-player mode will attempt to load:
|
|
||||||
- **Terrain** - Elwynn Forest ADT tile (32, 49) near Northshire Abbey
|
|
||||||
- **Textures** - Ground textures from MPQ archives
|
|
||||||
- **Water** - Water tiles from the terrain
|
|
||||||
- **Buildings** - Real WMO models (Shift+O key)
|
|
||||||
|
|
||||||
### Data Directory Structure
|
|
||||||
|
|
||||||
Your WoW Data directory should contain:
|
|
||||||
```
|
|
||||||
Data/
|
|
||||||
├── common.MPQ
|
|
||||||
├── common-2.MPQ
|
|
||||||
├── expansion.MPQ
|
|
||||||
├── lichking.MPQ
|
|
||||||
├── patch.MPQ
|
|
||||||
├── patch-2.MPQ
|
|
||||||
├── patch-3.MPQ
|
|
||||||
└── enUS/ (or your locale)
|
|
||||||
├── locale-enUS.MPQ
|
|
||||||
└── patch-enUS-3.MPQ
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### 1. Rendering Engine Testing
|
|
||||||
|
|
||||||
Perfect for testing and debugging rendering features:
|
|
||||||
- Test sky system day/night cycle
|
|
||||||
- Verify atmospheric effects
|
|
||||||
- Profile performance
|
|
||||||
- Test shader changes
|
|
||||||
- Debug camera controls
|
|
||||||
|
|
||||||
### 2. Visual Effects Development
|
|
||||||
|
|
||||||
Ideal for developing visual effects:
|
|
||||||
- Weather systems
|
|
||||||
- Particle effects
|
|
||||||
- Post-processing
|
|
||||||
- Shader effects
|
|
||||||
- Lighting changes
|
|
||||||
|
|
||||||
### 3. Screenshots & Videos
|
|
||||||
|
|
||||||
Great for capturing content:
|
|
||||||
- Time-lapse videos of day/night cycle
|
|
||||||
- Weather effect demonstrations
|
|
||||||
- Atmospheric rendering showcases
|
|
||||||
- Feature demonstrations
|
|
||||||
|
|
||||||
### 4. Performance Profiling
|
|
||||||
|
|
||||||
Excellent for performance analysis:
|
|
||||||
- Measure FPS with different effects
|
|
||||||
- Test GPU/CPU usage
|
|
||||||
- Profile draw calls and triangles
|
|
||||||
- Test memory usage
|
|
||||||
- Benchmark optimizations
|
|
||||||
|
|
||||||
### 5. Learning & Exploration
|
|
||||||
|
|
||||||
Good for learning the codebase:
|
|
||||||
- Understand rendering pipeline
|
|
||||||
- Explore atmospheric systems
|
|
||||||
- Test object spawning
|
|
||||||
- Experiment with controls
|
|
||||||
- Learn shader systems
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
|
|
||||||
**Normal Flow:**
|
|
||||||
```
|
|
||||||
AUTHENTICATION → REALM_SELECTION → CHARACTER_SELECTION → IN_GAME
|
|
||||||
```
|
|
||||||
|
|
||||||
**Single-Player Flow:**
|
|
||||||
```
|
|
||||||
AUTHENTICATION → [Single Player Button] → IN_GAME
|
|
||||||
```
|
|
||||||
|
|
||||||
Single-player mode bypasses:
|
|
||||||
- Network authentication
|
|
||||||
- Realm selection
|
|
||||||
- Character selection
|
|
||||||
- Server connection
|
|
||||||
|
|
||||||
### World Creation
|
|
||||||
|
|
||||||
When single-player starts:
|
|
||||||
1. Creates empty `World` object
|
|
||||||
2. Sets `singlePlayerMode = true` flag
|
|
||||||
3. Attempts terrain loading if asset manager available
|
|
||||||
4. Transitions to `IN_GAME` state
|
|
||||||
5. Continues with atmospheric rendering
|
|
||||||
|
|
||||||
### Terrain Loading Logic
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
if (WOW_DATA_PATH set && AssetManager initialized) {
|
|
||||||
try to load: "World\Maps\Azeroth\Azeroth_32_49.adt"
|
|
||||||
if (success) {
|
|
||||||
render terrain with textures
|
|
||||||
} else {
|
|
||||||
atmospheric rendering only
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
atmospheric rendering only
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Camera Behavior
|
|
||||||
|
|
||||||
**Single-Player Camera:**
|
|
||||||
- Starts at default position (0, 0, 100)
|
|
||||||
- Free-fly mode (no terrain collision)
|
|
||||||
- Full 360° rotation
|
|
||||||
- Adjustable speed (Shift for faster)
|
|
||||||
|
|
||||||
**In-Game Camera (with server):**
|
|
||||||
- Follows character position
|
|
||||||
- Same controls but synced with server
|
|
||||||
- Position updates sent to server
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
### Without Terrain
|
|
||||||
|
|
||||||
**Atmospheric Only:**
|
|
||||||
- FPS: 60 (vsync limited)
|
|
||||||
- Triangles: ~2,000 (skybox + clouds)
|
|
||||||
- Draw Calls: ~8
|
|
||||||
- CPU: 5-10%
|
|
||||||
- GPU: 10-15%
|
|
||||||
- Memory: ~150 MB
|
|
||||||
|
|
||||||
### With Terrain
|
|
||||||
|
|
||||||
**Full Rendering:**
|
|
||||||
- FPS: 60 (vsync maintained)
|
|
||||||
- Triangles: ~50,000
|
|
||||||
- Draw Calls: ~30
|
|
||||||
- CPU: 10-15%
|
|
||||||
- GPU: 15-25%
|
|
||||||
- Memory: ~200 MB
|
|
||||||
|
|
||||||
### With Test Objects
|
|
||||||
|
|
||||||
**Characters + Buildings:**
|
|
||||||
- Characters (10): +500 triangles, +1 draw call each
|
|
||||||
- Buildings (5): +5,000 triangles, +1 draw call each
|
|
||||||
- Total impact: ~10% GPU increase
|
|
||||||
|
|
||||||
## Differences from Server Mode
|
|
||||||
|
|
||||||
### What Works
|
|
||||||
|
|
||||||
- ✅ Full atmospheric rendering
|
|
||||||
- ✅ Camera movement
|
|
||||||
- ✅ Visual effects (clouds, weather, lens flare)
|
|
||||||
- ✅ Test object spawning
|
|
||||||
- ✅ Performance HUD
|
|
||||||
- ✅ All rendering toggles
|
|
||||||
- ✅ Time of day controls
|
|
||||||
|
|
||||||
### What Doesn't Work
|
|
||||||
|
|
||||||
- ❌ Network synchronization
|
|
||||||
- ❌ Real characters from database
|
|
||||||
- ❌ Creatures and NPCs
|
|
||||||
- ❌ Combat system
|
|
||||||
- ❌ Chat/social features
|
|
||||||
- ❌ Spells and abilities
|
|
||||||
- ❌ Inventory system
|
|
||||||
- ❌ Quest system
|
|
||||||
|
|
||||||
### Limitations
|
|
||||||
|
|
||||||
**No Server Features:**
|
|
||||||
- Cannot connect to other players
|
|
||||||
- No persistent world state
|
|
||||||
- No database-backed characters
|
|
||||||
- No server-side validation
|
|
||||||
- No creature AI or spawning
|
|
||||||
|
|
||||||
**Test Objects Only:**
|
|
||||||
- Characters are simple cubes
|
|
||||||
- Buildings are procedural or MPQ-loaded
|
|
||||||
- No real character models (yet)
|
|
||||||
- No animations beyond test cubes
|
|
||||||
|
|
||||||
## Tips & Tricks
|
|
||||||
|
|
||||||
### 1. Cinematic Screenshots
|
|
||||||
|
|
||||||
Create beautiful atmospheric shots:
|
|
||||||
```
|
|
||||||
1. Press F1 to hide HUD
|
|
||||||
2. Press F9 to auto-cycle time
|
|
||||||
3. Press C to enable clouds
|
|
||||||
4. Press L for lens flare
|
|
||||||
5. Wait for sunset/sunrise (golden hour)
|
|
||||||
6. Take screenshots!
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Performance Testing
|
|
||||||
|
|
||||||
Stress test the renderer:
|
|
||||||
```
|
|
||||||
1. Spawn 10 characters (press K ten times)
|
|
||||||
2. Spawn 5 buildings (press O five times)
|
|
||||||
3. Enable all effects (clouds, weather, lens flare)
|
|
||||||
4. Toggle F1 to monitor FPS
|
|
||||||
5. Profile different settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Day/Night Exploration
|
|
||||||
|
|
||||||
Experience the full day cycle:
|
|
||||||
```
|
|
||||||
1. Press F9 to start time progression
|
|
||||||
2. Watch stars appear at dusk
|
|
||||||
3. See moon phases change
|
|
||||||
4. Observe color transitions
|
|
||||||
5. Press F9 again to stop at favorite time
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Weather Showcase
|
|
||||||
|
|
||||||
Test weather systems:
|
|
||||||
```
|
|
||||||
1. Press W to enable rain
|
|
||||||
2. Press > to max intensity
|
|
||||||
3. Press W again for snow
|
|
||||||
4. Fly through particles
|
|
||||||
5. Test with different times of day
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Building Gallery
|
|
||||||
|
|
||||||
Create a building showcase:
|
|
||||||
```
|
|
||||||
1. Press O five times for procedural cubes
|
|
||||||
2. Press Shift+O to load real WMO (if data available)
|
|
||||||
3. Fly around to see different angles
|
|
||||||
4. Press F2 for wireframe view
|
|
||||||
5. Press P to clear and try others
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Black Screen
|
|
||||||
|
|
||||||
**Problem:** Screen is black, no rendering
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check if application is running
|
|
||||||
ps aux | grep wowee
|
|
||||||
|
|
||||||
# Check OpenGL initialization in logs
|
|
||||||
# Should see: "Renderer initialized"
|
|
||||||
|
|
||||||
# Verify graphics drivers
|
|
||||||
glxinfo | grep OpenGL
|
|
||||||
```
|
|
||||||
|
|
||||||
### Low FPS
|
|
||||||
|
|
||||||
**Problem:** Performance below 60 FPS
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Press F1 to check FPS counter
|
|
||||||
2. Disable heavy effects:
|
|
||||||
- Press C to disable clouds
|
|
||||||
- Press L to disable lens flare
|
|
||||||
- Press W until weather is "None"
|
|
||||||
3. Clear test objects:
|
|
||||||
- Press J to clear characters
|
|
||||||
- Press P to clear buildings
|
|
||||||
4. Check GPU usage in system monitor
|
|
||||||
|
|
||||||
### No Terrain
|
|
||||||
|
|
||||||
**Problem:** Only sky visible, no ground
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check if WOW_DATA_PATH is set
|
|
||||||
echo $WOW_DATA_PATH
|
|
||||||
|
|
||||||
# Set it if missing
|
|
||||||
export WOW_DATA_PATH="/path/to/WoW-3.3.5a/Data"
|
|
||||||
|
|
||||||
# Restart single-player mode
|
|
||||||
# Should see: "Test terrain loaded successfully"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Controls Not Working
|
|
||||||
|
|
||||||
**Problem:** Keyboard/mouse input not responding
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Click on the game window to focus it
|
|
||||||
2. Check if a UI element has focus (press Escape)
|
|
||||||
3. Verify input in logs (should see key presses)
|
|
||||||
4. Restart application if needed
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Planned Features
|
|
||||||
|
|
||||||
**Short-term:**
|
|
||||||
- [ ] Load multiple terrain tiles
|
|
||||||
- [ ] Real M2 character models
|
|
||||||
- [ ] Texture loading for WMOs
|
|
||||||
- [ ] Save/load camera position
|
|
||||||
- [ ] Screenshot capture (F11/F12)
|
|
||||||
|
|
||||||
**Medium-term:**
|
|
||||||
- [ ] Simple creature spawning (static models)
|
|
||||||
- [ ] Waypoint camera paths
|
|
||||||
- [ ] Time-lapse recording
|
|
||||||
- [ ] Custom weather patterns
|
|
||||||
- [ ] Terrain tile selection UI
|
|
||||||
|
|
||||||
**Long-term:**
|
|
||||||
- [ ] Offline character creation
|
|
||||||
- [ ] Basic movement animations
|
|
||||||
- [ ] Simple AI behaviors
|
|
||||||
- [ ] World exploration without server
|
|
||||||
- [ ] Local save/load system
|
|
||||||
|
|
||||||
## Comparison: Single-Player vs Server
|
|
||||||
|
|
||||||
| Feature | Single-Player | Server Mode |
|
|
||||||
|---------|---------------|-------------|
|
|
||||||
| **Setup Time** | Instant | 30-60 min |
|
|
||||||
| **Network Required** | No | Yes |
|
|
||||||
| **Terrain Loading** | Optional | Yes |
|
|
||||||
| **Character Models** | Test cubes | Real models |
|
|
||||||
| **Creatures** | None | Full AI |
|
|
||||||
| **Combat** | No | Yes |
|
|
||||||
| **Chat** | No | Yes |
|
|
||||||
| **Quests** | No | Yes |
|
|
||||||
| **Persistence** | No | Yes |
|
|
||||||
| **Performance** | High | Medium |
|
|
||||||
| **Good For** | Testing, visuals | Gameplay |
|
|
||||||
|
|
||||||
## Console Commands
|
|
||||||
|
|
||||||
While in single-player mode, you can use:
|
|
||||||
|
|
||||||
**Camera Commands:**
|
|
||||||
```
|
|
||||||
WASD - Move
|
|
||||||
Mouse - Look
|
|
||||||
Shift - Sprint
|
|
||||||
```
|
|
||||||
|
|
||||||
**Spawning Commands:**
|
|
||||||
```
|
|
||||||
K - Spawn character
|
|
||||||
O - Spawn building
|
|
||||||
Shift+O - Load WMO
|
|
||||||
J - Clear characters
|
|
||||||
P - Clear buildings
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rendering Commands:**
|
|
||||||
```
|
|
||||||
F1 - Toggle HUD
|
|
||||||
F2 - Wireframe
|
|
||||||
F8-F12 - Various toggles
|
|
||||||
C - Clouds
|
|
||||||
L - Lens flare
|
|
||||||
W - Weather
|
|
||||||
M - Moon phases
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Workflow
|
|
||||||
|
|
||||||
### Complete Testing Session
|
|
||||||
|
|
||||||
1. **Start Application**
|
|
||||||
```bash
|
|
||||||
cd /home/k/Desktop/wowee/wowee
|
|
||||||
./build/bin/wowee
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Enter Single-Player**
|
|
||||||
- Click "Start Single Player" button
|
|
||||||
- Wait for world load
|
|
||||||
|
|
||||||
3. **Enable Effects**
|
|
||||||
- Press F1 (show HUD)
|
|
||||||
- Press C (enable clouds)
|
|
||||||
- Press L (enable lens flare)
|
|
||||||
- Press W (enable rain)
|
|
||||||
|
|
||||||
4. **Spawn Objects**
|
|
||||||
- Press K × 3 (spawn 3 characters)
|
|
||||||
- Press O × 2 (spawn 2 buildings)
|
|
||||||
|
|
||||||
5. **Explore**
|
|
||||||
- Use WASD to fly around
|
|
||||||
- Mouse to look around
|
|
||||||
- Shift to move faster
|
|
||||||
|
|
||||||
6. **Time Progression**
|
|
||||||
- Press F9 (auto time)
|
|
||||||
- Watch day → night transition
|
|
||||||
- Press + or - for manual control
|
|
||||||
|
|
||||||
7. **Take Screenshots**
|
|
||||||
- Press F1 (hide HUD)
|
|
||||||
- Position camera
|
|
||||||
- Use external screenshot tool
|
|
||||||
|
|
||||||
8. **Performance Check**
|
|
||||||
- Press F1 (show HUD)
|
|
||||||
- Check FPS (should be 60)
|
|
||||||
- Note draw calls and triangles
|
|
||||||
- Monitor CPU/GPU usage
|
|
||||||
|
|
||||||
## Keyboard Reference Card
|
|
||||||
|
|
||||||
**Essential Controls:**
|
|
||||||
- **Start Single Player** - Button on auth screen
|
|
||||||
- **F1** - Performance HUD
|
|
||||||
- **WASD** - Move camera
|
|
||||||
- **Mouse** - Look around
|
|
||||||
- **Shift** - Move faster
|
|
||||||
- **Escape** - Release mouse (if captured)
|
|
||||||
|
|
||||||
**Rendering:**
|
|
||||||
- **F2-F12** - Various toggles
|
|
||||||
- **+/-** - Time of day
|
|
||||||
- **C** - Clouds
|
|
||||||
- **L** - Lens flare
|
|
||||||
- **W** - Weather
|
|
||||||
- **M** - Moon phases
|
|
||||||
|
|
||||||
**Objects:**
|
|
||||||
- **K** - Spawn character
|
|
||||||
- **O** - Spawn building
|
|
||||||
- **J** - Clear characters
|
|
||||||
- **P** - Clear buildings
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
**Single-Player Mode:**
|
|
||||||
- Designed for rapid testing and development
|
|
||||||
- No server setup required
|
|
||||||
- Full rendering engine access
|
|
||||||
- Perfect for content creators
|
|
||||||
|
|
||||||
**Powered by:**
|
|
||||||
- OpenGL 3.3 rendering
|
|
||||||
- GLM mathematics
|
|
||||||
- SDL2 windowing
|
|
||||||
- ImGui interface
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Mode Status**: ✅ Fully Functional
|
|
||||||
**Performance**: 60 FPS stable
|
|
||||||
**Setup Time**: Instant (one click)
|
|
||||||
**Server Required**: No
|
|
||||||
**Last Updated**: 2026-01-27
|
|
||||||
**Version**: 1.0.3
|
|
||||||
|
|
@ -56,16 +56,11 @@ public:
|
||||||
// Singleton access
|
// Singleton access
|
||||||
static Application& getInstance() { return *instance; }
|
static Application& getInstance() { return *instance; }
|
||||||
|
|
||||||
// Single-player mode
|
|
||||||
void startSinglePlayer();
|
|
||||||
bool isSinglePlayer() const { return singlePlayerMode; }
|
|
||||||
void logoutToLogin();
|
|
||||||
|
|
||||||
// Weapon loading (called at spawn and on equipment change)
|
// Weapon loading (called at spawn and on equipment change)
|
||||||
void loadEquippedWeapons();
|
void loadEquippedWeapons();
|
||||||
|
|
||||||
// Teleport to a spawn preset location (single-player only)
|
// Logout to login screen
|
||||||
void teleportTo(int presetIndex);
|
void logoutToLogin();
|
||||||
|
|
||||||
// Render bounds lookup (for click targeting / selection)
|
// Render bounds lookup (for click targeting / selection)
|
||||||
bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const;
|
bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const;
|
||||||
|
|
@ -104,20 +99,11 @@ private:
|
||||||
|
|
||||||
AppState state = AppState::AUTHENTICATION;
|
AppState state = AppState::AUTHENTICATION;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
bool singlePlayerMode = false;
|
|
||||||
bool playerCharacterSpawned = false;
|
bool playerCharacterSpawned = false;
|
||||||
bool npcsSpawned = false;
|
bool npcsSpawned = false;
|
||||||
bool spawnSnapToGround = true;
|
bool spawnSnapToGround = true;
|
||||||
float lastFrameTime = 0.0f;
|
float lastFrameTime = 0.0f;
|
||||||
float movementHeartbeatTimer = 0.0f;
|
float movementHeartbeatTimer = 0.0f;
|
||||||
game::Race spRace_ = game::Race::HUMAN;
|
|
||||||
game::Gender spGender_ = game::Gender::MALE;
|
|
||||||
game::Class spClass_ = game::Class::WARRIOR;
|
|
||||||
uint32_t spMapId_ = 0;
|
|
||||||
uint32_t spZoneId_ = 0;
|
|
||||||
glm::vec3 spSpawnCanonical_ = glm::vec3(62.0f, -9464.0f, 200.0f);
|
|
||||||
float spYawDeg_ = 0.0f;
|
|
||||||
float spPitchDeg_ = -5.0f;
|
|
||||||
|
|
||||||
// Weapon model ID counter (starting high to avoid collision with character model IDs)
|
// Weapon model ID counter (starting high to avoid collision with character model IDs)
|
||||||
uint32_t nextWeaponModelId_ = 1000;
|
uint32_t nextWeaponModelId_ = 1000;
|
||||||
|
|
|
||||||
|
|
@ -189,22 +189,6 @@ public:
|
||||||
// Money (copper)
|
// Money (copper)
|
||||||
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
||||||
|
|
||||||
// Single-player: mark character list ready for selection UI
|
|
||||||
void setSinglePlayerCharListReady();
|
|
||||||
struct SinglePlayerSettings {
|
|
||||||
bool fullscreen = false;
|
|
||||||
bool vsync = true;
|
|
||||||
bool shadows = true;
|
|
||||||
int resWidth = 1920;
|
|
||||||
int resHeight = 1080;
|
|
||||||
int musicVolume = 30;
|
|
||||||
int sfxVolume = 100;
|
|
||||||
float mouseSensitivity = 0.2f;
|
|
||||||
bool invertMouse = false;
|
|
||||||
};
|
|
||||||
bool getSinglePlayerSettings(SinglePlayerSettings& out) const;
|
|
||||||
void setSinglePlayerSettings(const SinglePlayerSettings& settings);
|
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
Inventory& getInventory() { return inventory; }
|
Inventory& getInventory() { return inventory; }
|
||||||
const Inventory& getInventory() const { return inventory; }
|
const Inventory& getInventory() const { return inventory; }
|
||||||
|
|
@ -252,54 +236,18 @@ public:
|
||||||
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
|
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
|
||||||
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
|
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
|
||||||
|
|
||||||
// Single-player mode
|
|
||||||
void setSinglePlayerMode(bool sp) { singlePlayerMode_ = sp; }
|
|
||||||
bool isSinglePlayerMode() const { return singlePlayerMode_; }
|
|
||||||
void simulateMotd(const std::vector<std::string>& lines);
|
|
||||||
void applySinglePlayerStartData(Race race, Class cls);
|
|
||||||
bool loadSinglePlayerCharacterState(uint64_t guid);
|
|
||||||
void notifyInventoryChanged();
|
|
||||||
void notifyEquipmentChanged();
|
|
||||||
void notifyQuestStateChanged();
|
|
||||||
void flushSinglePlayerSave();
|
|
||||||
|
|
||||||
// NPC death callback (single-player)
|
|
||||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
|
||||||
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
|
||||||
|
|
||||||
// NPC respawn callback (health 0 → >0, resets animation to idle)
|
|
||||||
using NpcRespawnCallback = std::function<void(uint64_t guid)>;
|
|
||||||
void setNpcRespawnCallback(NpcRespawnCallback cb) { npcRespawnCallback_ = std::move(cb); }
|
|
||||||
|
|
||||||
// Melee swing callback (for driving animation/SFX)
|
// Melee swing callback (for driving animation/SFX)
|
||||||
using MeleeSwingCallback = std::function<void()>;
|
using MeleeSwingCallback = std::function<void()>;
|
||||||
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
|
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// NPC swing callback (single-player combat: plays attack animation on NPC)
|
|
||||||
using NpcSwingCallback = std::function<void(uint64_t guid)>;
|
|
||||||
void setNpcSwingCallback(NpcSwingCallback cb) { npcSwingCallback_ = std::move(cb); }
|
|
||||||
|
|
||||||
// Local player stats (single-player)
|
|
||||||
uint32_t getLocalPlayerHealth() const { return localPlayerHealth_; }
|
|
||||||
uint32_t getLocalPlayerMaxHealth() const { return localPlayerMaxHealth_; }
|
|
||||||
void initLocalPlayerStats(uint32_t level, uint32_t hp, uint32_t maxHp) {
|
|
||||||
localPlayerLevel_ = level;
|
|
||||||
localPlayerHealth_ = hp;
|
|
||||||
localPlayerMaxHealth_ = maxHp;
|
|
||||||
playerNextLevelXp_ = xpForLevel(level);
|
|
||||||
playerXp_ = 0;
|
playerXp_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XP tracking (works in both single-player and server modes)
|
// XP tracking (works in both single-player and server modes)
|
||||||
uint32_t getPlayerXp() const { return playerXp_; }
|
uint32_t getPlayerXp() const { return playerXp_; }
|
||||||
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
||||||
uint32_t getPlayerLevel() const { return singlePlayerMode_ ? localPlayerLevel_ : serverPlayerLevel_; }
|
uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
|
||||||
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
||||||
|
|
||||||
// Hearthstone callback (single-player teleport)
|
|
||||||
using HearthstoneCallback = std::function<void()>;
|
|
||||||
void setHearthstoneCallback(HearthstoneCallback cb) { hearthstoneCallback = std::move(cb); }
|
|
||||||
|
|
||||||
// World entry callback (online mode - triggered when entering world)
|
// World entry callback (online mode - triggered when entering world)
|
||||||
// Parameters: mapId, x, y, z (canonical WoW coordinates)
|
// Parameters: mapId, x, y, z (canonical WoW coordinates)
|
||||||
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
||||||
|
|
@ -406,8 +354,6 @@ public:
|
||||||
auto it = itemInfoCache_.find(itemId);
|
auto it = itemInfoCache_.find(itemId);
|
||||||
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
||||||
}
|
}
|
||||||
std::string getItemTemplateName(uint32_t itemId) const;
|
|
||||||
ItemQuality getItemTemplateQuality(uint32_t itemId) const;
|
|
||||||
uint64_t getBackpackItemGuid(int index) const {
|
uint64_t getBackpackItemGuid(int index) const {
|
||||||
if (index < 0 || index >= static_cast<int>(backpackSlotGuids_.size())) return 0;
|
if (index < 0 || index >= static_cast<int>(backpackSlotGuids_.size())) return 0;
|
||||||
return backpackSlotGuids_[index];
|
return backpackSlotGuids_[index];
|
||||||
|
|
@ -427,16 +373,6 @@ public:
|
||||||
*/
|
*/
|
||||||
void update(float deltaTime);
|
void update(float deltaTime);
|
||||||
|
|
||||||
struct SinglePlayerCreateInfo {
|
|
||||||
uint32_t mapId = 0;
|
|
||||||
uint32_t zoneId = 0;
|
|
||||||
float x = 0.0f;
|
|
||||||
float y = 0.0f;
|
|
||||||
float z = 0.0f;
|
|
||||||
float orientation = 0.0f;
|
|
||||||
};
|
|
||||||
bool getSinglePlayerCreateInfo(Race race, Class cls, SinglePlayerCreateInfo& out) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* Handle incoming packet from world server
|
* Handle incoming packet from world server
|
||||||
|
|
@ -552,11 +488,6 @@ private:
|
||||||
void handleQuestRequestItems(network::Packet& packet);
|
void handleQuestRequestItems(network::Packet& packet);
|
||||||
void handleQuestOfferReward(network::Packet& packet);
|
void handleQuestOfferReward(network::Packet& packet);
|
||||||
void handleListInventory(network::Packet& packet);
|
void handleListInventory(network::Packet& packet);
|
||||||
LootResponseData generateLocalLoot(uint64_t guid);
|
|
||||||
void simulateLootResponse(const LootResponseData& data);
|
|
||||||
void simulateLootRelease();
|
|
||||||
void simulateLootRemove(uint8_t slotIndex);
|
|
||||||
void simulateXpGain(uint64_t victimGuid, uint32_t totalXp);
|
|
||||||
void addMoneyCopper(uint32_t amount);
|
void addMoneyCopper(uint32_t amount);
|
||||||
|
|
||||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource);
|
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource);
|
||||||
|
|
@ -661,7 +592,6 @@ private:
|
||||||
std::vector<CombatTextEntry> combatText;
|
std::vector<CombatTextEntry> combatText;
|
||||||
|
|
||||||
// ---- Phase 3: Spells ----
|
// ---- Phase 3: Spells ----
|
||||||
HearthstoneCallback hearthstoneCallback;
|
|
||||||
WorldEntryCallback worldEntryCallback_;
|
WorldEntryCallback worldEntryCallback_;
|
||||||
CreatureSpawnCallback creatureSpawnCallback_;
|
CreatureSpawnCallback creatureSpawnCallback_;
|
||||||
CreatureDespawnCallback creatureDespawnCallback_;
|
CreatureDespawnCallback creatureDespawnCallback_;
|
||||||
|
|
@ -739,69 +669,10 @@ private:
|
||||||
uint32_t playerXp_ = 0;
|
uint32_t playerXp_ = 0;
|
||||||
uint32_t playerNextLevelXp_ = 0;
|
uint32_t playerNextLevelXp_ = 0;
|
||||||
uint32_t serverPlayerLevel_ = 1;
|
uint32_t serverPlayerLevel_ = 1;
|
||||||
void awardLocalXp(uint64_t victimGuid, uint32_t victimLevel);
|
|
||||||
void levelUp();
|
|
||||||
static uint32_t xpForLevel(uint32_t level);
|
static uint32_t xpForLevel(uint32_t level);
|
||||||
|
|
||||||
// ---- Single-player combat ----
|
|
||||||
bool singlePlayerMode_ = false;
|
|
||||||
float swingTimer_ = 0.0f;
|
|
||||||
static constexpr float SWING_SPEED = 2.0f;
|
|
||||||
NpcDeathCallback npcDeathCallback_;
|
|
||||||
NpcRespawnCallback npcRespawnCallback_;
|
|
||||||
MeleeSwingCallback meleeSwingCallback_;
|
MeleeSwingCallback meleeSwingCallback_;
|
||||||
NpcSwingCallback npcSwingCallback_;
|
|
||||||
uint32_t localPlayerHealth_ = 0;
|
|
||||||
uint32_t localPlayerMaxHealth_ = 0;
|
|
||||||
uint32_t localPlayerLevel_ = 1;
|
|
||||||
bool playerDead_ = false;
|
bool playerDead_ = false;
|
||||||
|
|
||||||
struct NpcAggroEntry {
|
|
||||||
uint64_t guid;
|
|
||||||
float swingTimer;
|
|
||||||
};
|
|
||||||
std::vector<NpcAggroEntry> aggroList_;
|
|
||||||
|
|
||||||
void updateLocalCombat(float deltaTime);
|
|
||||||
void updateNpcAggro(float deltaTime);
|
|
||||||
void performPlayerSwing();
|
|
||||||
void performNpcSwing(uint64_t guid);
|
|
||||||
void handleNpcDeath(uint64_t guid);
|
|
||||||
void aggroNpc(uint64_t guid);
|
|
||||||
bool isNpcAggroed(uint64_t guid) const;
|
|
||||||
|
|
||||||
// ---- Single-player persistence ----
|
|
||||||
enum SinglePlayerDirty : uint32_t {
|
|
||||||
SP_DIRTY_NONE = 0,
|
|
||||||
SP_DIRTY_CHAR = 1 << 0,
|
|
||||||
SP_DIRTY_INVENTORY = 1 << 1,
|
|
||||||
SP_DIRTY_SPELLS = 1 << 2,
|
|
||||||
SP_DIRTY_ACTIONBAR = 1 << 3,
|
|
||||||
SP_DIRTY_AURAS = 1 << 4,
|
|
||||||
SP_DIRTY_QUESTS = 1 << 5,
|
|
||||||
SP_DIRTY_MONEY = 1 << 6,
|
|
||||||
SP_DIRTY_XP = 1 << 7,
|
|
||||||
SP_DIRTY_POSITION = 1 << 8,
|
|
||||||
SP_DIRTY_STATS = 1 << 9,
|
|
||||||
SP_DIRTY_SETTINGS = 1 << 10,
|
|
||||||
SP_DIRTY_ALL = 0xFFFFFFFFu
|
|
||||||
};
|
|
||||||
void markSinglePlayerDirty(uint32_t flags, bool highPriority);
|
|
||||||
void loadSinglePlayerCharacters();
|
|
||||||
void saveSinglePlayerCharacterState(bool force);
|
|
||||||
|
|
||||||
uint32_t spDirtyFlags_ = SP_DIRTY_NONE;
|
|
||||||
bool spDirtyHighPriority_ = false;
|
|
||||||
float spDirtyTimer_ = 0.0f;
|
|
||||||
float spPeriodicTimer_ = 0.0f;
|
|
||||||
float spLastDirtyX_ = 0.0f;
|
|
||||||
float spLastDirtyY_ = 0.0f;
|
|
||||||
float spLastDirtyZ_ = 0.0f;
|
|
||||||
float spLastDirtyOrientation_ = 0.0f;
|
|
||||||
std::unordered_map<uint64_t, bool> spHasState_;
|
|
||||||
std::unordered_map<uint64_t, float> spSavedOrientation_;
|
|
||||||
SinglePlayerSettings spSettings_{};
|
|
||||||
bool spSettingsLoaded_ = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,6 @@ public:
|
||||||
*/
|
*/
|
||||||
void setOnSuccess(std::function<void()> callback) { onSuccess = callback; }
|
void setOnSuccess(std::function<void()> callback) { onSuccess = callback; }
|
||||||
|
|
||||||
/**
|
|
||||||
* Set callback for single-player mode
|
|
||||||
*/
|
|
||||||
void setOnSinglePlayer(std::function<void()> callback) { onSinglePlayer = callback; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if authentication is in progress
|
* Check if authentication is in progress
|
||||||
|
|
@ -67,7 +63,6 @@ private:
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
std::function<void()> onSuccess;
|
std::function<void()> onSuccess;
|
||||||
std::function<void()> onSinglePlayer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt authentication
|
* Attempt authentication
|
||||||
|
|
|
||||||
|
|
@ -190,9 +190,6 @@ bool Application::initialize() {
|
||||||
void Application::run() {
|
void Application::run() {
|
||||||
LOG_INFO("Starting main loop");
|
LOG_INFO("Starting main loop");
|
||||||
|
|
||||||
// Terrain and character are loaded via startSinglePlayer() when the user
|
|
||||||
// picks single-player mode, so nothing is preloaded here.
|
|
||||||
|
|
||||||
auto lastTime = std::chrono::high_resolution_clock::now();
|
auto lastTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
while (running && !window->shouldClose()) {
|
while (running && !window->shouldClose()) {
|
||||||
|
|
@ -368,7 +365,7 @@ void Application::setState(AppState newState) {
|
||||||
if (renderer && renderer->getCameraController()) {
|
if (renderer && renderer->getCameraController()) {
|
||||||
auto* cc = renderer->getCameraController();
|
auto* cc = renderer->getCameraController();
|
||||||
cc->setMovementCallback([this](uint32_t opcode) {
|
cc->setMovementCallback([this](uint32_t opcode) {
|
||||||
if (gameHandler && !singlePlayerMode) {
|
if (gameHandler) {
|
||||||
gameHandler->sendMovement(static_cast<game::Opcode>(opcode));
|
gameHandler->sendMovement(static_cast<game::Opcode>(opcode));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -393,9 +390,7 @@ void Application::logoutToLogin() {
|
||||||
LOG_INFO("Logout requested");
|
LOG_INFO("Logout requested");
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
gameHandler->disconnect();
|
gameHandler->disconnect();
|
||||||
gameHandler->setSinglePlayerMode(false);
|
|
||||||
}
|
}
|
||||||
singlePlayerMode = false;
|
|
||||||
npcsSpawned = false;
|
npcsSpawned = false;
|
||||||
playerCharacterSpawned = false;
|
playerCharacterSpawned = false;
|
||||||
world.reset();
|
world.reset();
|
||||||
|
|
@ -448,10 +443,6 @@ void Application::update(float deltaTime) {
|
||||||
if (world) {
|
if (world) {
|
||||||
world->update(deltaTime);
|
world->update(deltaTime);
|
||||||
}
|
}
|
||||||
// Spawn/update local single-player NPCs.
|
|
||||||
if (!npcsSpawned && singlePlayerMode) {
|
|
||||||
spawnNpcs();
|
|
||||||
}
|
|
||||||
// Process deferred online creature spawns (throttled)
|
// Process deferred online creature spawns (throttled)
|
||||||
processCreatureSpawnQueue();
|
processCreatureSpawnQueue();
|
||||||
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
if (npcManager && renderer && renderer->getCharacterRenderer()) {
|
||||||
|
|
@ -471,7 +462,7 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send movement heartbeat every 500ms (keeps server position in sync)
|
// Send movement heartbeat every 500ms (keeps server position in sync)
|
||||||
if (gameHandler && renderer && !singlePlayerMode) {
|
if (gameHandler && renderer) {
|
||||||
movementHeartbeatTimer += deltaTime;
|
movementHeartbeatTimer += deltaTime;
|
||||||
if (movementHeartbeatTimer >= 0.5f) {
|
if (movementHeartbeatTimer >= 0.5f) {
|
||||||
movementHeartbeatTimer = 0.0f;
|
movementHeartbeatTimer = 0.0f;
|
||||||
|
|
@ -534,24 +525,6 @@ void Application::setupUICallbacks() {
|
||||||
setState(AppState::REALM_SELECTION);
|
setState(AppState::REALM_SELECTION);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Single-player mode callback — go to character creation first
|
|
||||||
uiManager->getAuthScreen().setOnSinglePlayer([this]() {
|
|
||||||
LOG_INFO("Single-player mode selected, opening character creation");
|
|
||||||
singlePlayerMode = true;
|
|
||||||
if (gameHandler) {
|
|
||||||
gameHandler->setSinglePlayerMode(true);
|
|
||||||
gameHandler->setSinglePlayerCharListReady();
|
|
||||||
}
|
|
||||||
// If characters exist, go to selection; otherwise go to creation
|
|
||||||
if (gameHandler && !gameHandler->getCharacters().empty()) {
|
|
||||||
setState(AppState::CHARACTER_SELECTION);
|
|
||||||
} else {
|
|
||||||
uiManager->getCharacterCreateScreen().reset();
|
|
||||||
uiManager->getCharacterCreateScreen().initializePreview(assetManager.get());
|
|
||||||
setState(AppState::CHARACTER_CREATION);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Realm selection callback
|
// Realm selection callback
|
||||||
uiManager->getRealmScreen().setOnRealmSelected([this](const std::string& realmName, const std::string& realmAddress) {
|
uiManager->getRealmScreen().setOnRealmSelected([this](const std::string& realmName, const std::string& realmAddress) {
|
||||||
LOG_INFO("Realm selected: ", realmName, " (", realmAddress, ")");
|
LOG_INFO("Realm selected: ", realmName, " (", realmAddress, ")");
|
||||||
|
|
@ -589,12 +562,8 @@ void Application::setupUICallbacks() {
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
gameHandler->setActiveCharacterGuid(characterGuid);
|
gameHandler->setActiveCharacterGuid(characterGuid);
|
||||||
}
|
}
|
||||||
if (singlePlayerMode) {
|
// Online mode - login will be handled by world entry callback
|
||||||
startSinglePlayer();
|
setState(AppState::IN_GAME);
|
||||||
} else {
|
|
||||||
// Online mode - login will be handled by world entry callback
|
|
||||||
setState(AppState::IN_GAME);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Character create screen callbacks
|
// Character create screen callbacks
|
||||||
|
|
@ -603,24 +572,13 @@ void Application::setupUICallbacks() {
|
||||||
});
|
});
|
||||||
|
|
||||||
uiManager->getCharacterCreateScreen().setOnCancel([this]() {
|
uiManager->getCharacterCreateScreen().setOnCancel([this]() {
|
||||||
if (singlePlayerMode) {
|
setState(AppState::CHARACTER_SELECTION);
|
||||||
setState(AppState::AUTHENTICATION);
|
|
||||||
singlePlayerMode = false;
|
|
||||||
gameHandler->setSinglePlayerMode(false);
|
|
||||||
} else {
|
|
||||||
setState(AppState::CHARACTER_SELECTION);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Character create result callback
|
// Character create result callback
|
||||||
gameHandler->setCharCreateCallback([this](bool success, const std::string& msg) {
|
gameHandler->setCharCreateCallback([this](bool success, const std::string& msg) {
|
||||||
if (success) {
|
if (success) {
|
||||||
if (singlePlayerMode) {
|
setState(AppState::CHARACTER_SELECTION);
|
||||||
// In single-player, go straight to character selection showing the new character
|
|
||||||
setState(AppState::CHARACTER_SELECTION);
|
|
||||||
} else {
|
|
||||||
setState(AppState::CHARACTER_SELECTION);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
uiManager->getCharacterCreateScreen().setStatus(msg, true);
|
uiManager->getCharacterCreateScreen().setStatus(msg, true);
|
||||||
}
|
}
|
||||||
|
|
@ -688,13 +646,7 @@ void Application::setupUICallbacks() {
|
||||||
|
|
||||||
// "Back" button on character screen
|
// "Back" button on character screen
|
||||||
uiManager->getCharacterScreen().setOnBack([this]() {
|
uiManager->getCharacterScreen().setOnBack([this]() {
|
||||||
if (singlePlayerMode) {
|
setState(AppState::REALM_SELECTION);
|
||||||
setState(AppState::AUTHENTICATION);
|
|
||||||
singlePlayerMode = false;
|
|
||||||
gameHandler->setSinglePlayerMode(false);
|
|
||||||
} else {
|
|
||||||
setState(AppState::REALM_SELECTION);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// "Delete Character" button on character screen
|
// "Delete Character" button on character screen
|
||||||
|
|
@ -709,11 +661,7 @@ void Application::setupUICallbacks() {
|
||||||
if (success) {
|
if (success) {
|
||||||
uiManager->getCharacterScreen().setStatus("Character deleted.");
|
uiManager->getCharacterScreen().setStatus("Character deleted.");
|
||||||
// Refresh character list
|
// Refresh character list
|
||||||
if (singlePlayerMode) {
|
gameHandler->requestCharacterList();
|
||||||
gameHandler->setSinglePlayerCharListReady();
|
|
||||||
} else {
|
|
||||||
gameHandler->requestCharacterList();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
uint8_t code = gameHandler ? gameHandler->getLastCharDeleteResult() : 0xFF;
|
uint8_t code = gameHandler ? gameHandler->getLastCharDeleteResult() : 0xFF;
|
||||||
uiManager->getCharacterScreen().setStatus(
|
uiManager->getCharacterScreen().setStatus(
|
||||||
|
|
@ -1282,472 +1230,6 @@ void Application::spawnNpcs() {
|
||||||
LOG_INFO("NPCs spawned for in-game session");
|
LOG_INFO("NPCs spawned for in-game session");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::startSinglePlayer() {
|
|
||||||
LOG_INFO("Starting single-player mode...");
|
|
||||||
|
|
||||||
// Set single-player flag
|
|
||||||
singlePlayerMode = true;
|
|
||||||
|
|
||||||
// Enable single-player combat mode on game handler
|
|
||||||
if (gameHandler) {
|
|
||||||
gameHandler->setSinglePlayerMode(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create world object for single-player
|
|
||||||
if (!world) {
|
|
||||||
world = std::make_unique<game::World>();
|
|
||||||
LOG_INFO("Single-player world created");
|
|
||||||
}
|
|
||||||
|
|
||||||
const game::Character* activeChar = gameHandler ? gameHandler->getActiveCharacter() : nullptr;
|
|
||||||
if (!activeChar && gameHandler) {
|
|
||||||
activeChar = gameHandler->getFirstCharacter();
|
|
||||||
if (activeChar) {
|
|
||||||
gameHandler->setActiveCharacterGuid(activeChar->guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!activeChar) {
|
|
||||||
LOG_ERROR("Single-player start: no character selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
spRace_ = activeChar->race;
|
|
||||||
spGender_ = activeChar->gender;
|
|
||||||
spClass_ = activeChar->characterClass;
|
|
||||||
spMapId_ = activeChar->mapId;
|
|
||||||
spZoneId_ = activeChar->zoneId;
|
|
||||||
spSpawnCanonical_ = glm::vec3(activeChar->x, activeChar->y, activeChar->z);
|
|
||||||
spYawDeg_ = 0.0f;
|
|
||||||
spPitchDeg_ = -5.0f;
|
|
||||||
|
|
||||||
bool loadedState = false;
|
|
||||||
if (gameHandler) {
|
|
||||||
gameHandler->setPlayerGuid(activeChar->guid);
|
|
||||||
loadedState = gameHandler->loadSinglePlayerCharacterState(activeChar->guid);
|
|
||||||
if (loadedState) {
|
|
||||||
const auto& movement = gameHandler->getMovementInfo();
|
|
||||||
spSpawnCanonical_ = glm::vec3(movement.x, movement.y, movement.z);
|
|
||||||
spYawDeg_ = glm::degrees(movement.orientation);
|
|
||||||
spawnSnapToGround = true;
|
|
||||||
} else {
|
|
||||||
game::GameHandler::SinglePlayerCreateInfo createInfo;
|
|
||||||
bool hasCreate = gameHandler->getSinglePlayerCreateInfo(activeChar->race, activeChar->characterClass, createInfo);
|
|
||||||
if (hasCreate) {
|
|
||||||
spMapId_ = createInfo.mapId;
|
|
||||||
spZoneId_ = createInfo.zoneId;
|
|
||||||
spSpawnCanonical_ = glm::vec3(createInfo.x, createInfo.y, createInfo.z);
|
|
||||||
spYawDeg_ = glm::degrees(createInfo.orientation);
|
|
||||||
spPitchDeg_ = -5.0f;
|
|
||||||
spawnSnapToGround = true;
|
|
||||||
}
|
|
||||||
uint32_t level = std::max<uint32_t>(1, activeChar->level);
|
|
||||||
uint32_t maxHealth = 20 + level * 10;
|
|
||||||
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
|
||||||
gameHandler->applySinglePlayerStartData(activeChar->race, activeChar->characterClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameHandler && renderer && window) {
|
|
||||||
game::GameHandler::SinglePlayerSettings settings;
|
|
||||||
bool hasSettings = gameHandler->getSinglePlayerSettings(settings);
|
|
||||||
if (!hasSettings) {
|
|
||||||
settings.fullscreen = window->isFullscreen();
|
|
||||||
settings.vsync = window->isVsyncEnabled();
|
|
||||||
settings.shadows = renderer->areShadowsEnabled();
|
|
||||||
settings.resWidth = window->getWidth();
|
|
||||||
settings.resHeight = window->getHeight();
|
|
||||||
if (auto* music = renderer->getMusicManager()) {
|
|
||||||
settings.musicVolume = music->getVolume();
|
|
||||||
}
|
|
||||||
if (auto* footstep = renderer->getFootstepManager()) {
|
|
||||||
settings.sfxVolume = static_cast<int>(footstep->getVolumeScale() * 100.0f + 0.5f);
|
|
||||||
}
|
|
||||||
if (auto* cameraController = renderer->getCameraController()) {
|
|
||||||
settings.mouseSensitivity = cameraController->getMouseSensitivity();
|
|
||||||
settings.invertMouse = cameraController->isInvertMouse();
|
|
||||||
}
|
|
||||||
gameHandler->setSinglePlayerSettings(settings);
|
|
||||||
hasSettings = true;
|
|
||||||
}
|
|
||||||
if (hasSettings) {
|
|
||||||
window->setVsync(settings.vsync);
|
|
||||||
window->setFullscreen(settings.fullscreen);
|
|
||||||
if (settings.resWidth > 0 && settings.resHeight > 0) {
|
|
||||||
window->applyResolution(settings.resWidth, settings.resHeight);
|
|
||||||
}
|
|
||||||
renderer->setShadowsEnabled(settings.shadows);
|
|
||||||
if (auto* music = renderer->getMusicManager()) {
|
|
||||||
music->setVolume(settings.musicVolume);
|
|
||||||
}
|
|
||||||
float sfxScale = static_cast<float>(settings.sfxVolume) / 100.0f;
|
|
||||||
if (auto* footstep = renderer->getFootstepManager()) {
|
|
||||||
footstep->setVolumeScale(sfxScale);
|
|
||||||
}
|
|
||||||
if (auto* activity = renderer->getActivitySoundManager()) {
|
|
||||||
activity->setVolumeScale(sfxScale);
|
|
||||||
}
|
|
||||||
if (auto* cameraController = renderer->getCameraController()) {
|
|
||||||
cameraController->setMouseSensitivity(settings.mouseSensitivity);
|
|
||||||
cameraController->setInvertMouse(settings.invertMouse);
|
|
||||||
cameraController->startIntroPan(2.8f, 140.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Loading screen ---
|
|
||||||
rendering::LoadingScreen loadingScreen;
|
|
||||||
bool loadingScreenOk = loadingScreen.initialize();
|
|
||||||
|
|
||||||
// Helper: poll events (resize/quit), update progress bar, swap buffers
|
|
||||||
auto showProgress = [&](const char* msg, float progress) {
|
|
||||||
// Poll SDL events so resizing and quit work during loading
|
|
||||||
SDL_Event event;
|
|
||||||
while (SDL_PollEvent(&event)) {
|
|
||||||
if (event.type == SDL_QUIT) {
|
|
||||||
window->setShouldClose(true);
|
|
||||||
loadingScreen.shutdown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.type == SDL_WINDOWEVENT &&
|
|
||||||
event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
|
||||||
int w = event.window.data1;
|
|
||||||
int h = event.window.data2;
|
|
||||||
window->setSize(w, h);
|
|
||||||
glViewport(0, 0, w, h);
|
|
||||||
if (renderer && renderer->getCamera()) {
|
|
||||||
renderer->getCamera()->setAspectRatio(static_cast<float>(w) / h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!loadingScreenOk) return;
|
|
||||||
loadingScreen.setStatus(msg);
|
|
||||||
loadingScreen.setProgress(progress);
|
|
||||||
loadingScreen.render();
|
|
||||||
window->swapBuffers();
|
|
||||||
};
|
|
||||||
|
|
||||||
showProgress("Preparing world...", 0.0f);
|
|
||||||
|
|
||||||
const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN"));
|
|
||||||
// Canonical WoW coords: +X=North, +Y=West, +Z=Up
|
|
||||||
glm::vec3 spawnCanonical = spawnPreset ? spawnPreset->spawnCanonical : spSpawnCanonical_;
|
|
||||||
std::string mapName = spawnPreset ? spawnPreset->mapName : mapIdToName(spMapId_);
|
|
||||||
float spawnYaw = spawnPreset ? spawnPreset->yawDeg : spYawDeg_;
|
|
||||||
float spawnPitch = spawnPreset ? spawnPreset->pitchDeg : spPitchDeg_;
|
|
||||||
spawnSnapToGround = spawnPreset ? spawnPreset->snapToGround : spawnSnapToGround;
|
|
||||||
|
|
||||||
if (auto envSpawnPos = parseVec3Csv(std::getenv("WOW_SPAWN_POS"))) {
|
|
||||||
spawnCanonical = *envSpawnPos;
|
|
||||||
LOG_INFO("Using WOW_SPAWN_POS override (canonical WoW X,Y,Z): (",
|
|
||||||
spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")");
|
|
||||||
}
|
|
||||||
if (auto envSpawnRot = parseYawPitchCsv(std::getenv("WOW_SPAWN_ROT"))) {
|
|
||||||
spawnYaw = envSpawnRot->first;
|
|
||||||
spawnPitch = envSpawnRot->second;
|
|
||||||
LOG_INFO("Using WOW_SPAWN_ROT override: yaw=", spawnYaw, " pitch=", spawnPitch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert canonical WoW → engine rendering coordinates (swap X/Y)
|
|
||||||
glm::vec3 spawnRender = core::coords::canonicalToRender(spawnCanonical);
|
|
||||||
if (renderer && renderer->getCameraController()) {
|
|
||||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, spawnYaw, spawnPitch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameHandler && !loadedState) {
|
|
||||||
gameHandler->setPosition(spawnCanonical.x, spawnCanonical.y, spawnCanonical.z);
|
|
||||||
gameHandler->setOrientation(glm::radians(spawnYaw - 90.0f));
|
|
||||||
gameHandler->flushSinglePlayerSave();
|
|
||||||
}
|
|
||||||
if (spawnPreset) {
|
|
||||||
LOG_INFO("Single-player spawn preset: ", spawnPreset->label,
|
|
||||||
" canonical=(",
|
|
||||||
spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z,
|
|
||||||
") (set WOW_SPAWN to change)");
|
|
||||||
LOG_INFO("Optional spawn overrides (canonical WoW X,Y,Z): WOW_SPAWN_POS=x,y,z WOW_SPAWN_ROT=yaw,pitch");
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgress("Loading character model...", 0.05f);
|
|
||||||
|
|
||||||
// Spawn player character (loads M2 model, skin, textures, animations, weapons)
|
|
||||||
spawnPlayerCharacter();
|
|
||||||
|
|
||||||
showProgress("Loading terrain...", 0.25f);
|
|
||||||
|
|
||||||
// Set map name for zone-specific floor cache
|
|
||||||
if (renderer->getWMORenderer()) {
|
|
||||||
renderer->getWMORenderer()->setMapName(mapName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load test terrain if WOW_DATA_PATH is set
|
|
||||||
bool terrainOk = false;
|
|
||||||
if (renderer && assetManager && assetManager->isInitialized()) {
|
|
||||||
// Compute ADT path from canonical spawn coordinates
|
|
||||||
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
|
||||||
std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
|
||||||
std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt";
|
|
||||||
LOG_INFO("Initial ADT tile [", tileX, ",", tileY, "] from canonical position");
|
|
||||||
terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
|
|
||||||
if (!terrainOk) {
|
|
||||||
LOG_WARNING("Could not load test terrain - atmospheric rendering only");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgress("Streaming terrain tiles...", 0.40f);
|
|
||||||
|
|
||||||
// Wait for surrounding terrain tiles to stream in
|
|
||||||
if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) {
|
|
||||||
auto* terrainMgr = renderer->getTerrainManager();
|
|
||||||
auto* camera = renderer->getCamera();
|
|
||||||
|
|
||||||
// First update with large dt to trigger streamTiles() immediately
|
|
||||||
terrainMgr->update(*camera, 1.0f);
|
|
||||||
|
|
||||||
auto startTime = std::chrono::high_resolution_clock::now();
|
|
||||||
const float maxWaitSeconds = 15.0f;
|
|
||||||
|
|
||||||
int initialRemaining = terrainMgr->getRemainingTileCount();
|
|
||||||
|
|
||||||
while (terrainMgr->getRemainingTileCount() > 0) {
|
|
||||||
// Poll events to keep window responsive
|
|
||||||
SDL_Event event;
|
|
||||||
while (SDL_PollEvent(&event)) {
|
|
||||||
if (event.type == SDL_QUIT) {
|
|
||||||
window->setShouldClose(true);
|
|
||||||
loadingScreen.shutdown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.type == SDL_WINDOWEVENT &&
|
|
||||||
event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
|
||||||
int w = event.window.data1;
|
|
||||||
int h = event.window.data2;
|
|
||||||
window->setSize(w, h);
|
|
||||||
glViewport(0, 0, w, h);
|
|
||||||
if (renderer && renderer->getCamera()) {
|
|
||||||
renderer->getCamera()->setAspectRatio(static_cast<float>(w) / h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process ready tiles from worker threads
|
|
||||||
terrainMgr->update(*camera, 0.016f);
|
|
||||||
terrainMgr->processAllReadyTiles();
|
|
||||||
|
|
||||||
// Update loading screen with tile progress (40% - 85% range)
|
|
||||||
if (loadingScreenOk) {
|
|
||||||
int loaded = terrainMgr->getLoadedTileCount();
|
|
||||||
int remaining = terrainMgr->getRemainingTileCount();
|
|
||||||
float tileProgress = (initialRemaining > 0)
|
|
||||||
? static_cast<float>(initialRemaining - remaining) / initialRemaining
|
|
||||||
: 1.0f;
|
|
||||||
float progress = 0.40f + tileProgress * 0.45f;
|
|
||||||
char buf[128];
|
|
||||||
snprintf(buf, sizeof(buf), "Loading terrain... %d tiles loaded, %d remaining",
|
|
||||||
loaded, remaining);
|
|
||||||
loadingScreen.setStatus(buf);
|
|
||||||
loadingScreen.setProgress(progress);
|
|
||||||
loadingScreen.render();
|
|
||||||
window->swapBuffers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeout safety
|
|
||||||
auto elapsed = std::chrono::high_resolution_clock::now() - startTime;
|
|
||||||
if (std::chrono::duration<float>(elapsed).count() > maxWaitSeconds) {
|
|
||||||
LOG_WARNING("Terrain streaming timeout after ", maxWaitSeconds, "s");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Delay(16); // ~60fps cap for loading screen
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO("Terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded");
|
|
||||||
|
|
||||||
showProgress("Building collision cache...", 0.88f);
|
|
||||||
|
|
||||||
// Load zone-specific floor cache, or precompute if none exists
|
|
||||||
if (renderer->getWMORenderer()) {
|
|
||||||
renderer->getWMORenderer()->loadFloorCache();
|
|
||||||
if (renderer->getWMORenderer()->getFloorCacheSize() == 0) {
|
|
||||||
renderer->getWMORenderer()->precomputeFloorCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-snap camera to ground now that all surrounding tiles are loaded
|
|
||||||
// (the initial reset inside loadTestTerrain only had 1 tile).
|
|
||||||
if (spawnSnapToGround && renderer->getCameraController()) {
|
|
||||||
renderer->getCameraController()->reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgress("Entering world...", 0.95f);
|
|
||||||
|
|
||||||
// Final camera reset: now that follow target exists and terrain is loaded,
|
|
||||||
// snap the third-person camera into the correct orbit position.
|
|
||||||
if (spawnSnapToGround && renderer && renderer->getCameraController()) {
|
|
||||||
renderer->getCameraController()->reset();
|
|
||||||
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgress("Entering world...", 1.0f);
|
|
||||||
|
|
||||||
if (loadingScreenOk) {
|
|
||||||
loadingScreen.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire hearthstone to camera reset (teleport home) in single-player
|
|
||||||
if (gameHandler && renderer && renderer->getCameraController()) {
|
|
||||||
auto* camCtrl = renderer->getCameraController();
|
|
||||||
gameHandler->setHearthstoneCallback([camCtrl]() {
|
|
||||||
camCtrl->reset();
|
|
||||||
camCtrl->startIntroPan(2.8f, 140.0f);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go directly to game
|
|
||||||
setState(AppState::IN_GAME);
|
|
||||||
// Emulate server MOTD in single-player (after entering game)
|
|
||||||
if (gameHandler) {
|
|
||||||
std::vector<std::string> motdLines;
|
|
||||||
if (const char* motdEnv = std::getenv("WOW_SP_MOTD")) {
|
|
||||||
std::string raw = motdEnv;
|
|
||||||
size_t start = 0;
|
|
||||||
while (start <= raw.size()) {
|
|
||||||
size_t pos = raw.find('|', start);
|
|
||||||
if (pos == std::string::npos) pos = raw.size();
|
|
||||||
std::string line = raw.substr(start, pos - start);
|
|
||||||
if (!line.empty()) motdLines.push_back(line);
|
|
||||||
start = pos + 1;
|
|
||||||
if (pos == raw.size()) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (motdLines.empty()) {
|
|
||||||
motdLines.push_back("Wowee Single Player");
|
|
||||||
}
|
|
||||||
gameHandler->simulateMotd(motdLines);
|
|
||||||
}
|
|
||||||
LOG_INFO("Single-player mode started - press F1 for performance HUD");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::teleportTo(int presetIndex) {
|
|
||||||
// Guard: only in single-player + IN_GAME state
|
|
||||||
if (!singlePlayerMode || state != AppState::IN_GAME) return;
|
|
||||||
if (presetIndex < 0 || presetIndex >= SPAWN_PRESET_COUNT) return;
|
|
||||||
|
|
||||||
const auto& preset = SPAWN_PRESETS[presetIndex];
|
|
||||||
LOG_INFO("Teleporting to: ", preset.label);
|
|
||||||
spawnSnapToGround = preset.snapToGround;
|
|
||||||
|
|
||||||
// Convert canonical WoW → engine rendering coordinates (swap X/Y)
|
|
||||||
glm::vec3 spawnRender = core::coords::canonicalToRender(preset.spawnCanonical);
|
|
||||||
|
|
||||||
// Update camera default spawn
|
|
||||||
if (renderer && renderer->getCameraController()) {
|
|
||||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, preset.yawDeg, preset.pitchDeg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current map's floor cache before unloading
|
|
||||||
if (renderer && renderer->getWMORenderer()) {
|
|
||||||
auto* wmo = renderer->getWMORenderer();
|
|
||||||
if (wmo->getFloorCacheSize() > 0) {
|
|
||||||
wmo->saveFloorCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unload all current terrain
|
|
||||||
if (renderer && renderer->getTerrainManager()) {
|
|
||||||
renderer->getTerrainManager()->unloadAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute ADT path from canonical spawn coordinates
|
|
||||||
auto [tileX, tileY] = core::coords::canonicalToTile(preset.spawnCanonical.x, preset.spawnCanonical.y);
|
|
||||||
std::string mapName = preset.mapName;
|
|
||||||
std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
|
||||||
std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt";
|
|
||||||
LOG_INFO("Teleport ADT tile [", tileX, ",", tileY, "]");
|
|
||||||
|
|
||||||
// Set map name on terrain manager and WMO renderer
|
|
||||||
if (renderer && renderer->getTerrainManager()) {
|
|
||||||
renderer->getTerrainManager()->setMapName(mapName);
|
|
||||||
}
|
|
||||||
if (renderer && renderer->getWMORenderer()) {
|
|
||||||
renderer->getWMORenderer()->setMapName(mapName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the initial tile
|
|
||||||
bool terrainOk = false;
|
|
||||||
if (renderer && assetManager && assetManager->isInitialized()) {
|
|
||||||
terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream surrounding tiles
|
|
||||||
if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) {
|
|
||||||
auto* terrainMgr = renderer->getTerrainManager();
|
|
||||||
auto* camera = renderer->getCamera();
|
|
||||||
|
|
||||||
terrainMgr->update(*camera, 1.0f);
|
|
||||||
|
|
||||||
auto startTime = std::chrono::high_resolution_clock::now();
|
|
||||||
const float maxWaitSeconds = 8.0f;
|
|
||||||
|
|
||||||
while (terrainMgr->getRemainingTileCount() > 0) {
|
|
||||||
SDL_Event event;
|
|
||||||
while (SDL_PollEvent(&event)) {
|
|
||||||
if (event.type == SDL_QUIT) {
|
|
||||||
window->setShouldClose(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
terrainMgr->update(*camera, 0.016f);
|
|
||||||
terrainMgr->processAllReadyTiles();
|
|
||||||
|
|
||||||
auto elapsed = std::chrono::high_resolution_clock::now() - startTime;
|
|
||||||
if (std::chrono::duration<float>(elapsed).count() > maxWaitSeconds) {
|
|
||||||
LOG_WARNING("Teleport terrain streaming timeout after ", maxWaitSeconds, "s");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Delay(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO("Teleport terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded");
|
|
||||||
|
|
||||||
// Load zone-specific floor cache, or precompute if none exists
|
|
||||||
if (renderer->getWMORenderer()) {
|
|
||||||
renderer->getWMORenderer()->loadFloorCache();
|
|
||||||
if (renderer->getWMORenderer()->getFloorCacheSize() == 0) {
|
|
||||||
renderer->getWMORenderer()->precomputeFloorCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Floor-snapping presets use camera reset. WMO-floor presets keep explicit Z.
|
|
||||||
if (spawnSnapToGround && renderer && renderer->getCameraController()) {
|
|
||||||
renderer->getCameraController()->reset();
|
|
||||||
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!spawnSnapToGround && renderer) {
|
|
||||||
renderer->getCharacterPosition() = spawnRender;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync final character position to game handler
|
|
||||||
if (renderer && gameHandler) {
|
|
||||||
glm::vec3 finalRender = renderer->getCharacterPosition();
|
|
||||||
glm::vec3 finalCanonical = core::coords::renderToCanonical(finalRender);
|
|
||||||
gameHandler->setPosition(finalCanonical.x, finalCanonical.y, finalCanonical.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild nearby NPC set for the new location.
|
|
||||||
if (singlePlayerMode && gameHandler && renderer && renderer->getCharacterRenderer()) {
|
|
||||||
if (npcManager) {
|
|
||||||
npcManager->clear(renderer->getCharacterRenderer(), &gameHandler->getEntityManager());
|
|
||||||
}
|
|
||||||
npcsSpawned = false;
|
|
||||||
spawnNpcs();
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO("Teleport to ", preset.label, " complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
||||||
if (!assetManager || !assetManager->isInitialized() || !gameHandler) return;
|
if (!assetManager || !assetManager->isInitialized() || !gameHandler) return;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
4921
src/game/game_handler.cpp.bak
Normal file
4921
src/game/game_handler.cpp.bak
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -214,21 +214,6 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Single-player mode button
|
|
||||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Single-Player Mode");
|
|
||||||
ImGui::TextWrapped("Skip server connection and play offline with local rendering.");
|
|
||||||
|
|
||||||
if (ImGui::Button("Start Single Player", ImVec2(240, 30))) {
|
|
||||||
// Call single-player callback
|
|
||||||
if (onSinglePlayer) {
|
|
||||||
onSinglePlayer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Info text
|
// Info text
|
||||||
ImGui::TextWrapped("Enter your account credentials to connect to the authentication server.");
|
ImGui::TextWrapped("Enter your account credentials to connect to the authentication server.");
|
||||||
ImGui::TextWrapped("Default port is 3724.");
|
ImGui::TextWrapped("Default port is 3724.");
|
||||||
|
|
|
||||||
|
|
@ -154,10 +154,8 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||||
ss << "Entering world with " << character.name << "...";
|
ss << "Entering world with " << character.name << "...";
|
||||||
setStatus(ss.str());
|
setStatus(ss.str());
|
||||||
|
|
||||||
// Only send CMSG_PLAYER_LOGIN in online mode
|
// Send CMSG_PLAYER_LOGIN to server
|
||||||
if (!gameHandler.isSinglePlayerMode()) {
|
gameHandler.selectCharacter(character.guid);
|
||||||
gameHandler.selectCharacter(character.guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call callback
|
// Call callback
|
||||||
if (onCharacterSelected) {
|
if (onCharacterSelected) {
|
||||||
|
|
|
||||||
|
|
@ -673,15 +673,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
auto unit = std::static_pointer_cast<game::Unit>(target);
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||||
gameHandler.lootTarget(target->getGuid());
|
gameHandler.lootTarget(target->getGuid());
|
||||||
} else if (gameHandler.isSinglePlayerMode()) {
|
|
||||||
// Single-player: interact with friendly NPCs, otherwise attack
|
|
||||||
if (!unit->isHostile() && unit->isInteractable()) {
|
|
||||||
gameHandler.interactWithNpc(target->getGuid());
|
|
||||||
} else {
|
|
||||||
gameHandler.startAutoAttack(target->getGuid());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Online mode: interact with friendly NPCs, otherwise attack
|
// Interact with friendly NPCs, otherwise attack
|
||||||
if (!unit->isHostile() && unit->isInteractable()) {
|
if (!unit->isHostile() && unit->isInteractable()) {
|
||||||
gameHandler.interactWithNpc(target->getGuid());
|
gameHandler.interactWithNpc(target->getGuid());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -761,12 +754,6 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override with local player stats in single-player mode
|
|
||||||
if (gameHandler.isSinglePlayerMode() && gameHandler.getLocalPlayerMaxHealth() > 0) {
|
|
||||||
playerHp = gameHandler.getLocalPlayerHealth();
|
|
||||||
playerMaxHp = gameHandler.getLocalPlayerMaxHealth();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health bar
|
// Health bar
|
||||||
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
||||||
ImVec4 hpColor = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
|
ImVec4 hpColor = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
|
||||||
|
|
@ -2789,27 +2776,6 @@ void GameScreen::renderSettingsWindow() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (auto* gameHandler = core::Application::getInstance().getGameHandler()) {
|
|
||||||
if (gameHandler->isSinglePlayerMode()) {
|
|
||||||
game::GameHandler::SinglePlayerSettings spSettings;
|
|
||||||
if (gameHandler->getSinglePlayerSettings(spSettings)) {
|
|
||||||
pendingFullscreen = spSettings.fullscreen;
|
|
||||||
pendingVsync = spSettings.vsync;
|
|
||||||
pendingShadows = spSettings.shadows;
|
|
||||||
pendingMusicVolume = spSettings.musicVolume;
|
|
||||||
pendingSfxVolume = spSettings.sfxVolume;
|
|
||||||
pendingMouseSensitivity = spSettings.mouseSensitivity;
|
|
||||||
pendingInvertMouse = spSettings.invertMouse;
|
|
||||||
for (int i = 0; i < kResCount; i++) {
|
|
||||||
if (kResolutions[i][0] == spSettings.resWidth &&
|
|
||||||
kResolutions[i][1] == spSettings.resHeight) {
|
|
||||||
pendingResIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingUiOpacity = static_cast<int>(uiOpacity_ * 100.0f + 0.5f);
|
pendingUiOpacity = static_cast<int>(uiOpacity_ * 100.0f + 0.5f);
|
||||||
settingsInit = true;
|
settingsInit = true;
|
||||||
}
|
}
|
||||||
|
|
@ -2909,21 +2875,6 @@ void GameScreen::renderSettingsWindow() {
|
||||||
cameraController->setInvertMouse(pendingInvertMouse);
|
cameraController->setInvertMouse(pendingInvertMouse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (auto* gameHandler = core::Application::getInstance().getGameHandler()) {
|
|
||||||
if (gameHandler->isSinglePlayerMode()) {
|
|
||||||
game::GameHandler::SinglePlayerSettings spSettings;
|
|
||||||
spSettings.fullscreen = pendingFullscreen;
|
|
||||||
spSettings.vsync = pendingVsync;
|
|
||||||
spSettings.shadows = pendingShadows;
|
|
||||||
spSettings.resWidth = kResolutions[pendingResIndex][0];
|
|
||||||
spSettings.resHeight = kResolutions[pendingResIndex][1];
|
|
||||||
spSettings.musicVolume = pendingMusicVolume;
|
|
||||||
spSettings.sfxVolume = pendingSfxVolume;
|
|
||||||
spSettings.mouseSensitivity = pendingMouseSensitivity;
|
|
||||||
spSettings.invertMouse = pendingInvertMouse;
|
|
||||||
gameHandler->setSinglePlayerSettings(spSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
||||||
|
|
|
||||||
|
|
@ -371,8 +371,7 @@ void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot
|
||||||
|
|
||||||
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
||||||
if (!holdingItem) return;
|
if (!holdingItem) return;
|
||||||
if (gameHandler_ && !gameHandler_->isSinglePlayerMode() &&
|
if (gameHandler_ && heldSource == HeldSource::EQUIPMENT) {
|
||||||
heldSource == HeldSource::EQUIPMENT) {
|
|
||||||
// Online mode: avoid client-side unequip; wait for server update.
|
// Online mode: avoid client-side unequip; wait for server update.
|
||||||
cancelPickup(inv);
|
cancelPickup(inv);
|
||||||
return;
|
return;
|
||||||
|
|
@ -394,7 +393,7 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
||||||
|
|
||||||
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||||
if (!holdingItem) return;
|
if (!holdingItem) return;
|
||||||
if (gameHandler_ && !gameHandler_->isSinglePlayerMode()) {
|
if (gameHandler_) {
|
||||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||||
// Online mode: request server auto-equip and keep local state intact.
|
// Online mode: request server auto-equip and keep local state intact.
|
||||||
gameHandler_->autoEquipItemBySlot(heldBackpackIndex);
|
gameHandler_->autoEquipItemBySlot(heldBackpackIndex);
|
||||||
|
|
@ -1101,7 +1100,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
inventoryDirty = true;
|
inventoryDirty = true;
|
||||||
}
|
}
|
||||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
if (gameHandler_ && !gameHandler_->isSinglePlayerMode()) {
|
if (gameHandler_) {
|
||||||
if (item.inventoryType > 0) {
|
if (item.inventoryType > 0) {
|
||||||
// Auto-equip (online)
|
// Auto-equip (online)
|
||||||
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
||||||
|
|
@ -1109,36 +1108,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
// Use consumable (online)
|
// Use consumable (online)
|
||||||
gameHandler_->useItemBySlot(backpackIndex);
|
gameHandler_->useItemBySlot(backpackIndex);
|
||||||
}
|
}
|
||||||
} else if (item.inventoryType > 0 || item.armor > 0 ||
|
|
||||||
!item.subclassName.empty()) {
|
|
||||||
// Auto-equip (single-player)
|
|
||||||
uint8_t equippingType = item.inventoryType;
|
|
||||||
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
|
|
||||||
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
|
|
||||||
const auto& eqSlot = inventory.getEquipSlot(targetSlot);
|
|
||||||
if (eqSlot.empty()) {
|
|
||||||
inventory.setEquipSlot(targetSlot, item);
|
|
||||||
inventory.clearBackpackSlot(backpackIndex);
|
|
||||||
} else {
|
|
||||||
game::ItemDef equippedItem = eqSlot.item;
|
|
||||||
inventory.setEquipSlot(targetSlot, item);
|
|
||||||
inventory.setBackpackSlot(backpackIndex, equippedItem);
|
|
||||||
}
|
|
||||||
if (targetSlot == game::EquipSlot::MAIN_HAND && equippingType == 17) {
|
|
||||||
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
|
|
||||||
if (!offHand.empty()) {
|
|
||||||
inventory.addItem(offHand.item);
|
|
||||||
inventory.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targetSlot == game::EquipSlot::OFF_HAND &&
|
|
||||||
inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
|
||||||
inventory.addItem(inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
|
||||||
inventory.clearEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
||||||
}
|
|
||||||
equipmentDirty = true;
|
|
||||||
inventoryDirty = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue