perf: Optimize for iGPU and battery mode with batching, frustum culling, and graphics presets

This commit is contained in:
Игорь Федосеев 2026-10-07 17:48:22 +03:00
parent 24885ef7bc
commit 2730ff6720
10 changed files with 481 additions and 177 deletions

View file

@ -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;
}

View file

@ -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

View file

@ -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<int>(gQuality) + 2) % 3;
auto newQ = static_cast<GraphicsQuality>(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<int>(gQuality) + 1) % 3;
auto newQ = static_cast<GraphicsQuality>(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<float>(menuX + (menuW - 240) / 2), static_cast<float>(menuY + menuH - 65), 240.0f, 44.0f };
Rectangle btnBack = { static_cast<float>(menuX + (menuW - 240) / 2), static_cast<float>(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<float>(sliderX - 10), static_cast<float>(itemY - 6), static_cast<float>(sliderW + 20), static_cast<float>(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<float>(sliderX), static_cast<float>(item3Y - 2), 28.0f, static_cast<float>(sliderH + 4) };
Rectangle btnRight = { static_cast<float>(sliderX + sliderW - 28), static_cast<float>(item3Y - 2), 28.0f, static_cast<float>(sliderH + 4) };
Rectangle nameBox = { static_cast<float>(sliderX + 34), static_cast<float>(item3Y - 2), static_cast<float>(sliderW - 68), static_cast<float>(sliderH + 4) };
if (CheckCollisionPointRec(mouse, btnLeft)) {
m_selectedSlider = 3;
int qVal = (static_cast<int>(gQuality) + 2) % 3;
auto newQ = static_cast<GraphicsQuality>(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<int>(gQuality) + 1) % 3;
auto newQ = static_cast<GraphicsQuality>(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;

View file

@ -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<int>(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<GraphicsQuality>(gVal);
}
}
m_volMaster = std::clamp(m_volMaster, 0.0f, 1.0f);

View file

@ -1,6 +1,7 @@
#pragma once
#include "raylib.h"
#include "core/Common.hpp"
#include <vector>
#include <string>
@ -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;

View file

@ -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);

View file

@ -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<QuadSubmission> batchFloors;
batchFloors.reserve(128);
std::vector<QuadSubmission> batchCeils;
batchCeils.reserve(128);
std::vector<BoxSubmission> batchWalls;
batchWalls.reserve(128);
std::vector<BoxSubmission> batchDoorFrames;
batchDoorFrames.reserve(32);
std::vector<BoxSubmission> 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<unsigned char>(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<unsigned char>(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&>(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<QuadSubmission> batchFloors;
batchFloors.reserve(64);
std::vector<QuadSubmission> batchCeils;
batchCeils.reserve(64);
std::vector<BoxSubmission> 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()) {

View file

@ -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;

View file

@ -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<float>(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<float>(menuX + 20), static_cast<float>(itemY - 10), static_cast<float>(menuW - 40), 65.0f };
Rectangle itemBox = { static_cast<float>(menuX + 16), static_cast<float>(itemY - 8), static_cast<float>(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<float>(sliderX), static_cast<float>(itemY), static_cast<float>(sliderW), static_cast<float>(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<int>(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<float>(menuX + (menuW - 240) / 2), static_cast<float>(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<float>(menuX + 16), static_cast<float>(itemY - 8), static_cast<float>(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<float>(sliderX), static_cast<float>(itemY - 2), 28.0f, static_cast<float>(sliderH + 4) };
Rectangle btnRight = { static_cast<float>(sliderX + sliderW - 28), static_cast<float>(itemY - 2), 28.0f, static_cast<float>(sliderH + 4) };
Rectangle nameBox = { static_cast<float>(sliderX + 34), static_cast<float>(itemY - 2), static_cast<float>(sliderW - 68), static_cast<float>(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<int>(btnLeft.x) + 10, static_cast<int>(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<int>(btnRight.x) + 10, static_cast<int>(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<int>(nameBox.x + (nameBox.width - textW) * 0.5f), static_cast<int>(nameBox.y + 6), 13, qCol);
// Explanatory descriptor underneath
DrawText(qDesc, menuX + 200, itemY + 30, 11, TacticalColors::MutedText);
}
// Back / Return Button
Rectangle btnBack = { static_cast<float>(menuX + (menuW - 240) / 2), static_cast<float>(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<int>(btnBack.x) + 18, static_cast<int>(btnBack.y) + 13, 14, hoverBack ? CyberColors::NeonYellow : RAYWHITE);
DrawText("[ESC / ENTER] SAVE & RETURN", static_cast<int>(btnBack.x) + 18, static_cast<int>(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

View file

@ -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);