diff --git a/assets/shaders/postprocess.fs b/assets/shaders/postprocess.fs index 2f9d3a1..44d99f9 100644 --- a/assets/shaders/postprocess.fs +++ b/assets/shaders/postprocess.fs @@ -11,6 +11,7 @@ uniform vec4 colDiffuse; uniform float u_time; uniform float u_glitchIntensity; uniform vec2 u_resolution; +uniform int u_preset; // Output fragment color out vec4 finalColor; @@ -20,7 +21,7 @@ void main() vec2 uv = fragTexCoord; // 1. Digital Glitch / Tactical Chromatic Aberration - float glitch = clamp(u_glitchIntensity, 0.0, 1.0); + float glitch = (u_preset >= 2) ? clamp(u_glitchIntensity, 0.0, 1.0) : 0.0; if (glitch > 0.001) { // Horizontal line slice displacement @@ -66,9 +67,12 @@ void main() float vignette = smoothstep(0.78, 0.32, d); finalColor.rgb *= mix(0.40, 1.0, vignette); - // 4. Subtle Tactical CRT Scanlines - float scanline = sin(uv.y * u_resolution.y * 1.5) * 0.025; - finalColor.rgb -= scanline; + // 4. Subtle Tactical CRT Scanlines (Ultra mode only) + if (u_preset >= 2) + { + float scanline = sin(uv.y * u_resolution.y * 1.5) * 0.025; + finalColor.rgb -= scanline; + } finalColor.a = 1.0; } diff --git a/src/core/Common.hpp b/src/core/Common.hpp index c0ea284..e2b53a0 100644 --- a/src/core/Common.hpp +++ b/src/core/Common.hpp @@ -23,6 +23,12 @@ enum class GameState { VICTORY_LEVEL }; +enum class GraphicsQuality { + PERFORMANCE, // iGPU / Battery: Direct backbuffer render, no offscreen/postprocess overhead, max culling + BALANCED, // Standard: 720p with post-processing pass + ULTRA // Max: Full post-process pass with scanlines and glitch +}; + enum class AmmoType { EMPTY, KINETIC, // Pierce / Raw physical diff --git a/src/core/Game.cpp b/src/core/Game.cpp index a55d506..36b0f79 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -16,6 +16,7 @@ Game::~Game() { void Game::init() { SoundManager::get().init(); m_renderer.init(); + m_renderer.setGraphicsQuality(SoundManager::get().getGraphicsQuality()); m_state = GameState::MAIN_MENU; SoundManager::get().playTrack(MusicTrack::MENU_HUB); } @@ -1004,14 +1005,15 @@ void Game::updateSettings(float dt) { float mVol = sm.getMasterVolume(); float muVol = sm.getMusicVolume(); float sVol = sm.getSfxVolume(); + GraphicsQuality gQuality = sm.getGraphicsQuality(); - // Keyboard selection & adjustment + // Keyboard selection (4 items: 0=Master, 1=Music, 2=SFX, 3=Graphics Preset) if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) { - m_selectedSlider = (m_selectedSlider + 2) % 3; + m_selectedSlider = (m_selectedSlider + 3) % 4; sm.playTumblerClick(); } if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) { - m_selectedSlider = (m_selectedSlider + 1) % 3; + m_selectedSlider = (m_selectedSlider + 1) % 4; sm.playTumblerClick(); } @@ -1019,27 +1021,39 @@ void Game::updateSettings(float dt) { if (m_selectedSlider == 0) sm.setMasterVolume(mVol - 0.05f); else if (m_selectedSlider == 1) sm.setMusicVolume(muVol - 0.05f); else if (m_selectedSlider == 2) sm.setSfxVolume(sVol - 0.05f); + else if (m_selectedSlider == 3) { + int qVal = (static_cast(gQuality) + 2) % 3; + auto newQ = static_cast(qVal); + sm.setGraphicsQuality(newQ); + m_renderer.setGraphicsQuality(newQ); + } sm.playTumblerClick(); } if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) { if (m_selectedSlider == 0) sm.setMasterVolume(mVol + 0.05f); else if (m_selectedSlider == 1) sm.setMusicVolume(muVol + 0.05f); else if (m_selectedSlider == 2) sm.setSfxVolume(sVol + 0.05f); + else if (m_selectedSlider == 3) { + int qVal = (static_cast(gQuality) + 1) % 3; + auto newQ = static_cast(qVal); + sm.setGraphicsQuality(newQ); + m_renderer.setGraphicsQuality(newQ); + } sm.playTumblerClick(); } // Mouse Interaction Vector2 mouse = GetMousePosition(); - int menuW = 580; - int menuH = 430; + int menuW = 620; + int menuH = 495; int menuX = (GetScreenWidth() - menuW) / 2; int menuY = (GetScreenHeight() - menuH) / 2; - int startY = menuY + 85; + int startY = menuY + 76; int sliderW = 340; - int sliderH = 26; - int sliderX = menuX + 180; + int sliderH = 24; + int sliderX = menuX + 200; - Rectangle btnBack = { static_cast(menuX + (menuW - 240) / 2), static_cast(menuY + menuH - 65), 240.0f, 44.0f }; + Rectangle btnBack = { static_cast(menuX + (menuW - 240) / 2), static_cast(menuY + menuH - 60), 240.0f, 40.0f }; if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { if (CheckCollisionPointRec(mouse, btnBack)) { @@ -1048,8 +1062,9 @@ void Game::updateSettings(float dt) { return; } + // Check audio sliders (0, 1, 2) for (int i = 0; i < 3; ++i) { - int itemY = startY + i * 78; + int itemY = startY + i * 68; Rectangle sBar = { static_cast(sliderX - 10), static_cast(itemY - 6), static_cast(sliderW + 20), static_cast(sliderH + 12) }; if (CheckCollisionPointRec(mouse, sBar)) { m_selectedSlider = i; @@ -1062,6 +1077,28 @@ void Game::updateSettings(float dt) { break; } } + + // Check graphics preset (item 3) + int item3Y = startY + 3 * 68; + Rectangle btnLeft = { static_cast(sliderX), static_cast(item3Y - 2), 28.0f, static_cast(sliderH + 4) }; + Rectangle btnRight = { static_cast(sliderX + sliderW - 28), static_cast(item3Y - 2), 28.0f, static_cast(sliderH + 4) }; + Rectangle nameBox = { static_cast(sliderX + 34), static_cast(item3Y - 2), static_cast(sliderW - 68), static_cast(sliderH + 4) }; + + if (CheckCollisionPointRec(mouse, btnLeft)) { + m_selectedSlider = 3; + int qVal = (static_cast(gQuality) + 2) % 3; + auto newQ = static_cast(qVal); + sm.setGraphicsQuality(newQ); + m_renderer.setGraphicsQuality(newQ); + sm.playTumblerClick(); + } else if (CheckCollisionPointRec(mouse, btnRight) || CheckCollisionPointRec(mouse, nameBox)) { + m_selectedSlider = 3; + int qVal = (static_cast(gQuality) + 1) % 3; + auto newQ = static_cast(qVal); + sm.setGraphicsQuality(newQ); + m_renderer.setGraphicsQuality(newQ); + sm.playTumblerClick(); + } } if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && m_isDraggingSlider) { @@ -1168,6 +1205,7 @@ void Game::draw() { m_uiRenderer.drawSettingsMenu(SoundManager::get().getMasterVolume(), SoundManager::get().getMusicVolume(), SoundManager::get().getSfxVolume(), + SoundManager::get().getGraphicsQuality(), m_selectedSlider); break; diff --git a/src/core/SoundManager.cpp b/src/core/SoundManager.cpp index dd8116d..8925f39 100644 --- a/src/core/SoundManager.cpp +++ b/src/core/SoundManager.cpp @@ -224,6 +224,11 @@ void SoundManager::setSfxVolume(float vol) { saveSettings(); } +void SoundManager::setGraphicsQuality(GraphicsQuality q) { + m_graphicsQuality = q; + saveSettings(); +} + void SoundManager::saveSettings(const std::string& path) { std::ofstream out(path); if (!out.is_open()) return; @@ -231,6 +236,7 @@ void SoundManager::saveSettings(const std::string& path) { out << "master " << m_volMaster << "\n"; out << "music " << m_volMusic << "\n"; out << "sfx " << m_volSfx << "\n"; + out << "graphics " << static_cast(m_graphicsQuality) << "\n"; } void SoundManager::loadSettings(const std::string& path) { @@ -239,6 +245,7 @@ void SoundManager::loadSettings(const std::string& path) { m_volMaster = 1.0f; m_volMusic = 0.7f; m_volSfx = 0.9f; + m_graphicsQuality = GraphicsQuality::BALANCED; return; } @@ -251,6 +258,12 @@ void SoundManager::loadSettings(const std::string& path) { if (key == "master") in >> m_volMaster; else if (key == "music") in >> m_volMusic; else if (key == "sfx") in >> m_volSfx; + else if (key == "graphics") { + int gVal = 1; + in >> gVal; + gVal = std::clamp(gVal, 0, 2); + m_graphicsQuality = static_cast(gVal); + } } m_volMaster = std::clamp(m_volMaster, 0.0f, 1.0f); diff --git a/src/core/SoundManager.hpp b/src/core/SoundManager.hpp index 6930a71..79822d4 100644 --- a/src/core/SoundManager.hpp +++ b/src/core/SoundManager.hpp @@ -1,6 +1,7 @@ #pragma once #include "raylib.h" +#include "core/Common.hpp" #include #include @@ -28,10 +29,12 @@ public: float getMasterVolume() const { return m_volMaster; } float getMusicVolume() const { return m_volMusic; } float getSfxVolume() const { return m_volSfx; } + GraphicsQuality getGraphicsQuality() const { return m_graphicsQuality; } void setMasterVolume(float vol); void setMusicVolume(float vol); void setSfxVolume(float vol); + void setGraphicsQuality(GraphicsQuality q); void saveSettings(const std::string& path = "settings.cfg"); void loadSettings(const std::string& path = "settings.cfg"); @@ -72,6 +75,7 @@ private: float m_volMaster = 1.0f; float m_volMusic = 0.7f; float m_volSfx = 0.9f; + GraphicsQuality m_graphicsQuality = GraphicsQuality::BALANCED; // Music streaming state MusicTrack m_currentTrack = MusicTrack::NONE; diff --git a/src/main.cpp b/src/main.cpp index f15b091..ecdb1d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ int main() { const int screenWidth = 1280; const int screenHeight = 720; - SetConfigFlags(FLAG_VSYNC_HINT | FLAG_MSAA_4X_HINT); + SetConfigFlags(FLAG_VSYNC_HINT); InitWindow(screenWidth, screenHeight, "DoorNroll // Cyber-Revolver Cyberspace Crawler"); SetExitKey(KEY_NULL); // Prevent ESC from closing the game window SetTargetFPS(60); diff --git a/src/render/CyberRenderer.cpp b/src/render/CyberRenderer.cpp index 2188dfb..bed034f 100644 --- a/src/render/CyberRenderer.cpp +++ b/src/render/CyberRenderer.cpp @@ -12,32 +12,28 @@ namespace dnr { namespace { +// Direct vertex submission for batched textured quad without state flips +inline void submitQuad(Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, float uTiles = 1.0f, float vTiles = 1.0f, Color tint = WHITE) { + rlColor4ub(tint.r, tint.g, tint.b, tint.a); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(v0.x, v0.y, v0.z); + rlTexCoord2f(0.0f, vTiles); rlVertex3f(v1.x, v1.y, v1.z); + rlTexCoord2f(uTiles, vTiles); rlVertex3f(v2.x, v2.y, v2.z); + rlTexCoord2f(uTiles, 0.0f); rlVertex3f(v3.x, v3.y, v3.z); +} + // Draw a textured quad in 3D using rlgl void drawTexturedQuad(Texture2D tex, Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, float uTiles = 1.0f, float vTiles = 1.0f, Color tint = WHITE) { rlDisableBackfaceCulling(); rlSetTexture(tex.id); rlBegin(RL_QUADS); - rlColor4ub(tint.r, tint.g, tint.b, tint.a); - - rlTexCoord2f(0.0f, 0.0f); - rlVertex3f(v0.x, v0.y, v0.z); - - rlTexCoord2f(0.0f, vTiles); - rlVertex3f(v1.x, v1.y, v1.z); - - rlTexCoord2f(uTiles, vTiles); - rlVertex3f(v2.x, v2.y, v2.z); - - rlTexCoord2f(uTiles, 0.0f); - rlVertex3f(v3.x, v3.y, v3.z); - + submitQuad(v0, v1, v2, v3, uTiles, vTiles, tint); rlEnd(); rlSetTexture(0); rlEnableBackfaceCulling(); } -// Draw a textured 3D box using rlgl -void drawTexturedBox(Texture2D tex, Vector3 pos, Vector3 size, float uTile = 1.0f, float vTile = 1.0f, Color tint = WHITE) { +// Direct vertex submission for batched textured box without state flips +inline void submitBoxQuads(Vector3 pos, Vector3 size, float uTile = 1.0f, float vTile = 1.0f, Color tint = WHITE) { float x = pos.x - size.x * 0.5f; float y = pos.y - size.y * 0.5f; float z = pos.z - size.z * 0.5f; @@ -45,47 +41,51 @@ void drawTexturedBox(Texture2D tex, Vector3 pos, Vector3 size, float uTile = 1.0 float h = size.y; float d = size.z; - rlDisableBackfaceCulling(); - rlSetTexture(tex.id); - rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); // Front face (+Z) - rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y + h, z + d); - rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y, z + d); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y + h, z + d); + rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y, z + d); rlTexCoord2f(uTile, vTile); rlVertex3f(x + w, y, z + d); - rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y + h, z + d); + rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y + h, z + d); // Back face (-Z) - rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + w, y + h, z); - rlTexCoord2f(0.0f, vTile); rlVertex3f(x + w, y, z); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + w, y + h, z); + rlTexCoord2f(0.0f, vTile); rlVertex3f(x + w, y, z); rlTexCoord2f(uTile, vTile); rlVertex3f(x, y, z); - rlTexCoord2f(uTile, 0.0f); rlVertex3f(x, y + h, z); + rlTexCoord2f(uTile, 0.0f); rlVertex3f(x, y + h, z); // Top face (+Y) - rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y + h, z); - rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y + h, z + d); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y + h, z); + rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y + h, z + d); rlTexCoord2f(uTile, vTile); rlVertex3f(x + w, y + h, z + d); - rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y + h, z); + rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y + h, z); // Bottom face (-Y) - rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y, z + d); - rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y, z); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y, z + d); + rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y, z); rlTexCoord2f(uTile, vTile); rlVertex3f(x + w, y, z); - rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y, z + d); + rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y, z + d); // Right face (+X) - rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + w, y + h, z + d); - rlTexCoord2f(0.0f, vTile); rlVertex3f(x + w, y, z + d); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + w, y + h, z + d); + rlTexCoord2f(0.0f, vTile); rlVertex3f(x + w, y, z + d); rlTexCoord2f(uTile, vTile); rlVertex3f(x + w, y, z); - rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y + h, z); + rlTexCoord2f(uTile, 0.0f); rlVertex3f(x + w, y + h, z); // Left face (-X) - rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y + h, z); - rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y, z); + rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x, y + h, z); + rlTexCoord2f(0.0f, vTile); rlVertex3f(x, y, z); rlTexCoord2f(uTile, vTile); rlVertex3f(x, y, z + d); - rlTexCoord2f(uTile, 0.0f); rlVertex3f(x, y + h, z + d); + rlTexCoord2f(uTile, 0.0f); rlVertex3f(x, y + h, z + d); +} +// Draw a textured 3D box using rlgl +void drawTexturedBox(Texture2D tex, Vector3 pos, Vector3 size, float uTile = 1.0f, float vTile = 1.0f, Color tint = WHITE) { + rlDisableBackfaceCulling(); + rlSetTexture(tex.id); + rlBegin(RL_QUADS); + submitBoxQuads(pos, size, uTile, vTile, tint); rlEnd(); rlSetTexture(0); rlEnableBackfaceCulling(); @@ -1164,6 +1164,7 @@ void CyberRenderer::init() { m_locTime = GetShaderLocation(m_shaderPostProcess, "u_time"); m_locGlitch = GetShaderLocation(m_shaderPostProcess, "u_glitchIntensity"); m_locRes = GetShaderLocation(m_shaderPostProcess, "u_resolution"); + m_locPreset = GetShaderLocation(m_shaderPostProcess, "u_preset"); m_postProcessLoaded = true; break; } @@ -1263,6 +1264,10 @@ void CyberRenderer::unloadTextures() { void CyberRenderer::beginPostProcess() { + if (m_graphicsQuality == GraphicsQuality::PERFORMANCE) { + ClearBackground(Color{ 4, 6, 12, 255 }); + return; + } if (m_postProcessLoaded) { BeginTextureMode(m_renderTarget); ClearBackground(Color{ 4, 6, 12, 255 }); @@ -1270,6 +1275,9 @@ void CyberRenderer::beginPostProcess() { } void CyberRenderer::endPostProcess(float animTime) { + if (m_graphicsQuality == GraphicsQuality::PERFORMANCE) { + return; + } if (m_postProcessLoaded) { EndTextureMode(); @@ -1280,6 +1288,10 @@ void CyberRenderer::endPostProcess(float animTime) { SetShaderValue(m_shaderPostProcess, m_locGlitch, &m_glitchIntensity, SHADER_UNIFORM_FLOAT); float res[2] = { (float)GetScreenWidth(), (float)GetScreenHeight() }; SetShaderValue(m_shaderPostProcess, m_locRes, res, SHADER_UNIFORM_VEC2); + int pVal = (m_graphicsQuality == GraphicsQuality::ULTRA) ? 2 : 1; + if (m_locPreset >= 0) { + SetShaderValue(m_shaderPostProcess, m_locPreset, &pVal, SHADER_UNIFORM_INT); + } BeginShaderMode(m_shaderPostProcess); DrawTextureRec(m_renderTarget.texture, @@ -1383,21 +1395,52 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float int minY = std::max(0, pY - renderDist); int maxY = std::min(dungeon.getHeight() - 1, pY + renderDist); + struct QuadSubmission { + Vector3 v0, v1, v2, v3; + Color tint; + }; + struct BoxSubmission { + Vector3 pos; + Vector3 size; + float uTile; + float vTile; + Color tint; + }; + + std::vector batchFloors; + batchFloors.reserve(128); + std::vector batchCeils; + batchCeils.reserve(128); + std::vector batchWalls; + batchWalls.reserve(128); + std::vector batchDoorFrames; + batchDoorFrames.reserve(32); + std::vector batchDoors; + batchDoors.reserve(32); + for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { Vector3 center = { x * tileSize, 0.0f, y * tileSize }; + + // Camera Frustum Culling: discard tiles behind camera plane (unless very close to player) + float dx = (center.x - eye.x); + float dz = (center.z - eye.z); + float distSq = dx * dx + dz * dz; + float dot = dx * dirX + dz * dirZ; + + if (distSq > 3.5f && dot < -0.2f) { + continue; // Outside camera viewing angle + } + TileType tile = dungeon.getTile(x, y); // Tactical Helmet Spotlight / Flashlight Cone Falloff - float dx = (center.x - eye.x); - float dz = (center.z - eye.z); - float dist = std::sqrt(dx * dx + dz * dz); - float b = 0.12f; // Deep atmospheric shadow baseline + float dist = std::sqrt(distSq); + float b = 0.12f; if (dist < 0.4f) { b = 1.0f; } else { - float dot = (dx * dirX + dz * dirZ) / dist; - float cone = std::clamp((dot - 0.20f) / 0.80f, 0.0f, 1.0f); + float cone = std::clamp((dot / dist - 0.20f) / 0.80f, 0.0f, 1.0f); float distFall = std::clamp(1.0f - (dist / (renderDist * 2.0f)), 0.0f, 1.0f); b = std::clamp(0.12f + cone * distFall * 0.88f, 0.08f, 1.0f); } @@ -1406,24 +1449,25 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float if (tile == TileType::WALL) { Vector3 wallCenter = { center.x, wallHeight * 0.5f, center.z }; Vector3 wallSize = { tileSize, wallHeight, tileSize }; - - drawTexturedBox(m_texDungeonWall, wallCenter, wallSize, 1.0f, 1.0f, tileTint); + batchWalls.push_back({ wallCenter, wallSize, 1.0f, 1.0f, tileTint }); } else { // Textured Industrial Grate Floor - drawTexturedQuad(m_texDungeonFloor, - { center.x - tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, - { center.x - tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, - { center.x + tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, - { center.x + tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, - 1.0f, 1.0f, tileTint); + batchFloors.push_back({ + { center.x - tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, + { center.x - tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, + { center.x + tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, + { center.x + tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, + tileTint + }); // Textured Beam Ceiling with Dim Red Fixtures - drawTexturedQuad(m_texDungeonCeil, - { center.x + tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, - { center.x + tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, - { center.x - tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, - { center.x - tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, - 1.0f, 1.0f, tileTint); + batchCeils.push_back({ + { center.x + tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, + { center.x + tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, + { center.x - tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, + { center.x - tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, + tileTint + }); if (tile == TileType::DOOR_LOCKED || tile == TileType::DOOR_UNLOCKED) { bool locked = (tile == TileType::DOOR_LOCKED); @@ -1431,102 +1475,171 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float if (wallLeftRight) { Vector3 leftPost = { center.x - 0.9f, wallHeight * 0.5f, center.z }; - drawTexturedBox(m_texDungeonDoorFrame, leftPost, { 0.2f, wallHeight, 0.35f }, 1.0f, 1.0f, tileTint); + batchDoorFrames.push_back({ leftPost, { 0.2f, wallHeight, 0.35f }, 1.0f, 1.0f, tileTint }); Vector3 rightPost = { center.x + 0.9f, wallHeight * 0.5f, center.z }; - drawTexturedBox(m_texDungeonDoorFrame, rightPost, { 0.2f, wallHeight, 0.35f }, 1.0f, 1.0f, tileTint); + batchDoorFrames.push_back({ rightPost, { 0.2f, wallHeight, 0.35f }, 1.0f, 1.0f, tileTint }); Vector3 topLintel = { center.x, wallHeight - 0.1f, center.z }; - drawTexturedBox(m_texDungeonDoorFrame, topLintel, { 1.8f, 0.2f, 0.35f }, 2.0f, 1.0f, tileTint); + batchDoorFrames.push_back({ topLintel, { 1.8f, 0.2f, 0.35f }, 2.0f, 1.0f, tileTint }); if (locked) { Vector3 doorBody = { center.x, (wallHeight - 0.2f) * 0.5f, center.z }; - drawTexturedBox(m_texDungeonDoor, doorBody, { 1.6f, wallHeight - 0.2f, 0.2f }, 1.0f, 1.0f, tileTint); - - float pulse = 0.5f + 0.5f * std::sin(animTime * 6.0f); - Color lockCol = (pulse > 0.5f) ? TacticalColors::Crimson : TacticalColors::DarkRed; - Vector3 lockCore = { center.x, 0.9f, center.z }; - DrawCube(lockCore, 0.38f, 0.22f, 0.24f, lockCol); - DrawCubeWires(lockCore, 0.40f, 0.24f, 0.26f, TacticalColors::BrightRed); + batchDoors.push_back({ doorBody, { 1.6f, wallHeight - 0.2f, 0.2f }, 1.0f, 1.0f, tileTint }); } else { Vector3 leftSlab = { center.x - 0.7f, (wallHeight - 0.2f) * 0.5f, center.z }; Vector3 rightSlab = { center.x + 0.7f, (wallHeight - 0.2f) * 0.5f, center.z }; - drawTexturedBox(m_texDungeonDoor, leftSlab, { 0.25f, wallHeight - 0.2f, 0.18f }, 0.5f, 1.0f, tileTint); - drawTexturedBox(m_texDungeonDoor, rightSlab, { 0.25f, wallHeight - 0.2f, 0.18f }, 0.5f, 1.0f, tileTint); + batchDoors.push_back({ leftSlab, { 0.25f, wallHeight - 0.2f, 0.18f }, 0.5f, 1.0f, tileTint }); + batchDoors.push_back({ rightSlab, { 0.25f, wallHeight - 0.2f, 0.18f }, 0.5f, 1.0f, tileTint }); } } else { Vector3 northPost = { center.x, wallHeight * 0.5f, center.z - 0.9f }; - drawTexturedBox(m_texDungeonDoorFrame, northPost, { 0.35f, wallHeight, 0.2f }, 1.0f, 1.0f, tileTint); + batchDoorFrames.push_back({ northPost, { 0.35f, wallHeight, 0.2f }, 1.0f, 1.0f, tileTint }); Vector3 southPost = { center.x, wallHeight * 0.5f, center.z + 0.9f }; - drawTexturedBox(m_texDungeonDoorFrame, southPost, { 0.35f, wallHeight, 0.2f }, 1.0f, 1.0f, tileTint); + batchDoorFrames.push_back({ southPost, { 0.35f, wallHeight, 0.2f }, 1.0f, 1.0f, tileTint }); Vector3 topLintel = { center.x, wallHeight - 0.1f, center.z }; - drawTexturedBox(m_texDungeonDoorFrame, topLintel, { 0.35f, 0.2f, 1.8f }, 1.0f, 2.0f, tileTint); + batchDoorFrames.push_back({ topLintel, { 0.35f, 0.2f, 1.8f }, 1.0f, 2.0f, tileTint }); if (locked) { Vector3 doorBody = { center.x, (wallHeight - 0.2f) * 0.5f, center.z }; - drawTexturedBox(m_texDungeonDoor, doorBody, { 0.2f, wallHeight - 0.2f, 1.6f }, 1.0f, 1.0f, tileTint); - - float pulse = 0.5f + 0.5f * std::sin(animTime * 6.0f); - Color lockCol = (pulse > 0.5f) ? TacticalColors::Crimson : TacticalColors::DarkRed; - Vector3 lockCore = { center.x, 0.9f, center.z }; - DrawCube(lockCore, 0.24f, 0.22f, 0.38f, lockCol); - DrawCubeWires(lockCore, 0.26f, 0.24f, 0.40f, TacticalColors::BrightRed); + batchDoors.push_back({ doorBody, { 0.2f, wallHeight - 0.2f, 1.6f }, 1.0f, 1.0f, tileTint }); } else { Vector3 northSlab = { center.x, (wallHeight - 0.2f) * 0.5f, center.z - 0.7f }; Vector3 southSlab = { center.x, (wallHeight - 0.2f) * 0.5f, center.z + 0.7f }; - drawTexturedBox(m_texDungeonDoor, northSlab, { 0.18f, wallHeight - 0.2f, 0.25f }, 0.5f, 1.0f, tileTint); - drawTexturedBox(m_texDungeonDoor, southSlab, { 0.18f, wallHeight - 0.2f, 0.25f }, 0.5f, 1.0f, tileTint); + batchDoors.push_back({ northSlab, { 0.18f, wallHeight - 0.2f, 0.25f }, 0.5f, 1.0f, tileTint }); + batchDoors.push_back({ southSlab, { 0.18f, wallHeight - 0.2f, 0.25f }, 0.5f, 1.0f, tileTint }); } } - } else if (tile == TileType::LOOT_CACHE) { - float floatY = 0.55f + std::sin(animTime * 3.0f + x) * 0.08f; - Vector3 lootPos = { center.x, floatY, center.z }; - DrawCube(lootPos, 0.52f, 0.40f, 0.52f, Color{ 24, 30, 40, 255 }); - DrawCubeWires(lootPos, 0.54f, 0.42f, 0.54f, TacticalColors::Crimson); - DrawCube({ lootPos.x, lootPos.y + 0.12f, lootPos.z }, 0.18f, 0.08f, 0.18f, TacticalColors::BrightRed); - } else if (tile == TileType::EXIT_PORTAL) { - float pulse = 0.5f + 0.5f * std::sin(animTime * 4.0f); - - // Tactical Extraction Terminal Platform - Vector3 basePos = { center.x, 0.10f, center.z }; - DrawCylinder(basePos, 0.95f, 1.0f, 0.20f, 8, Color{ 22, 28, 38, 255 }); - DrawCylinderWires(basePos, 0.96f, 1.01f, 0.20f, 8, TacticalColors::Crimson); - - // 4 Corner Pylons with Crimson Warning Beacons - float pylonDist = 0.72f; - Vector3 pylons[4] = { - { center.x - pylonDist, 0.42f, center.z - pylonDist }, - { center.x + pylonDist, 0.42f, center.z - pylonDist }, - { center.x - pylonDist, 0.42f, center.z + pylonDist }, - { center.x + pylonDist, 0.42f, center.z + pylonDist } - }; - for (int p = 0; p < 4; ++p) { - DrawCylinder(pylons[p], 0.06f, 0.08f, 0.65f, 8, Color{ 34, 42, 56, 255 }); - DrawCylinderWires(pylons[p], 0.065f, 0.085f, 0.65f, 8, TacticalColors::DarkRed); - DrawCube({ pylons[p].x, 0.76f, pylons[p].z }, 0.12f, 0.12f, 0.12f, TacticalColors::Crimson); - } - - // Central Quantum Extraction Beacon - Vector3 corePos = { center.x, 0.85f + pulse * 0.05f, center.z }; - DrawCube(corePos, 0.32f, 1.25f, 0.32f, Color{ 230, 25, 45, static_cast(120 + pulse * 70) }); - DrawCubeWires(corePos, 0.34f, 1.27f, 0.34f, TacticalColors::PureWhite); - - // Overhead Extraction Airlock Frame - Vector3 signPos = { center.x, wallHeight - 0.20f, center.z }; - DrawCube(signPos, 1.3f, 0.18f, 0.06f, Color{ 14, 18, 26, 240 }); - DrawCubeWires(signPos, 1.32f, 0.20f, 0.08f, TacticalColors::Crimson); } } } } - // Render Laser Barriers + // Submit All Batches to GPU with minimum state changes (5 draw calls total) + rlDisableBackfaceCulling(); + + if (!batchFloors.empty()) { + rlSetTexture(m_texDungeonFloor.id); + rlBegin(RL_QUADS); + for (const auto& q : batchFloors) { + submitQuad(q.v0, q.v1, q.v2, q.v3, 1.0f, 1.0f, q.tint); + } + rlEnd(); + } + + if (!batchCeils.empty()) { + rlSetTexture(m_texDungeonCeil.id); + rlBegin(RL_QUADS); + for (const auto& q : batchCeils) { + submitQuad(q.v0, q.v1, q.v2, q.v3, 1.0f, 1.0f, q.tint); + } + rlEnd(); + } + + if (!batchWalls.empty()) { + rlSetTexture(m_texDungeonWall.id); + rlBegin(RL_QUADS); + for (const auto& b : batchWalls) { + submitBoxQuads(b.pos, b.size, b.uTile, b.vTile, b.tint); + } + rlEnd(); + } + + if (!batchDoorFrames.empty()) { + rlSetTexture(m_texDungeonDoorFrame.id); + rlBegin(RL_QUADS); + for (const auto& b : batchDoorFrames) { + submitBoxQuads(b.pos, b.size, b.uTile, b.vTile, b.tint); + } + rlEnd(); + } + + if (!batchDoors.empty()) { + rlSetTexture(m_texDungeonDoor.id); + rlBegin(RL_QUADS); + for (const auto& b : batchDoors) { + submitBoxQuads(b.pos, b.size, b.uTile, b.vTile, b.tint); + } + rlEnd(); + } + + rlSetTexture(0); + rlEnableBackfaceCulling(); + + // Render interactive tile objects (Caches, Exit Portal, Door Lock Lights) with frustum check + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + Vector3 center = { x * tileSize, 0.0f, y * tileSize }; + float dx = (center.x - eye.x); + float dz = (center.z - eye.z); + if (dx * dx + dz * dz > 3.5f && (dx * dirX + dz * dirZ) < -0.2f) continue; + + TileType tile = dungeon.getTile(x, y); + + if (tile == TileType::DOOR_LOCKED) { + bool wallLeftRight = (dungeon.getTile(x - 1, y) == TileType::WALL && dungeon.getTile(x + 1, y) == TileType::WALL); + float pulse = 0.5f + 0.5f * std::sin(animTime * 6.0f); + Color lockCol = (pulse > 0.5f) ? TacticalColors::Crimson : TacticalColors::DarkRed; + if (wallLeftRight) { + Vector3 lockCore = { center.x, 0.9f, center.z }; + DrawCube(lockCore, 0.38f, 0.22f, 0.24f, lockCol); + DrawCubeWires(lockCore, 0.40f, 0.24f, 0.26f, TacticalColors::BrightRed); + } else { + Vector3 lockCore = { center.x, 0.9f, center.z }; + DrawCube(lockCore, 0.24f, 0.22f, 0.38f, lockCol); + DrawCubeWires(lockCore, 0.26f, 0.24f, 0.40f, TacticalColors::BrightRed); + } + } else if (tile == TileType::LOOT_CACHE) { + float floatY = 0.55f + std::sin(animTime * 3.0f + x) * 0.08f; + Vector3 lootPos = { center.x, floatY, center.z }; + DrawCube(lootPos, 0.52f, 0.40f, 0.52f, Color{ 24, 30, 40, 255 }); + DrawCubeWires(lootPos, 0.54f, 0.42f, 0.54f, TacticalColors::Crimson); + DrawCube({ lootPos.x, lootPos.y + 0.12f, lootPos.z }, 0.18f, 0.08f, 0.18f, TacticalColors::BrightRed); + } else if (tile == TileType::EXIT_PORTAL) { + float pulse = 0.5f + 0.5f * std::sin(animTime * 4.0f); + + // Tactical Extraction Terminal Platform + Vector3 basePos = { center.x, 0.10f, center.z }; + DrawCylinder(basePos, 0.95f, 1.0f, 0.20f, 8, Color{ 22, 28, 38, 255 }); + DrawCylinderWires(basePos, 0.96f, 1.01f, 0.20f, 8, TacticalColors::Crimson); + + // 4 Corner Pylons with Crimson Warning Beacons + float pylonDist = 0.72f; + Vector3 pylons[4] = { + { center.x - pylonDist, 0.42f, center.z - pylonDist }, + { center.x + pylonDist, 0.42f, center.z - pylonDist }, + { center.x - pylonDist, 0.42f, center.z + pylonDist }, + { center.x + pylonDist, 0.42f, center.z + pylonDist } + }; + for (int p = 0; p < 4; ++p) { + DrawCylinder(pylons[p], 0.06f, 0.08f, 0.65f, 8, Color{ 34, 42, 56, 255 }); + DrawCylinderWires(pylons[p], 0.065f, 0.085f, 0.65f, 8, TacticalColors::DarkRed); + DrawCube({ pylons[p].x, 0.76f, pylons[p].z }, 0.12f, 0.12f, 0.12f, TacticalColors::Crimson); + } + + // Central Quantum Extraction Beacon + Vector3 corePos = { center.x, 0.85f + pulse * 0.05f, center.z }; + DrawCube(corePos, 0.32f, 1.25f, 0.32f, Color{ 230, 25, 45, static_cast(120 + pulse * 70) }); + DrawCubeWires(corePos, 0.34f, 1.27f, 0.34f, TacticalColors::PureWhite); + + // Overhead Extraction Airlock Frame + Vector3 signPos = { center.x, wallHeight - 0.20f, center.z }; + DrawCube(signPos, 1.3f, 0.18f, 0.06f, Color{ 14, 18, 26, 240 }); + DrawCubeWires(signPos, 1.32f, 0.20f, 0.08f, TacticalColors::Crimson); + } + } + } + + // Render Laser Barriers (with frustum culling) for (const auto& l : dungeon.getLasers()) { if (l.disarmed) continue; if (std::abs(l.x - pX) > renderDist || std::abs(l.y - pY) > renderDist) continue; Vector3 lCenter = { l.x * tileSize, 0.9f, l.y * tileSize }; + float dx = lCenter.x - eye.x; + float dz = lCenter.z - eye.z; + if (dx * dx + dz * dz > 3.5f && (dx * dirX + dz * dirZ) < -0.2f) continue; if (l.vertical) { DrawCube({ lCenter.x, lCenter.y, lCenter.z - 0.95f }, 0.25f, 0.25f, 0.10f, Color{ 45, 52, 65, 255 }); @@ -1551,11 +1664,14 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float } } - // Render Proximity Mines (Zero-rounding: rectangular plate with flashing sensor) + // Render Proximity Mines (with frustum culling) for (const auto& m : dungeon.getMines()) { if (m.defused || m.triggered) continue; if (std::abs(m.x - pX) > renderDist || std::abs(m.y - pY) > renderDist) continue; Vector3 mCenter = { m.x * tileSize, 0.04f, m.y * tileSize }; + float dx = mCenter.x - eye.x; + float dz = mCenter.z - eye.z; + if (dx * dx + dz * dz > 3.5f && (dx * dirX + dz * dirZ) < -0.2f) continue; DrawCube(mCenter, 0.44f, 0.08f, 0.44f, Color{ 30, 36, 46, 255 }); DrawCubeWires(mCenter, 0.45f, 0.09f, 0.45f, TacticalColors::DarkRed); @@ -1566,10 +1682,13 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float DrawCubeWires({ mCenter.x, 0.10f, mCenter.z }, 0.15f, 0.09f, 0.15f, TacticalColors::PureWhite); } - // Render Cyber Shrines (Monolithic Arasaka Server Terminal) + // Render Cyber Shrines (with frustum culling) for (const auto& s : dungeon.getShrines()) { if (std::abs(s.x - pX) > renderDist || std::abs(s.y - pY) > renderDist) continue; Vector3 sCenter = { s.x * tileSize, 0.0f, s.y * tileSize }; + float dx = sCenter.x - eye.x; + float dz = sCenter.z - eye.z; + if (dx * dx + dz * dz > 3.5f && (dx * dirX + dz * dirZ) < -0.2f) continue; DrawCube({ sCenter.x, 0.10f, sCenter.z }, 1.1f, 0.20f, 1.1f, Color{ 22, 28, 38, 255 }); DrawCubeWires({ sCenter.x, 0.10f, sCenter.z }, 1.12f, 0.22f, 1.12f, TacticalColors::Crimson); @@ -1584,13 +1703,17 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float DrawCubeWires(terminalPos, 0.26f, 0.26f, 0.26f, TacticalColors::PureWhite); } - // Render Corridor Patrol Enemies as 3D Cyber-Drones + // Render Corridor Patrol Enemies as 3D Cyber-Drones (with frustum culling) auto& enemies = const_cast(dungeon).getEnemies(); for (const auto& me : enemies) { if (!me.alive) continue; if (std::abs(me.x - pX) > renderDist || std::abs(me.y - pY) > renderDist) continue; Vector3 ePos = { me.x * tileSize, 0.9f, me.y * tileSize }; + float dx = ePos.x - eye.x; + float dz = ePos.z - eye.z; + if (dx * dx + dz * dz > 3.5f && (dx * dirX + dz * dirZ) < -0.2f) continue; + renderCyberDrone(ePos, me.enemy.getColor(), 1.0f, animTime, false, false, me.enemy.isElite()); } @@ -1628,21 +1751,45 @@ void CyberRenderer::renderCombat3D(const Dungeon& dungeon, const Player& player, int minY = std::max(0, pY - renderDist); int maxY = std::min(dungeon.getHeight() - 1, pY + renderDist); - // 1. Render surrounding corridor geometry + struct QuadSubmission { + Vector3 v0, v1, v2, v3; + Color tint; + }; + struct BoxSubmission { + Vector3 pos; + Vector3 size; + Color tint; + }; + + std::vector batchFloors; + batchFloors.reserve(64); + std::vector batchCeils; + batchCeils.reserve(64); + std::vector batchWalls; + batchWalls.reserve(64); + + // 1. Collect surrounding corridor geometry with frustum check for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { Vector3 center = { x * tileSize, 0.0f, y * tileSize }; - TileType tile = dungeon.getTile(x, y); float dx = (center.x - eye.x); float dz = (center.z - eye.z); - float dist = std::sqrt(dx * dx + dz * dz); + float distSq = dx * dx + dz * dz; + float dot = dx * dirX + dz * dirZ; + + if (distSq > 3.5f && dot < -0.2f) { + continue; // Behind player in combat + } + + TileType tile = dungeon.getTile(x, y); + + float dist = std::sqrt(distSq); float b = 0.15f; if (dist < 0.4f) { b = 1.0f; } else { - float dot = (dx * dirX + dz * dirZ) / dist; - float cone = std::clamp((dot - 0.20f) / 0.80f, 0.0f, 1.0f); + float cone = std::clamp((dot / dist - 0.20f) / 0.80f, 0.0f, 1.0f); float distFall = std::clamp(1.0f - (dist / 10.0f), 0.0f, 1.0f); b = std::clamp(0.15f + cone * distFall * 0.85f, 0.10f, 1.0f); } @@ -1650,25 +1797,60 @@ void CyberRenderer::renderCombat3D(const Dungeon& dungeon, const Player& player, if (tile == TileType::WALL) { Vector3 wallCenter = { center.x, wallHeight * 0.5f, center.z }; - drawTexturedBox(m_texDungeonWall, wallCenter, { tileSize, wallHeight, tileSize }, 1.0f, 1.0f, tileTint); + batchWalls.push_back({ wallCenter, { tileSize, wallHeight, tileSize }, tileTint }); } else { - drawTexturedQuad(m_texDungeonFloor, - { center.x - tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, - { center.x - tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, - { center.x + tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, - { center.x + tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, - 1.0f, 1.0f, tileTint); + batchFloors.push_back({ + { center.x - tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, + { center.x - tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, + { center.x + tileSize * 0.5f, -0.01f, center.z + tileSize * 0.5f }, + { center.x + tileSize * 0.5f, -0.01f, center.z - tileSize * 0.5f }, + tileTint + }); - drawTexturedQuad(m_texDungeonCeil, - { center.x + tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, - { center.x + tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, - { center.x - tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, - { center.x - tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, - 1.0f, 1.0f, tileTint); + batchCeils.push_back({ + { center.x + tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, + { center.x + tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, + { center.x - tileSize * 0.5f, wallHeight, center.z + tileSize * 0.5f }, + { center.x - tileSize * 0.5f, wallHeight, center.z - tileSize * 0.5f }, + tileTint + }); } } } + // Submit corridor geometry in 3 batches + rlDisableBackfaceCulling(); + + if (!batchFloors.empty()) { + rlSetTexture(m_texDungeonFloor.id); + rlBegin(RL_QUADS); + for (const auto& q : batchFloors) { + submitQuad(q.v0, q.v1, q.v2, q.v3, 1.0f, 1.0f, q.tint); + } + rlEnd(); + } + + if (!batchCeils.empty()) { + rlSetTexture(m_texDungeonCeil.id); + rlBegin(RL_QUADS); + for (const auto& q : batchCeils) { + submitQuad(q.v0, q.v1, q.v2, q.v3, 1.0f, 1.0f, q.tint); + } + rlEnd(); + } + + if (!batchWalls.empty()) { + rlSetTexture(m_texDungeonWall.id); + rlBegin(RL_QUADS); + for (const auto& b : batchWalls) { + submitBoxQuads(b.pos, b.size, 1.0f, 1.0f, b.tint); + } + rlEnd(); + } + + rlSetTexture(0); + rlEnableBackfaceCulling(); + // 2. STAGE ENEMY 3D DRONE IN FRONT OF PLAYER const auto& enemy = combat.getEnemy(); if (enemy && enemy->isAlive()) { diff --git a/src/render/CyberRenderer.hpp b/src/render/CyberRenderer.hpp index 6356544..50e6b81 100644 --- a/src/render/CyberRenderer.hpp +++ b/src/render/CyberRenderer.hpp @@ -25,6 +25,9 @@ public: void triggerGlitch(float intensity = 0.7f); + void setGraphicsQuality(GraphicsQuality q) { m_graphicsQuality = q; } + GraphicsQuality getGraphicsQuality() const { return m_graphicsQuality; } + private: void generateTextures(); void unloadTextures(); @@ -36,6 +39,7 @@ private: Camera3D m_camera{}; bool m_texturesLoaded = false; + GraphicsQuality m_graphicsQuality = GraphicsQuality::BALANCED; // Post-Processing Framebuffer & GLSL Shader RenderTexture2D m_renderTarget{}; @@ -43,6 +47,7 @@ private: int m_locTime = -1; int m_locGlitch = -1; int m_locRes = -1; + int m_locPreset = -1; float m_glitchIntensity = 0.0f; bool m_postProcessLoaded = false; diff --git a/src/render/UIRenderer.cpp b/src/render/UIRenderer.cpp index 553eba4..5e5124a 100644 --- a/src/render/UIRenderer.cpp +++ b/src/render/UIRenderer.cpp @@ -165,13 +165,7 @@ void UIRenderer::drawMatrixDigitalRain(float animTime) { } void UIRenderer::drawMatrixOverlay(float animTime) { - // Subtle CRT scanlines int screenH = GetScreenHeight(); - int screenW = GetScreenWidth(); - - for (int y = 0; y < screenH; y += 3) { - DrawLine(0, y, screenW, y, Color{ 180, 195, 215, 10 }); - } // Top terminal status bar DrawText("[SYS::TACTICAL_OS] [ARASAKA_NET] [PROTOCOL::357_MAGNUM] [SECURITY::ACTIVE]", @@ -1725,11 +1719,11 @@ void UIRenderer::drawMainMenu(bool canContinue) { drawMatrixOverlay(static_cast(GetTime())); } -void UIRenderer::drawSettingsMenu(float masterVol, float musicVol, float sfxVol, int selectedSlider) { +void UIRenderer::drawSettingsMenu(float masterVol, float musicVol, float sfxVol, GraphicsQuality quality, int selectedSlider) { DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 230 }); - int menuW = 580; - int menuH = 430; + int menuW = 620; + int menuH = 495; int menuX = (GetScreenWidth() - menuW) / 2; int menuY = (GetScreenHeight() - menuH) / 2; @@ -1738,23 +1732,24 @@ void UIRenderer::drawSettingsMenu(float masterVol, float musicVol, float sfxVol, // Title Bar DrawRectangle(menuX, menuY, menuW, 52, Color{ 16, 26, 44, 255 }); - DrawText("// AUDIO CONFIGURATION PROTOCOL", menuX + 25, menuY + 16, 20, CyberColors::NeonCyan); + DrawText("// HARDWARE & AUDIO CONFIGURATION PROTOCOL", menuX + 25, menuY + 16, 20, CyberColors::NeonCyan); Vector2 mouse = GetMousePosition(); const char* titles[] = { "MASTER VOLUME", "MUSIC VOLUME", "SFX VOLUME" }; float vols[] = { masterVol, musicVol, sfxVol }; - int startY = menuY + 85; + int startY = menuY + 76; int sliderW = 340; - int sliderH = 26; - int sliderX = menuX + 180; + int sliderH = 24; + int sliderX = menuX + 200; + // 1. Audio volume sliders (0, 1, 2) for (int i = 0; i < 3; ++i) { - int itemY = startY + i * 78; + int itemY = startY + i * 68; bool isSel = (i == selectedSlider); - Rectangle itemBox = { static_cast(menuX + 20), static_cast(itemY - 10), static_cast(menuW - 40), 65.0f }; + Rectangle itemBox = { static_cast(menuX + 16), static_cast(itemY - 8), static_cast(menuW - 32), 60.0f }; bool isHover = CheckCollisionPointRec(mouse, itemBox); if (isSel || isHover) { @@ -1762,7 +1757,7 @@ void UIRenderer::drawSettingsMenu(float masterVol, float musicVol, float sfxVol, DrawRectangleLinesEx(itemBox, 1.0f, isSel ? CyberColors::NeonYellow : Color{ 40, 70, 110, 255 }); } - DrawText(titles[i], menuX + 30, itemY + 4, 15, isSel ? CyberColors::NeonYellow : RAYWHITE); + DrawText(titles[i], menuX + 28, itemY + 4, 14, isSel ? CyberColors::NeonYellow : RAYWHITE); Rectangle sBar = { static_cast(sliderX), static_cast(itemY), static_cast(sliderW), static_cast(sliderH) }; DrawRectangleRec(sBar, Color{ 14, 20, 32, 255 }); @@ -1776,17 +1771,74 @@ void UIRenderer::drawSettingsMenu(float masterVol, float musicVol, float sfxVol, DrawRectangle(knobX - 4, itemY - 3, 8, sliderH + 6, WHITE); std::string pctStr = std::to_string(static_cast(std::round(vols[i] * 100.0f))) + "%"; - DrawText(pctStr.c_str(), sliderX + sliderW + 15, itemY + 4, 16, isSel ? CyberColors::NeonYellow : RAYWHITE); + DrawText(pctStr.c_str(), sliderX + sliderW + 15, itemY + 3, 15, isSel ? CyberColors::NeonYellow : RAYWHITE); } - Rectangle btnBack = { static_cast(menuX + (menuW - 240) / 2), static_cast(menuY + menuH - 65), 240.0f, 44.0f }; + // 2. Graphics Preset Item (Index 3) + { + int itemY = startY + 3 * 68; + bool isSel = (selectedSlider == 3); + + Rectangle itemBox = { static_cast(menuX + 16), static_cast(itemY - 8), static_cast(menuW - 32), 62.0f }; + bool isHover = CheckCollisionPointRec(mouse, itemBox); + + if (isSel || isHover) { + DrawRectangleRec(itemBox, Color{ 20, 32, 52, 140 }); + DrawRectangleLinesEx(itemBox, 1.0f, isSel ? CyberColors::NeonYellow : Color{ 40, 70, 110, 255 }); + } + + DrawText("GRAPHICS PRESET", menuX + 28, itemY + 4, 14, isSel ? CyberColors::NeonYellow : RAYWHITE); + + // Arrows & Preset Name Box + Rectangle btnLeft = { static_cast(sliderX), static_cast(itemY - 2), 28.0f, static_cast(sliderH + 4) }; + Rectangle btnRight = { static_cast(sliderX + sliderW - 28), static_cast(itemY - 2), 28.0f, static_cast(sliderH + 4) }; + Rectangle nameBox = { static_cast(sliderX + 34), static_cast(itemY - 2), static_cast(sliderW - 68), static_cast(sliderH + 4) }; + + bool hoverL = CheckCollisionPointRec(mouse, btnLeft); + bool hoverR = CheckCollisionPointRec(mouse, btnRight); + bool hoverName = CheckCollisionPointRec(mouse, nameBox); + + DrawRectangleRec(btnLeft, hoverL ? Color{ 40, 70, 110, 255 } : Color{ 18, 26, 42, 255 }); + DrawRectangleLinesEx(btnLeft, 1.0f, hoverL ? CyberColors::NeonYellow : Color{ 50, 75, 110, 255 }); + DrawText("<", static_cast(btnLeft.x) + 10, static_cast(btnLeft.y) + 5, 16, hoverL ? CyberColors::NeonYellow : WHITE); + + DrawRectangleRec(btnRight, hoverR ? Color{ 40, 70, 110, 255 } : Color{ 18, 26, 42, 255 }); + DrawRectangleLinesEx(btnRight, 1.0f, hoverR ? CyberColors::NeonYellow : Color{ 50, 75, 110, 255 }); + DrawText(">", static_cast(btnRight.x) + 10, static_cast(btnRight.y) + 5, 16, hoverR ? CyberColors::NeonYellow : WHITE); + + DrawRectangleRec(nameBox, hoverName ? Color{ 25, 40, 65, 255 } : Color{ 14, 20, 32, 255 }); + DrawRectangleLinesEx(nameBox, 1.0f, isSel ? CyberColors::NeonGreen : Color{ 50, 75, 110, 255 }); + + const char* qName = "BALANCED 720p"; + Color qCol = CyberColors::NeonCyan; + const char* qDesc = "Standard 720p mode with CRT vignette and tone grading."; + + if (quality == GraphicsQuality::PERFORMANCE) { + qName = "PERFORMANCE (iGPU)"; + qCol = CyberColors::NeonGreen; + qDesc = "Direct backbuffer render. 0 shader blit. Max battery life & 60 FPS."; + } else if (quality == GraphicsQuality::ULTRA) { + qName = "ULTRA CYBER"; + qCol = TacticalColors::Crimson; + qDesc = "Full post-processing with glitch artifacts and chromatic aberration."; + } + + int textW = MeasureText(qName, 13); + DrawText(qName, static_cast(nameBox.x + (nameBox.width - textW) * 0.5f), static_cast(nameBox.y + 6), 13, qCol); + + // Explanatory descriptor underneath + DrawText(qDesc, menuX + 200, itemY + 30, 11, TacticalColors::MutedText); + } + + // Back / Return Button + Rectangle btnBack = { static_cast(menuX + (menuW - 240) / 2), static_cast(menuY + menuH - 60), 240.0f, 40.0f }; bool hoverBack = CheckCollisionPointRec(mouse, btnBack); DrawRectangleRec(btnBack, hoverBack ? Color{ 30, 60, 95, 255 } : Color{ 16, 26, 42, 255 }); DrawRectangleLinesEx(btnBack, hoverBack ? 2.0f : 1.0f, hoverBack ? CyberColors::NeonGreen : CyberColors::NeonCyan); - DrawText("[ESC / ENTER] SAVE & RETURN", static_cast(btnBack.x) + 18, static_cast(btnBack.y) + 13, 14, hoverBack ? CyberColors::NeonYellow : RAYWHITE); + DrawText("[ESC / ENTER] SAVE & RETURN", static_cast(btnBack.x) + 18, static_cast(btnBack.y) + 12, 14, hoverBack ? CyberColors::NeonYellow : RAYWHITE); - DrawText("[W/S] SELECT SLIDER | [A/D] ADJUST +/-5% | CLICK & DRAG SLIDERS", - menuX + 40, menuY + menuH - 18, 12, GRAY); + DrawText("[W/S] SELECT OPTION | [A/D] ADJUST / CYCLE PRESET | CLICK TO SELECT", + menuX + 35, menuY + menuH - 16, 11, GRAY); } } // namespace dnr diff --git a/src/render/UIRenderer.hpp b/src/render/UIRenderer.hpp index adf0128..1987fdd 100644 --- a/src/render/UIRenderer.hpp +++ b/src/render/UIRenderer.hpp @@ -36,7 +36,7 @@ public: void drawMatrixOverlay(float animTime); void drawPauseMenu(bool hasSavedMessage); void drawMainMenu(bool canContinue); - void drawSettingsMenu(float masterVol, float musicVol, float sfxVol, int selectedSlider); + void drawSettingsMenu(float masterVol, float musicVol, float sfxVol, GraphicsQuality quality, int selectedSlider); private: void drawMinimap(const Dungeon& dungeon, const Player& player, int x, int y, int size);