DoorNroll/src/render/UIRenderer.cpp

1223 lines
61 KiB
C++

#include "UIRenderer.hpp"
#include <cmath>
#include <algorithm>
namespace dnr {
UIRenderer::UIRenderer() = default;
void UIRenderer::drawProgressBar(int x, int y, int width, int height, float ratio, Color barColor, Color bgColor) {
ratio = std::clamp(ratio, 0.0f, 1.0f);
DrawRectangle(x, y, width, height, bgColor);
DrawRectangle(x, y, static_cast<int>(width * ratio), height, barColor);
DrawRectangleLines(x, y, width, height, Color{ 40, 80, 120, 255 });
}
void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int startX, int startY, int size) {
int w = dungeon.getWidth();
int h = dungeon.getHeight();
float cellSize = static_cast<float>(size) / std::max(w, h);
// Frame
DrawRectangle(startX - 4, startY - 4, size + 8, size + 8, Color{ 4, 10, 18, 230 });
DrawRectangleLines(startX - 4, startY - 4, size + 8, size + 8, CyberColors::NeonCyan);
DrawText("NET_MAP // 2.5D", startX, startY - 18, 11, CyberColors::NeonCyan);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
if (!dungeon.isExplored(x, y)) continue;
int px = startX + static_cast<int>(x * cellSize);
int py = startY + static_cast<int>(y * cellSize);
int cs = static_cast<int>(cellSize) + 1;
TileType t = dungeon.getTile(x, y);
switch (t) {
case TileType::WALL:
DrawRectangle(px, py, cs, cs, Color{ 25, 45, 75, 255 });
break;
case TileType::FLOOR:
DrawRectangle(px, py, cs, cs, Color{ 10, 20, 35, 255 });
break;
case TileType::DOOR_LOCKED:
DrawRectangle(px, py, cs, cs, CyberColors::NeonOrange);
break;
case TileType::DOOR_UNLOCKED:
DrawRectangle(px, py, cs, cs, CyberColors::NeonGreen);
break;
case TileType::LOOT_CACHE:
DrawRectangle(px, py, cs, cs, CyberColors::NeonYellow);
break;
case TileType::EXIT_PORTAL:
DrawRectangle(px, py, cs, cs, CyberColors::NeonCyan);
break;
default:
break;
}
}
}
// Player icon and direction on minimap
int plX = startX + static_cast<int>((player.getGridX() + 0.5f) * cellSize);
int plY = startY + static_cast<int>((player.getGridY() + 0.5f) * cellSize);
DrawCircle(plX, plY, cellSize * 0.7f, CyberColors::NeonYellow);
Vector2 dirVec = getDirectionOffset(player.getFacing());
DrawLine(plX, plY, plX + static_cast<int>(dirVec.x * cellSize * 1.5f), plY + static_cast<int>(dirVec.y * cellSize * 1.5f), CyberColors::NeonGreen);
}
void UIRenderer::drawMatrixDigitalRain(float animTime) {
static const char* glyphs[] = { "01", "FF", "A7", "EXE", "NULL", "ACK", "SYN", "ICE", "ERR", "CPU", "357", "7F" };
int colWidth = 55;
int cols = GetScreenWidth() / colWidth;
for (int c = 0; c < cols; ++c) {
float speed = 120.0f + (c % 5) * 40.0f;
float yPos = std::fmod(animTime * speed + c * 80.0f, static_cast<float>(GetScreenHeight() + 100)) - 50.0f;
int xPos = c * colWidth + 10;
const char* txt = glyphs[(c + static_cast<int>(animTime * 2.0f)) % 12];
DrawText(txt, xPos, static_cast<int>(yPos), 12, Color{ 0, 255, 100, 35 });
DrawText(txt, xPos, static_cast<int>(yPos - 20), 11, Color{ 0, 255, 100, 18 });
}
}
void UIRenderer::drawMatrixOverlay(float animTime) {
// Subtle Matrix CRT scanlines
int screenH = GetScreenHeight();
int screenW = GetScreenWidth();
for (int y = 0; y < screenH; y += 3) {
DrawLine(0, y, screenW, y, Color{ 0, 255, 120, 14 });
}
// Top terminal status bar
DrawText("[SYS::MATRIX_ONLINE] [SUBNET::0x7F00] [PROTOCOL::CYBER_REVOLVER] [SECURITY::ICE_ACTIVE]",
15, screenH - 18, 11, Color{ 0, 240, 160, 160 });
}
void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player, const std::string& prompt) {
// Top-Left: Player Vital Stats
int hudX = 20;
int hudY = 20;
DrawRectangle(hudX, hudY, 330, 145, Color{ 6, 12, 22, 220 });
DrawRectangleLines(hudX, hudY, 330, 145, CyberColors::NeonCyan);
DrawText(("SECTOR LAYER // " + std::to_string(dungeon.getFloor())).c_str(), hudX + 15, hudY + 12, 16, CyberColors::NeonCyan);
// HP Bar
float hpRatio = static_cast<float>(player.getHp()) / player.getMaxHp();
drawProgressBar(hudX + 60, hudY + 40, 250, 18, hpRatio, CyberColors::NeonPink, Color{ 30, 10, 20, 255 });
DrawText("CORE", hudX + 15, hudY + 40, 14, CyberColors::NeonPink);
DrawText((std::to_string(player.getHp()) + " / " + std::to_string(player.getMaxHp())).c_str(), hudX + 145, hudY + 42, 13, RAYWHITE);
// Shield Bar
float shRatio = static_cast<float>(player.getShield()) / player.getMaxShield();
drawProgressBar(hudX + 60, hudY + 68, 250, 18, shRatio, CyberColors::NeonCyan, Color{ 10, 25, 45, 255 });
DrawText("SHLD", hudX + 15, hudY + 68, 14, CyberColors::NeonCyan);
DrawText((std::to_string(player.getShield()) + " / " + std::to_string(player.getMaxShield())).c_str(), hudX + 145, hudY + 70, 13, RAYWHITE);
// Cyber-Picks & Statuses
DrawText(("ICE PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 102, 15, CyberColors::NeonYellow);
std::string statStr = "STATUS: ";
if (player.getStatuses().empty()) {
statStr += "SECURE";
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, CyberColors::NeonGreen);
} else {
statStr += "MALWARE_DEBUFF";
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, RED);
}
// Top-Right: Holographic Minimap
drawMinimap(dungeon, player, GetScreenWidth() - 200, 20, 180);
// Bottom-Right: Revolver Ready Ammo preview
int revX = GetScreenWidth() - 260;
int revY = GetScreenHeight() - 110;
DrawRectangle(revX, revY, 240, 90, Color{ 6, 12, 22, 220 });
DrawRectangleLines(revX, revY, 240, 90, CyberColors::NeonCyan);
AmmoType curAmmo = player.getRevolver().getCurrentAmmo();
AmmoType nextAmmo = player.getRevolver().getNextAmmo();
const auto& curInfo = getAmmoInfo(curAmmo);
const auto& nextInfo = getAmmoInfo(nextAmmo);
DrawText("REVOLVER READY", revX + 15, revY + 10, 13, CyberColors::NeonCyan);
DrawCircle(revX + 25, revY + 42, 10, curInfo.color);
DrawText(curInfo.name.c_str(), revX + 45, revY + 34, 15, curInfo.color);
DrawText(("NEXT: " + nextInfo.shortName).c_str(), revX + 15, revY + 65, 12, GRAY);
DrawText("[TAB] ARMORY", revX + 140, revY + 65, 12, CyberColors::NeonYellow);
// Bottom-Center: Context Action Prompt
if (!prompt.empty()) {
int promptW = MeasureText(prompt.c_str(), 18) + 40;
int promptX = (GetScreenWidth() - promptW) / 2;
int promptY = GetScreenHeight() - 70;
DrawRectangle(promptX, promptY, promptW, 45, Color{ 10, 18, 32, 240 });
DrawRectangleLines(promptX, promptY, promptW, 45, CyberColors::NeonYellow);
DrawText(prompt.c_str(), promptX + 20, promptY + 13, 18, CyberColors::NeonYellow);
}
// Controls hints in bottom-left
DrawText("[W/S] WALK | [A/D] TURN 90 | [Q/E] STRAFE | [SPACE/E] INTERACT | [TAB] ARMORY",
20, GetScreenHeight() - 35, 13, Color{ 120, 160, 200, 220 });
}
void UIRenderer::drawHackingUI(const HackingGame& hacking) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 235 });
int panelW = 760;
int panelH = 520;
int panelX = (GetScreenWidth() - panelW) / 2;
int panelY = (GetScreenHeight() - panelH) / 2;
DrawRectangle(panelX, panelY, panelW, panelH, Color{ 8, 14, 26, 255 });
DrawRectangleLines(panelX, panelY, panelW, panelH, CyberColors::NeonCyan);
// Header
DrawRectangle(panelX, panelY, panelW, 50, Color{ 14, 24, 42, 255 });
DrawText(("SECURITY ICE BYPASS TERMINAL // DEFENSE LEVEL 0" + std::to_string(hacking.getSecurityLevel())).c_str(),
panelX + 25, panelY + 16, 18, CyberColors::NeonCyan);
if (hacking.getPhase() == HackingPhase::PHASE_1_TUMBLER) {
DrawText("PHASE 1: FREQUENCY TUMBLER ALIGNMENT", panelX + 25, panelY + 65, 16, CyberColors::NeonYellow);
DrawText("Tune each pin into the sweet spot frequency band, then lock it into place.", panelX + 25, panelY + 90, 13, RAYWHITE);
const auto& tumbler = hacking.getTumbler();
int pinCount = tumbler.getPinCount();
int pinW = 60;
int pinGap = 40;
int totalPinsW = pinCount * pinW + (pinCount - 1) * pinGap;
int startPinX = panelX + (panelW - totalPinsW) / 2;
int trackY = panelY + 130;
int trackH = 260;
for (int i = 0; i < pinCount; ++i) {
const auto& pin = tumbler.getPin(i);
int px = startPinX + i * (pinW + pinGap);
bool isSelected = (i == tumbler.getSelectedPin());
DrawRectangle(px, trackY, pinW, trackH, Color{ 14, 20, 32, 255 });
DrawRectangleLines(px, trackY, pinW, trackH, isSelected ? CyberColors::NeonYellow : Color{ 40, 60, 90, 255 });
// Sweet spot target zone
int sweetY = trackY + static_cast<int>((1.0f - pin.targetHeight) * trackH);
int sweetSpan = static_cast<int>(pin.tolerance * 2.0f * trackH);
DrawRectangle(px + 2, sweetY - sweetSpan / 2, pinW - 4, sweetSpan, Color{ 0, 240, 255, 60 });
DrawLine(px, sweetY, px + pinW, sweetY, CyberColors::NeonCyan);
int pinH = 40;
int pinCurrY = trackY + static_cast<int>((1.0f - pin.currentHeight) * (trackH - pinH));
Color pinColor = pin.locked ? CyberColors::NeonGreen :
(pin.isInSweetSpot() ? CyberColors::NeonYellow : Color{ 70, 110, 160, 255 });
DrawRectangle(px + 4, pinCurrY, pinW - 8, pinH, pinColor);
DrawRectangleLines(px + 4, pinCurrY, pinW - 8, pinH, WHITE);
if (pin.locked) {
DrawText("LOCKED", px + 8, trackY + trackH + 12, 12, CyberColors::NeonGreen);
} else if (pin.isInSweetSpot()) {
DrawText("SWEET", px + 12, trackY + trackH + 12, 12, CyberColors::NeonYellow);
} else {
DrawText(std::to_string(i + 1).c_str(), px + 24, trackY + trackH + 12, 14, GRAY);
}
}
DrawRectangle(panelX + 20, panelY + panelH - 65, panelW - 40, 45, Color{ 12, 18, 30, 255 });
if (tumbler.isAllPinsLocked()) {
DrawText("[ENTER / SPACE] ALL PINS LOCKED! INITIATE DEAD-LOCK TIMING CHECK", panelX + 70, panelY + panelH - 52, 16, CyberColors::NeonGreen);
} else {
DrawText("[A/D] Select Pin [W/S] Tune Frequency [SPACE] Lock Pin", panelX + 130, panelY + panelH - 52, 14, CyberColors::NeonCyan);
}
} else if (hacking.getPhase() == HackingPhase::PHASE_2_SKILLCHECK) {
DrawText("PHASE 2: DEAD-LOCK TIMING INJECTION (DBD SKILL CHECK)", panelX + 25, panelY + 65, 16, CyberColors::NeonPink);
DrawText("STRIKE [SPACE] WHEN THE NEEDLE HITS THE TARGET ZONE!", panelX + 25, panelY + 90, 14, CyberColors::NeonYellow);
const auto& sc = hacking.getSkillCheck();
int dialCenterX = panelX + panelW / 2;
int dialCenterY = panelY + panelH / 2 + 10;
float dialRadius = 130.0f;
DrawCircle(dialCenterX, dialCenterY, dialRadius + 12.0f, Color{ 10, 16, 26, 255 });
DrawCircleLines(dialCenterX, dialCenterY, dialRadius + 12.0f, CyberColors::NeonCyan);
DrawCircle(dialCenterX, dialCenterY, dialRadius - 20.0f, Color{ 6, 10, 18, 255 });
// Success zone
DrawCircleSectorLines({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) },
dialRadius,
sc.getSuccessStart() - 90.0f,
sc.getSuccessEnd() - 90.0f,
32, RAYWHITE);
DrawCircleSector({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) },
dialRadius,
sc.getSuccessStart() - 90.0f,
sc.getSuccessEnd() - 90.0f,
32, Color{ 220, 220, 220, 120 });
// Great success zone
DrawCircleSector({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) },
dialRadius,
sc.getGreatStart() - 90.0f,
sc.getGreatEnd() - 90.0f,
16, CyberColors::NeonGreen);
// Rotating needle
float needleRad = (sc.getNeedleAngle() - 90.0f) * (3.14159265f / 180.0f);
int needleEndX = dialCenterX + static_cast<int>(std::cos(needleRad) * (dialRadius + 8.0f));
int needleEndY = dialCenterY + static_cast<int>(std::sin(needleRad) * (dialRadius + 8.0f));
DrawLineEx({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) },
{ static_cast<float>(needleEndX), static_cast<float>(needleEndY) },
4.0f, RED);
DrawCircle(dialCenterX, dialCenterY, 14, RED);
DrawText("HIT [ SPACE ]", dialCenterX - 60, dialCenterY + static_cast<int>(dialRadius) + 30, 20, CyberColors::NeonYellow);
} else {
int cx = panelX + panelW / 2;
int cy = panelY + panelH / 2;
if (hacking.getPhase() == HackingPhase::RESULT_CRITICAL) {
DrawText(">>> CRITICAL OVERRIDE SUCCESS! <<<", cx - 210, cy - 40, 24, CyberColors::NeonGreen);
DrawText("Subroutines penetrated. Bonus cyber-cartridge recovered!", cx - 200, cy + 10, 16, RAYWHITE);
} else if (hacking.getPhase() == HackingPhase::RESULT_SUCCESS) {
DrawText(">>> ACCESS GRANTED <<<", cx - 140, cy - 40, 26, CyberColors::NeonCyan);
DrawText("Security locks bypassed successfully.", cx - 150, cy + 10, 16, RAYWHITE);
} else {
DrawText("!!! ICE ALARM TRIGGERED !!!", cx - 180, cy - 40, 26, RED);
DrawText("Intrusion countermeasures activated. Defense Sentinel deployed!", cx - 240, cy + 10, 16, CyberColors::NeonOrange);
}
}
}
void UIRenderer::drawRevolverCylinderCombat(const Revolver& revolver, int centerX, int centerY, float animTime, bool isSpinning) {
float cylinderR = 95.0f;
float chamberOrbitR = cylinderR - 30.0f;
float chamberR = 20.0f;
// 1. Heavy gunmetal outer housing
DrawCircle(centerX, centerY, cylinderR + 14.0f, Color{ 8, 14, 24, 250 });
DrawCircleLines(centerX, centerY, cylinderR + 14.0f, Color{ 30, 50, 75, 255 });
// Metallic cylinder body
DrawCircle(centerX, centerY, cylinderR, Color{ 20, 28, 42, 255 });
DrawCircleLines(centerX, centerY, cylinderR, CyberColors::NeonCyan);
// Decorative flutes/grooves between chambers (6 radial outer notches)
float visAngle = revolver.getVisualAngle();
for (int i = 0; i < 6; ++i) {
float fluteAngle = (-visAngle + i * 60.0f + 30.0f - 90.0f) * (3.14159265f / 180.0f);
int fluteX = centerX + static_cast<int>(std::cos(fluteAngle) * (cylinderR + 2.0f));
int fluteY = centerY + static_cast<int>(std::sin(fluteAngle) * (cylinderR + 2.0f));
DrawCircle(fluteX, fluteY, 12.0f, Color{ 8, 14, 24, 255 });
DrawCircleLines(fluteX, fluteY, 12.0f, Color{ 40, 65, 95, 255 });
}
// 2. Center extractor ratchet & hub
DrawCircle(centerX, centerY, 34.0f, Color{ 14, 20, 32, 255 });
DrawCircleLines(centerX, centerY, 34.0f, Color{ 70, 100, 140, 255 });
DrawCircle(centerX, centerY, 14.0f, Color{ 220, 175, 45, 255 });
DrawCircleLines(centerX, centerY, 14.0f, CyberColors::NeonYellow);
DrawText(".357", centerX - 15, centerY - 6, 12, CyberColors::NeonYellow);
// 3. Golden Striker Hammer / Firing Pin at 12 o'clock (-90 deg / top)
float strikerPulse = 0.5f + 0.5f * std::sin(animTime * 6.0f);
int strikerTipY = centerY - static_cast<int>(cylinderR) - 4;
int strikerBaseY = centerY - static_cast<int>(cylinderR) - 26;
DrawTriangle(
{ static_cast<float>(centerX), static_cast<float>(strikerTipY) },
{ static_cast<float>(centerX - 12), static_cast<float>(strikerBaseY) },
{ static_cast<float>(centerX + 12), static_cast<float>(strikerBaseY) },
Color{ 255, 210, 50, 255 }
);
DrawRectangle(centerX - 18, strikerBaseY - 14, 36, 14, Color{ 210, 160, 30, 255 });
DrawRectangleLines(centerX - 18, strikerBaseY - 14, 36, 14, CyberColors::NeonYellow);
DrawCircle(centerX, strikerTipY, 2.5f + strikerPulse * 1.5f, CyberColors::NeonYellow);
// Striker tag
DrawRectangle(centerX - 65, strikerBaseY - 32, 130, 16, Color{ 10, 16, 26, 230 });
DrawRectangleLines(centerX - 65, strikerBaseY - 32, 130, 16, Color{ 180, 150, 40, 200 });
DrawText("FIRING PIN [12:00]", centerX - 58, strikerBaseY - 30, 11, CyberColors::NeonYellow);
// 4. Six Chambers (Top chamber at 12:00 aligns with striker when visualAngle == index * 60)
for (int i = 0; i < 6; ++i) {
float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f);
int chX = centerX + static_cast<int>(std::cos(angleRad) * chamberOrbitR);
int chY = centerY + static_cast<int>(std::sin(angleRad) * chamberOrbitR);
AmmoType ammo = revolver.getChamber(i);
const auto& info = getAmmoInfo(ammo);
bool isCurrent = (i == revolver.getCurrentIndex());
DrawCircle(chX, chY, chamberR, Color{ 16, 22, 34, 255 });
DrawCircleLines(chX, chY, chamberR, isCurrent ? CyberColors::NeonYellow : Color{ 55, 80, 120, 255 });
if (ammo == AmmoType::EMPTY) {
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 8, 12, 18, 255 });
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 60, 60, 60, 255 });
DrawText("0", chX - 4, chY - 6, 12, DARKGRAY);
} else {
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 34, 46, 62, 255 });
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 180, 140, 50, 255 });
DrawCircle(chX, chY, chamberR - 8.0f, info.color);
DrawCircle(chX - 3, chY - 3, 2.5f, Color{ 255, 255, 255, 200 });
DrawText(info.shortName.substr(0, 3).c_str(), chX - 9, chY - 5, 10, BLACK);
}
if (isCurrent) {
float reticleR = chamberR + 5.0f + 2.0f * std::sin(animTime * 6.0f);
DrawCircleLines(chX, chY, reticleR, CyberColors::NeonYellow);
DrawLine(chX - static_cast<int>(reticleR) - 3, chY, chX - static_cast<int>(reticleR) + 3, chY, CyberColors::NeonYellow);
DrawLine(chX + static_cast<int>(reticleR) - 3, chY, chX + static_cast<int>(reticleR) + 3, chY, CyberColors::NeonYellow);
DrawLine(chX, chY - static_cast<int>(reticleR) - 3, chX, chY - static_cast<int>(reticleR) + 3, CyberColors::NeonYellow);
DrawLine(chX, chY + static_cast<int>(reticleR) - 3, chX, chY + static_cast<int>(reticleR) + 3, CyberColors::NeonYellow);
}
}
}
void UIRenderer::drawCodexModal() {
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
DrawRectangle(0, 0, screenW, screenH, Color{ 4, 8, 16, 235 });
int modalW = 980;
int modalH = 580;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
DrawRectangle(modalX, modalY, modalW, modalH, Color{ 8, 14, 26, 255 });
DrawRectangleLines(modalX, modalY, modalW, modalH, CyberColors::NeonCyan);
// Decorative corners
DrawRectangle(modalX - 2, modalY - 2, 8, 8, CyberColors::NeonCyan);
DrawRectangle(modalX + modalW - 6, modalY - 2, 8, 8, CyberColors::NeonCyan);
DrawRectangle(modalX - 2, modalY + modalH - 6, 8, 8, CyberColors::NeonCyan);
DrawRectangle(modalX + modalW - 6, modalY + modalH - 6, 8, 8, CyberColors::NeonCyan);
// Header banner
DrawRectangle(modalX, modalY, modalW, 48, Color{ 14, 24, 42, 255 });
DrawText("// TACTICAL AMMUNITION & COMBAT CODEX // DATABASE 404", modalX + 25, modalY + 15, 17, CyberColors::NeonCyan);
DrawText("[H] / [ESC] TO CLOSE", modalX + modalW - 190, modalY + 16, 13, CyberColors::NeonYellow);
// Left column: 7 Ammo Types breakdown
int leftX = modalX + 25;
int colY = modalY + 62;
DrawText("AMMUNITION CARTRIDGES (7 PROTOCOLS)", leftX, colY, 14, CyberColors::NeonYellow);
AmmoType ammos[7] = {
AmmoType::KINETIC,
AmmoType::EMP,
AmmoType::OVERHEAT,
AmmoType::CORROSIVE,
AmmoType::GLITCH,
AmmoType::NANITE,
AmmoType::EMPTY
};
int cardY = colY + 24;
for (int i = 0; i < 7; ++i) {
const auto& inf = getAmmoInfo(ammos[i]);
DrawRectangle(leftX, cardY, 455, 46, Color{ 12, 18, 30, 255 });
DrawRectangleLines(leftX, cardY, 455, 46, (ammos[i] == AmmoType::EMPTY) ? DARKGRAY : inf.color);
DrawCircle(leftX + 20, cardY + 23, 10.0f, (ammos[i] == AmmoType::EMPTY) ? Color{ 25, 25, 25, 255 } : inf.color);
DrawCircleLines(leftX + 20, cardY + 23, 10.0f, WHITE);
std::string title = inf.name + " [" + inf.shortName + "]";
if (ammos[i] != AmmoType::EMPTY) {
title += " // DMG: " + std::to_string(inf.baseDamage);
}
DrawText(title.c_str(), leftX + 40, cardY + 8, 13, inf.color);
DrawText(inf.description.c_str(), leftX + 40, cardY + 25, 11, RAYWHITE);
cardY += 52;
}
// Right column: Combat Rules & Tactical Intel
int rightX = modalX + 505;
DrawText("TACTICAL COMBAT MECHANICS & PROTOCOLS", rightX, colY, 14, CyberColors::NeonYellow);
struct GuideRule {
std::string title;
std::string desc1;
std::string desc2;
Color color;
};
GuideRule rules[4] = {
{ "1. THE 12:00 FIRING PIN RULE",
"The cylinder spins roulette-style. The chamber aligned at 12 o'clock",
"directly beneath the golden striker is the round that detonates.",
CyberColors::NeonYellow },
{ "2. CRITICAL SURGE WEAKNESS (+100% DMG)",
"Scan the hostile unit data scanner at top. Matching target weakness",
"immediately triggers a Critical Surge dealing +100% double damage!",
CyberColors::NeonGreen },
{ "3. DEFENSE MATRIX OVERCLOCK",
"Use [2] Overclock Shield when facing heavy retaliation or low ammo.",
"Instantly grants +25 shield and cuts incoming damage by 50% for 1 turn.",
CyberColors::NeonCyan },
{ "4. FAN THE HAMMER (3-ROUND BURST)",
"Protocol [3] discharges 3 consecutive chambers in rapid succession.",
"Verify your cylinder is filled in [TAB] Armory for devastating bursts!",
CyberColors::NeonPink }
};
int ruleY = colY + 24;
for (int i = 0; i < 4; ++i) {
DrawRectangle(rightX, ruleY, 450, 84, Color{ 12, 18, 30, 255 });
DrawRectangleLines(rightX, ruleY, 450, 84, Color{ 40, 65, 100, 255 });
DrawText(rules[i].title.c_str(), rightX + 15, ruleY + 10, 13, rules[i].color);
DrawText(rules[i].desc1.c_str(), rightX + 15, ruleY + 34, 11, RAYWHITE);
DrawText(rules[i].desc2.c_str(), rightX + 15, ruleY + 52, 11, RAYWHITE);
ruleY += 92;
}
// Close Button at bottom
Rectangle btnClose = { static_cast<float>(modalX + modalW / 2 - 120), static_cast<float>(modalY + modalH - 52), 240.0f, 38.0f };
Vector2 mousePos = GetMousePosition();
bool hoverClose = CheckCollisionPointRec(mousePos, btnClose);
DrawRectangleRec(btnClose, hoverClose ? Color{ 40, 80, 120, 255 } : Color{ 20, 36, 60, 255 });
DrawRectangleLinesEx(btnClose, hoverClose ? 2.0f : 1.0f, hoverClose ? CyberColors::NeonYellow : CyberColors::NeonCyan);
DrawText("[ CLOSE CODEX (PRESS H) ]", modalX + modalW / 2 - 95, modalY + modalH - 40, 13, hoverClose ? CyberColors::NeonYellow : WHITE);
}
void UIRenderer::drawCombatUI(const CombatSystem& combat, const Player& player, float animTime, bool showCodex) {
const Enemy* enemy = combat.getEnemy();
if (!enemy) return;
// Digital matrix rain in combat background
drawMatrixDigitalRain(animTime);
// 1. Top: Tactical Target Scanner HUD
int scanW = 760;
int scanH = 145;
int scanX = (GetScreenWidth() - scanW) / 2;
int scanY = 18;
DrawRectangle(scanX, scanY, scanW, scanH, Color{ 8, 14, 26, 240 });
DrawRectangleLines(scanX, scanY, scanW, scanH, enemy->getColor());
// Corner decorative accents
DrawRectangle(scanX - 2, scanY - 2, 8, 8, enemy->getColor());
DrawRectangle(scanX + scanW - 6, scanY - 2, 8, 8, enemy->getColor());
DrawRectangle(scanX - 2, scanY + scanH - 6, 8, 8, enemy->getColor());
DrawRectangle(scanX + scanW - 6, scanY + scanH - 6, 8, 8, enemy->getColor());
DrawText("// TACTICAL TARGET SCANNER // HOSTILE_UNIT_DATA", scanX + 25, scanY + 10, 11, CyberColors::NeonCyan);
DrawText(enemy->getName().c_str(), scanX + 25, scanY + 26, 22, enemy->getColor());
// Core HP Segmented Bar
float eHpRatio = static_cast<float>(enemy->getHp()) / enemy->getMaxHp();
int hpBarW = scanW - 220;
drawProgressBar(scanX + 105, scanY + 54, hpBarW, 20, eHpRatio, enemy->getColor(), Color{ 36, 12, 18, 255 });
DrawText("CORE HP", scanX + 25, scanY + 56, 14, RAYWHITE);
std::string hpTxt = std::to_string(enemy->getHp()) + " / " + std::to_string(enemy->getMaxHp()) +
" (" + std::to_string(static_cast<int>(eHpRatio * 100)) + "%)";
DrawText(hpTxt.c_str(), scanX + scanW / 2 - 40, scanY + 56, 13, WHITE);
// Shield Segmented Bar
float eShRatio = (enemy->getMaxShield() > 0) ? (static_cast<float>(enemy->getShield()) / enemy->getMaxShield()) : 0.0f;
drawProgressBar(scanX + 105, scanY + 80, hpBarW, 16, eShRatio, CyberColors::NeonCyan, Color{ 14, 24, 44, 255 });
DrawText("SHIELD", scanX + 25, scanY + 81, 14, CyberColors::NeonCyan);
std::string shTxt = std::to_string(enemy->getShield()) + " / " + std::to_string(enemy->getMaxShield()) +
" (" + std::to_string(static_cast<int>(eShRatio * 100)) + "%)";
DrawText(shTxt.c_str(), scanX + scanW / 2 - 35, scanY + 81, 12, WHITE);
// Weakness & Weapon Tags Row
const auto& atkInfo = getAmmoInfo(enemy->getAttackElement());
const auto& wknInfo = getAmmoInfo(enemy->getWeakness());
const auto& rev = player.getRevolver();
DrawText("WEAPON:", scanX + 25, scanY + 112, 12, GRAY);
DrawText(atkInfo.shortName.c_str(), scanX + 85, scanY + 112, 12, atkInfo.color);
DrawText("WEAKNESS:", scanX + 160, scanY + 112, 12, GRAY);
DrawRectangle(scanX + 235, scanY + 107, 240, 24, Color{ 35, 28, 12, 240 });
DrawRectangleLines(scanX + 235, scanY + 107, 240, 24, CyberColors::NeonYellow);
std::string wknText = wknInfo.shortName + " -> +100% CRIT SURGE!";
DrawText(wknText.c_str(), scanX + 242, scanY + 112, 12, CyberColors::NeonYellow);
if (rev.getCurrentAmmo() == enemy->getWeakness()) {
float surgePulse = 0.5f + 0.5f * std::sin(animTime * 10.0f);
Color badgeCol = (surgePulse > 0.5f) ? CyberColors::NeonGreen : CyberColors::NeonYellow;
DrawText("[SURGE PRIMED!]", scanX + 482, scanY + 112, 12, badgeCol);
}
// Active Status Chips
int chipX = scanX + scanW - 20;
const auto& statuses = enemy->getStatuses();
if (statuses.empty()) {
int w = MeasureText("[STATUS: NOMINAL]", 11);
DrawText("[STATUS: NOMINAL]", chipX - w, scanY + 112, 11, Color{ 120, 150, 180, 255 });
} else {
int curRight = chipX;
for (const auto& st : statuses) {
std::string stStr = "";
Color stCol = WHITE;
switch (st.effect) {
case StatusEffect::BURNING:
stStr = "BURN " + std::to_string(st.durationTurns) + "T";
stCol = CyberColors::NeonOrange;
break;
case StatusEffect::SHOCKED:
stStr = "STUN " + std::to_string(st.durationTurns) + "T";
stCol = CyberColors::NeonCyan;
break;
case StatusEffect::CORRODED:
stStr = "ACID +50% " + std::to_string(st.durationTurns) + "T";
stCol = CyberColors::NeonGreen;
break;
case StatusEffect::GLITCHED:
stStr = "GLITCH " + std::to_string(st.durationTurns) + "T";
stCol = CyberColors::NeonPurple;
break;
default: break;
}
if (!stStr.empty()) {
int stW = MeasureText(stStr.c_str(), 11) + 12;
curRight -= (stW + 6);
DrawRectangle(curRight, scanY + 108, stW, 20, Color{ 15, 22, 35, 230 });
DrawRectangleLines(curRight, scanY + 108, stW, 20, stCol);
DrawText(stStr.c_str(), curRight + 6, scanY + 112, 11, stCol);
}
}
}
// 2. Center-Left: Revolver Cylinder & Under-cylinder Odds breakdown
int drumCenterX = 210;
int drumCenterY = 320;
drawRevolverCylinderCombat(rev, drumCenterX, drumCenterY, animTime, rev.isSpinning());
AmmoType curAmmo = rev.getCurrentAmmo();
const auto& curInfo = getAmmoInfo(curAmmo);
int activeW = 310;
int activeX = drumCenterX - activeW / 2;
int activeY = drumCenterY + 115;
DrawRectangle(activeX, activeY, activeW, 46, Color{ 10, 16, 28, 240 });
DrawRectangleLines(activeX, activeY, activeW, 46, curAmmo == AmmoType::EMPTY ? RED : curInfo.color);
DrawText("12:00 CHAMBER // PRIMED TO FIRE", activeX + 12, activeY + 6, 10, CyberColors::NeonCyan);
if (curAmmo == AmmoType::EMPTY) {
DrawText("BLANK // DRY-FIRE MISFIRE (0 DMG)", activeX + 12, activeY + 22, 14, RED);
} else {
std::string actTxt = curInfo.name + " (" + std::to_string(curInfo.baseDamage) + " DMG)";
DrawText(actTxt.c_str(), activeX + 12, activeY + 22, 14, curInfo.color);
}
int statsY = activeY + 52;
int statsH = 68;
DrawRectangle(activeX, statsY, activeW, statsH, Color{ 8, 14, 24, 240 });
DrawRectangleLines(activeX, statsY, activeW, statsH, Color{ 35, 60, 95, 255 });
DrawText("// AMMO PROBABILITY ROULETTE", activeX + 12, statsY + 6, 10, GRAY);
int loadedCount = 0;
int typeCounts[7] = { 0 };
for (int i = 0; i < 6; ++i) {
AmmoType a = rev.getChamber(i);
typeCounts[static_cast<int>(a)]++;
if (a != AmmoType::EMPTY) loadedCount++;
}
int hitChance = (loadedCount * 100) / 6;
int blankChance = 100 - hitChance;
std::string oddsStr = "LOADED: " + std::to_string(loadedCount) + "/6 (" + std::to_string(hitChance) + "% HIT)";
DrawText(oddsStr.c_str(), activeX + 12, statsY + 22, 13, hitChance >= 50 ? CyberColors::NeonGreen : CyberColors::NeonYellow);
std::string blankStr = "MISFIRE RISK: " + std::to_string(blankChance) + "%";
DrawText(blankStr.c_str(), activeX + 175, statsY + 22, 13, blankChance == 0 ? CyberColors::NeonGreen : (blankChance > 33 ? RED : CyberColors::NeonYellow));
std::string chipSummary = "";
for (int t = 1; t <= 6; ++t) {
if (typeCounts[t] > 0) {
const auto& inf = getAmmoInfo(static_cast<AmmoType>(t));
if (!chipSummary.empty()) chipSummary += " | ";
chipSummary += inf.shortName + "x" + std::to_string(typeCounts[t]);
}
}
if (chipSummary.empty()) chipSummary = "ALL CHAMBERS EMPTY - RELOAD IN ARMORY";
DrawText(chipSummary.c_str(), activeX + 12, statsY + 44, 11, Color{ 170, 195, 225, 255 });
// 3. Center-Right: Combat Data Stream & Operator Vitals
int logX = 770;
int logY = 180;
int logW = 460;
int logH = 210;
DrawRectangle(logX, logY, logW, logH, Color{ 6, 12, 22, 245 });
DrawRectangleLines(logX, logY, logW, logH, Color{ 30, 55, 90, 255 });
DrawText("// COMBAT_DATA_STREAM // REAL-TIME LOG", logX + 15, logY + 10, 11, CyberColors::NeonCyan);
const auto& logs = combat.getLog();
int logTextY = logY + 30;
for (const auto& line : logs) {
Color logCol = RAYWHITE;
if (line.find("CRITICAL") != std::string::npos || line.find("SURGE") != std::string::npos) logCol = CyberColors::NeonYellow;
else if (line.find("FLATLINE") != std::string::npos || line.find("attacks") != std::string::npos) logCol = Color{ 255, 120, 120, 255 };
else if (line.find("STUNNED") != std::string::npos || line.find("Nanites") != std::string::npos) logCol = CyberColors::NeonGreen;
else if (line.find("FAN THE HAMMER") != std::string::npos) logCol = CyberColors::NeonPink;
DrawText(("> " + line).c_str(), logX + 15, logTextY, 13, logCol);
logTextY += 21;
}
int pvY = logY + logH + 12;
int pvH = 78;
DrawRectangle(logX, pvY, logW, pvH, Color{ 8, 14, 26, 245 });
DrawRectangleLines(logX, pvY, logW, pvH, CyberColors::NeonGreen);
DrawText("// OPERATOR VITALS // NEURAL SYNC", logX + 15, pvY + 8, 10, CyberColors::NeonGreen);
float pHpRatio = static_cast<float>(player.getHp()) / player.getMaxHp();
drawProgressBar(logX + 80, pvY + 26, logW - 190, 18, pHpRatio, CyberColors::NeonGreen, Color{ 35, 12, 15, 255 });
DrawText("HULL HP", logX + 15, pvY + 28, 12, RAYWHITE);
std::string pHpStr = std::to_string(player.getHp()) + "/" + std::to_string(player.getMaxHp());
DrawText(pHpStr.c_str(), logX + 85 + (logW - 190) / 2 - 25, pvY + 28, 12, WHITE);
float pShRatio = (player.getMaxShield() > 0) ? (static_cast<float>(player.getShield()) / player.getMaxShield()) : 0.0f;
drawProgressBar(logX + 80, pvY + 50, logW - 190, 16, pShRatio, CyberColors::NeonCyan, Color{ 12, 22, 40, 255 });
DrawText("SHIELD", logX + 15, pvY + 51, 12, CyberColors::NeonCyan);
std::string pShStr = std::to_string(player.getShield()) + "/" + std::to_string(player.getMaxShield());
DrawText(pShStr.c_str(), logX + 85 + (logW - 190) / 2 - 20, pvY + 51, 11, WHITE);
Rectangle btnCodex = { static_cast<float>(logX + logW - 95), static_cast<float>(pvY + 25), 85.0f, 40.0f };
Vector2 mousePos = GetMousePosition();
bool hoverCodex = CheckCollisionPointRec(mousePos, btnCodex);
DrawRectangleRec(btnCodex, hoverCodex ? Color{ 40, 65, 100, 255 } : Color{ 16, 28, 48, 255 });
DrawRectangleLinesEx(btnCodex, hoverCodex ? 2.0f : 1.0f, hoverCodex ? CyberColors::NeonYellow : CyberColors::NeonCyan);
DrawText("[H] CODEX", logX + logW - 87, pvY + 38, 12, hoverCodex ? CyberColors::NeonYellow : RAYWHITE);
// 4. Center: Tactical AI Advisor Hologram Ticker
int advX = 380;
int advY = 485;
int advW = 370;
int advH = 46;
DrawRectangle(advX, advY, advW, advH, Color{ 8, 16, 30, 240 });
DrawRectangleLines(advX, advY, advW, advH, CyberColors::NeonCyan);
float aiPulse = 0.5f + 0.5f * std::sin(animTime * 5.0f);
DrawCircle(advX + 18, advY + 23, 6.0f + aiPulse * 2.0f, CyberColors::NeonCyan);
DrawCircleLines(advX + 18, advY + 23, 10.0f, CyberColors::NeonYellow);
DrawText("AI ADVISOR", advX + 32, advY + 8, 10, CyberColors::NeonYellow);
std::string advisorMsg = "";
if (rev.getCurrentAmmo() == enemy->getWeakness()) {
advisorMsg = "WEAKNESS MATCH! Striking now triggers +100% Critical Surge!";
} else if (rev.getCurrentAmmo() == AmmoType::EMPTY) {
advisorMsg = "WARNING: 12:00 chamber is EMPTY! Will misfire. Spin again or shield!";
} else if (enemy->getShield() > 0 && rev.getCurrentAmmo() == AmmoType::EMP) {
advisorMsg = "EMP PRIMED: Discharge to collapse enemy barrier and stun!";
} else if (player.getHp() < 35) {
advisorMsg = "CRITICAL: Hull integrity low! Overclock Shield [2] recommended!";
} else if (combat.getTripleCooldown() == 0 && loadedCount >= 3) {
advisorMsg = "OPPORTUNITY: Fan the Hammer [3] ready for 3-round rapid burst!";
} else if (enemy->getShield() > 0) {
advisorMsg = "Hostile shield online. Kinetic sabots or EMP advised.";
} else {
advisorMsg = "Spin cylinder [1] to strike, or press [H] to review Ammunition Codex.";
}
DrawText(advisorMsg.c_str(), advX + 32, advY + 24, 11, RAYWHITE);
// 5. Bottom Action Bar: Cyber Ability Cards
int barX = 50;
int barY = 548;
int barW = GetScreenWidth() - 100;
int barH = 120;
DrawRectangle(barX, barY, barW, barH, Color{ 8, 14, 24, 245 });
DrawRectangleLines(barX, barY, barW, barH, CyberColors::NeonCyan);
if (combat.getPhase() == CombatPhase::PLAYER_TURN) {
// Card 1: [1 / SPACE] SPIN & SHOOT
Rectangle btnShoot = { static_cast<float>(barX + 15), static_cast<float>(barY + 12), 370.0f, 96.0f };
bool hoverShoot = CheckCollisionPointRec(mousePos, btnShoot);
DrawRectangleRec(btnShoot, hoverShoot ? Color{ 25, 45, 75, 255 } : Color{ 14, 24, 42, 255 });
DrawRectangleLinesEx(btnShoot, hoverShoot ? 2.0f : 1.0f, hoverShoot ? CyberColors::NeonGreen : CyberColors::NeonYellow);
DrawText("// PROTOCOL 01: ROULETTE", barX + 30, barY + 20, 10, CyberColors::NeonCyan);
DrawText("[1 / SPACE] SPIN & SHOOT", barX + 30, barY + 36, 18, hoverShoot ? CyberColors::NeonGreen : CyberColors::NeonYellow);
DrawText("Spins cylinder randomly & strikes 12:00 chamber.", barX + 30, barY + 62, 12, RAYWHITE);
std::string oddsBadge = "ODDS: " + std::to_string(hitChance) + "% HIT / " + std::to_string(blankChance) + "% BLANK";
DrawText(oddsBadge.c_str(), barX + 30, barY + 82, 12, hitChance >= 50 ? CyberColors::NeonGreen : CyberColors::NeonOrange);
// Card 2: [2] OVERCLOCK SHIELD
Rectangle btnShield = { static_cast<float>(barX + 405), static_cast<float>(barY + 12), 370.0f, 96.0f };
bool hoverShield = CheckCollisionPointRec(mousePos, btnShield);
DrawRectangleRec(btnShield, hoverShield ? Color{ 25, 45, 75, 255 } : Color{ 14, 24, 42, 255 });
DrawRectangleLinesEx(btnShield, hoverShield ? 2.0f : 1.0f, hoverShield ? CyberColors::NeonGreen : CyberColors::NeonCyan);
DrawText("// PROTOCOL 02: DEFENSE MATRIX", barX + 420, barY + 20, 10, CyberColors::NeonCyan);
DrawText("[2] OVERCLOCK SHIELD", barX + 420, barY + 36, 18, hoverShield ? CyberColors::NeonGreen : CyberColors::NeonCyan);
DrawText("Instantly adds +25 Shield & absorbs 50% damage this turn.", barX + 420, barY + 62, 12, RAYWHITE);
DrawText("ACTIVE MITIGATION // ALWAYS READY", barX + 420, barY + 82, 12, CyberColors::NeonGreen);
// Card 3: [3] FAN THE HAMMER
int cd = combat.getTripleCooldown();
Rectangle btnTriple = { static_cast<float>(barX + 795), static_cast<float>(barY + 12), 370.0f, 96.0f };
bool hoverTriple = CheckCollisionPointRec(mousePos, btnTriple) && (cd == 0);
Color tripleBg = (cd == 0) ? (hoverTriple ? Color{ 60, 25, 55, 255 } : Color{ 32, 16, 32, 255 }) : Color{ 14, 16, 22, 255 };
Color tripleBorder = (cd == 0) ? (hoverTriple ? CyberColors::NeonPink : Color{ 220, 70, 150, 255 }) : Color{ 45, 50, 65, 255 };
Color tripleTitle = (cd == 0) ? (hoverTriple ? WHITE : CyberColors::NeonPink) : GRAY;
DrawRectangleRec(btnTriple, tripleBg);
DrawRectangleLinesEx(btnTriple, hoverTriple ? 2.0f : 1.0f, tripleBorder);
DrawText("// PROTOCOL 03: BURST SURGE", barX + 810, barY + 20, 10, (cd == 0) ? CyberColors::NeonPink : GRAY);
DrawText("[3] FAN THE HAMMER", barX + 810, barY + 36, 18, tripleTitle);
DrawText("Rapid-fires 3 consecutive chambers in continuous sequence.", barX + 810, barY + 62, 12, (cd == 0) ? RAYWHITE : GRAY);
if (cd == 0) {
DrawText("READY TO FIRE! (Requires 3 rounds in cylinder)", barX + 810, barY + 82, 12, CyberColors::NeonYellow);
} else {
DrawText(("RECHARGING [COOLDOWN: " + std::to_string(cd) + " TURNS LEFT]").c_str(), barX + 810, barY + 82, 12, Color{ 150, 155, 170, 255 });
}
} else if (combat.getPhase() == CombatPhase::SPINNING_SHOT) {
float pulse = 0.5f + 0.5f * std::sin(animTime * 10.0f);
Color spinCol = (pulse > 0.5f) ? CyberColors::NeonYellow : CyberColors::NeonPink;
DrawText(">>> ROULETTE SPINNING... DECELERATING TO TARGET CHAMBER <<<", barX + 220, barY + 45, 22, spinCol);
} else if (combat.getPhase() == CombatPhase::TRIPLE_SHOT_BURST) {
float pulse = 0.5f + 0.5f * std::sin(animTime * 14.0f);
Color burstCol = (pulse > 0.5f) ? CyberColors::NeonPink : CyberColors::NeonYellow;
DrawText(">>> FANNING THE HAMMER! 3-ROUND CYBER-BURST RAPID FIRE! <<<", barX + 210, barY + 45, 22, burstCol);
} else if (combat.getPhase() == CombatPhase::VICTORY) {
DrawText(">>> HOSTILE SENTINEL TERMINATED! PRESS [SPACE / CLICK] TO ADVANCE <<<", barX + 160, barY + 45, 22, CyberColors::NeonGreen);
} else if (combat.getPhase() == CombatPhase::DEFEAT) {
DrawText(">>> SYSTEM COLLAPSE: PLAYER DEFEATED! PRESS [SPACE / CLICK] TO REBOOT <<<", barX + 140, barY + 45, 22, RED);
} else {
DrawText("CALCULATING COMBAT RESOLUTION DATA...", barX + 360, barY + 45, 20, CyberColors::NeonCyan);
}
// Floating combat numbers
for (const auto& ft : combat.getFloatingTexts()) {
DrawText(ft.text.c_str(), static_cast<int>(ft.pos.x), static_cast<int>(ft.pos.y), 22, ft.color);
}
// 6. Overlay Codex Modal if active
if (showCodex) {
drawCodexModal();
}
}
void UIRenderer::drawLevelClearScreen(int floor) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 235 });
int cx = GetScreenWidth() / 2;
int cy = GetScreenHeight() / 2;
DrawText(">>> SECTOR SECURED <<<", cx - 180, cy - 60, 30, CyberColors::NeonGreen);
DrawText(("FLOOR " + std::to_string(floor) + " CLEARED. DATA DOWNLOAD COMPLETE.").c_str(), cx - 220, cy - 10, 18, RAYWHITE);
DrawText("PRESS [ SPACE ] TO DESCEND TO NEXT DEEPER CYBERSPACE LAYER", cx - 290, cy + 40, 18, CyberColors::NeonYellow);
}
void UIRenderer::drawGameOverScreen() {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 6, 12, 245 });
int cx = GetScreenWidth() / 2;
int cy = GetScreenHeight() / 2;
DrawText(">>> CONNECTION TERMINATED <<<", cx - 240, cy - 60, 32, RED);
DrawText("FLATLINE: NEURAL INTERFACE DISCONNECTED", cx - 210, cy - 10, 18, GRAY);
DrawText("PRESS [ SPACE / ENTER ] TO RECONNECT AND START NEW RUN", cx - 260, cy + 50, 18, CyberColors::NeonCyan);
}
void UIRenderer::drawHubHUD(const Player& player, const std::string& prompt) {
int hudX = 20;
int hudY = 20;
DrawRectangle(hudX, hudY, 320, 80, Color{ 16, 22, 30, 220 });
DrawRectangleLines(hudX, hudY, 320, 80, Color{ 180, 150, 100, 255 });
DrawText("PHYSICAL REALITY // ROOM 404", hudX + 15, hudY + 12, 16, Color{ 240, 210, 160, 255 });
DrawText("NEURAL DECK: ONLINE & READY", hudX + 15, hudY + 38, 14, CyberColors::NeonGreen);
DrawText("STATUS: SAFE APARTMENT", hudX + 15, hudY + 56, 12, GRAY);
if (!prompt.empty()) {
int promptW = MeasureText(prompt.c_str(), 18) + 40;
int promptX = (GetScreenWidth() - promptW) / 2;
int promptY = GetScreenHeight() - 70;
DrawRectangle(promptX, promptY, promptW, 45, Color{ 18, 25, 36, 240 });
DrawRectangleLines(promptX, promptY, promptW, 45, CyberColors::NeonGreen);
DrawText(prompt.c_str(), promptX + 20, promptY + 13, 18, CyberColors::NeonGreen);
}
DrawText("[W/S] WALK | [A/D] TURN 90 | [SPACE/E] INTERACT | [TAB] ARMORY | [ESC] PAUSE MENU",
20, GetScreenHeight() - 35, 13, Color{ 180, 190, 200, 220 });
}
void UIRenderer::drawRestRoomHUD(int nextFloor, const Player& player, const std::string& prompt) {
int hudX = 20;
int hudY = 20;
DrawRectangle(hudX, hudY, 350, 142, Color{ 14, 18, 28, 230 });
DrawRectangleLines(hudX, hudY, 350, 142, Color{ 210, 170, 70, 255 });
DrawText("PALACE STOREROOM // SAFE REFUGE", hudX + 15, hudY + 10, 15, Color{ 240, 210, 140, 255 });
DrawText(("NEXT DEPTH: LAYER 0" + std::to_string(nextFloor)).c_str(), hudX + 15, hudY + 30, 13, CyberColors::NeonYellow);
// HP Bar
float hpRatio = static_cast<float>(player.getHp()) / player.getMaxHp();
drawProgressBar(hudX + 55, hudY + 52, 270, 16, hpRatio, CyberColors::NeonGreen, Color{ 40, 15, 15, 255 });
DrawText("HP", hudX + 15, hudY + 52, 13, CyberColors::NeonGreen);
DrawText((std::to_string(player.getHp()) + "/" + std::to_string(player.getMaxHp())).c_str(), hudX + 155, hudY + 53, 12, RAYWHITE);
// Shield Bar
float shRatio = static_cast<float>(player.getShield()) / player.getMaxShield();
drawProgressBar(hudX + 55, hudY + 74, 270, 16, shRatio, CyberColors::NeonCyan, Color{ 10, 25, 45, 255 });
DrawText("SHLD", hudX + 15, hudY + 74, 13, CyberColors::NeonCyan);
DrawText((std::to_string(player.getShield()) + "/" + std::to_string(player.getMaxShield())).c_str(), hudX + 155, hudY + 75, 12, RAYWHITE);
DrawText(("PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 98, 13, CyberColors::NeonYellow);
DrawText(("CORE CHIPS: " + std::to_string(player.getTechScraps())).c_str(), hudX + 120, hudY + 98, 13, CyberColors::NeonCyan);
DrawText("STATUS: PALACE SANCTUARY", hudX + 15, hudY + 118, 12, CyberColors::NeonGreen);
// Context Action Prompt
if (!prompt.empty()) {
int promptW = MeasureText(prompt.c_str(), 18) + 40;
int promptX = (GetScreenWidth() - promptW) / 2;
int promptY = GetScreenHeight() - 70;
DrawRectangle(promptX, promptY, promptW, 45, Color{ 14, 20, 32, 240 });
DrawRectangleLines(promptX, promptY, promptW, 45, Color{ 220, 180, 80, 255 });
DrawText(prompt.c_str(), promptX + 20, promptY + 13, 18, Color{ 255, 220, 100, 255 });
}
DrawText("[W/S] WALK | [A/D] TURN 90 | [SPACE/E] INTERACT | [TAB] ARMORY | [ESC] PAUSE MENU",
20, GetScreenHeight() - 35, 13, Color{ 180, 190, 200, 220 });
}
Rectangle UIRenderer::getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH) {
int modalW = 1020;
int modalH = 600;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
int col = ammoIndex % 3;
int row = ammoIndex / 3;
int cardW = 310;
int cardH = 220;
int startX = modalX + 30;
int startY = modalY + 68;
int gapX = 15;
int gapY = 16;
int cardX = startX + col * (cardW + gapX);
int cardY = startY + row * (cardH + gapY);
return Rectangle{ static_cast<float>(cardX + 12), static_cast<float>(cardY + cardH - 42), static_cast<float>(cardW - 24), 32.0f };
}
Rectangle UIRenderer::getWorkbenchCloseButtonRect(int screenW, int screenH) {
int modalW = 1020;
int modalH = 600;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
return Rectangle{ static_cast<float>(modalX + modalW - 140), static_cast<float>(modalY + modalH - 42), 115.0f, 32.0f };
}
void UIRenderer::drawWorkbenchModal(const Player& player) {
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
DrawRectangle(0, 0, screenW, screenH, Color{ 4, 8, 16, 235 });
int modalW = 1020;
int modalH = 600;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
// Outer background and classical bronze border
DrawRectangle(modalX, modalY, modalW, modalH, Color{ 10, 16, 26, 255 });
DrawRectangleLines(modalX, modalY, modalW, modalH, Color{ 200, 160, 60, 255 });
// Decorative corners
DrawRectangle(modalX - 2, modalY - 2, 8, 8, Color{ 220, 180, 80, 255 });
DrawRectangle(modalX + modalW - 6, modalY - 2, 8, 8, Color{ 220, 180, 80, 255 });
DrawRectangle(modalX - 2, modalY + modalH - 6, 8, 8, Color{ 220, 180, 80, 255 });
DrawRectangle(modalX + modalW - 6, modalY + modalH - 6, 8, 8, Color{ 220, 180, 80, 255 });
// Header Banner
DrawRectangle(modalX, modalY, modalW, 52, Color{ 18, 24, 38, 255 });
DrawRectangleLines(modalX, modalY, modalW, 52, Color{ 140, 110, 50, 255 });
DrawText("// CRAFTSMAN WORKBENCH // RUN CARTRIDGE FORGE & OVERCLOCK", modalX + 25, modalY + 16, 17, Color{ 255, 215, 80, 255 });
// Live Core Chips Balance badge in header
std::string scrapBadge = "[ ❖ CORE CHIPS: " + std::to_string(player.getTechScraps()) + " ]";
int badgeW = MeasureText(scrapBadge.c_str(), 16);
DrawRectangle(modalX + modalW - badgeW - 40, modalY + 12, badgeW + 20, 28, Color{ 8, 28, 38, 255 });
DrawRectangleLines(modalX + modalW - badgeW - 40, modalY + 12, badgeW + 20, 28, CyberColors::NeonCyan);
DrawText(scrapBadge.c_str(), modalX + modalW - badgeW - 30, modalY + 18, 16, CyberColors::NeonCyan);
Vector2 mouse = GetMousePosition();
AmmoType ammos[6] = {
AmmoType::KINETIC,
AmmoType::EMP,
AmmoType::OVERHEAT,
AmmoType::CORROSIVE,
AmmoType::GLITCH,
AmmoType::NANITE
};
int startX = modalX + 30;
int startY = modalY + 68;
int cardW = 310;
int cardH = 220;
int gapX = 15;
int gapY = 16;
for (int i = 0; i < 6; ++i) {
AmmoType ammo = ammos[i];
const auto& info = getAmmoInfo(ammo);
int curTier = player.getAmmoTier(ammo);
const auto& curTierData = getAmmoTierData(ammo, curTier);
int col = i % 3;
int row = i / 3;
int cardX = startX + col * (cardW + gapX);
int cardY = startY + row * (cardH + gapY);
Rectangle btnRect = getWorkbenchUpgradeButtonRect(i, screenW, screenH);
bool isHoverBtn = CheckCollisionPointRec(mouse, btnRect);
// Card container
DrawRectangle(cardX, cardY, cardW, cardH, Color{ 14, 20, 32, 255 });
DrawRectangleLines(cardX, cardY, cardW, cardH, isHoverBtn ? CyberColors::NeonYellow : info.color);
// Header inside card: Ammo icon & Name
DrawCircle(cardX + 22, cardY + 24, 10.0f, info.color);
DrawCircleLines(cardX + 22, cardY + 24, 10.0f, WHITE);
std::string nameTxt = info.name + " [" + info.shortName + "]";
DrawText(nameTxt.c_str(), cardX + 40, cardY + 15, 13, info.color);
// Tier badge
DrawText(curTierData.tierBadge.c_str(), cardX + cardW - 75, cardY + 15, 13, Color{ 255, 215, 60, 255 });
// Tier Title (e.g. TIER I: BASE SPEC)
DrawText(curTierData.tierTitle.c_str(), cardX + 16, cardY + 44, 12, Color{ 220, 190, 120, 255 });
// Damage display
int effDmg = player.getEffectiveDamage(ammo);
std::string dmgStr = "EFFECTIVE DAMAGE: " + std::to_string(effDmg);
if (curTier < 3) {
int nextDmg = getAmmoTierData(ammo, curTier + 1).damage;
dmgStr += " -> " + std::to_string(nextDmg);
}
DrawText(dmgStr.c_str(), cardX + 16, cardY + 64, 12, RAYWHITE);
// Current Tier Perk & Effects
DrawRectangle(cardX + 12, cardY + 84, cardW - 24, 84, Color{ 10, 14, 24, 255 });
DrawRectangleLines(cardX + 12, cardY + 84, cardW - 24, 84, Color{ 35, 45, 65, 255 });
// Format perk text wrapping
DrawText("PERK:", cardX + 18, cardY + 90, 10, GRAY);
std::string perk = curTierData.perkDesc;
size_t mid1 = perk.find(". ");
if (mid1 != std::string::npos && mid1 < perk.length() - 2) {
std::string line1 = perk.substr(0, mid1 + 1);
std::string line2 = perk.substr(mid1 + 2);
DrawText(line1.c_str(), cardX + 18, cardY + 104, 11, CyberColors::NeonCyan);
DrawText(line2.c_str(), cardX + 18, cardY + 122, 11, RAYWHITE);
} else {
DrawText(perk.c_str(), cardX + 18, cardY + 106, 11, CyberColors::NeonCyan);
}
// Bottom Action Button / Status
if (curTier >= 3) {
DrawRectangleRec(btnRect, Color{ 40, 34, 14, 255 });
DrawRectangleLinesEx(btnRect, 1.0f, Color{ 220, 180, 50, 255 });
DrawText("[ ★ MASTER SPEC REACHED ★ ]", cardX + 32, static_cast<int>(btnRect.y + 9), 12, Color{ 255, 215, 60, 255 });
} else {
int nextTier = curTier + 1;
const auto& nextData = getAmmoTierData(ammo, nextTier);
int cost = nextData.upgradeCost;
bool canAfford = player.canUpgradeAmmo(ammo);
Color btnBg = canAfford ? (isHoverBtn ? Color{ 25, 65, 45, 255 } : Color{ 16, 42, 30, 255 })
: Color{ 30, 18, 22, 255 };
Color btnLine = canAfford ? (isHoverBtn ? CyberColors::NeonYellow : CyberColors::NeonGreen)
: Color{ 120, 50, 60, 255 };
Color btnText = canAfford ? (isHoverBtn ? CyberColors::NeonYellow : CyberColors::NeonGreen)
: Color{ 190, 110, 120, 255 };
DrawRectangleRec(btnRect, btnBg);
DrawRectangleLinesEx(btnRect, isHoverBtn ? 2.0f : 1.0f, btnLine);
std::string btnLabel = "[" + std::to_string(i + 1) + "] UPGRADE TO TIER " + (nextTier == 2 ? "II" : "III") + " (" + std::to_string(cost) + " ❖)";
if (!canAfford) {
btnLabel = "[" + std::to_string(i + 1) + "] NEED " + std::to_string(cost) + " ❖ (LACK " + std::to_string(cost - player.getTechScraps()) + ")";
}
int labelW = MeasureText(btnLabel.c_str(), 12);
DrawText(btnLabel.c_str(), cardX + (cardW - labelW) / 2, static_cast<int>(btnRect.y + 9), 12, btnText);
}
}
// Footer Info & Close Button
DrawText("[1 - 6] OR CLICK BUTTON TO UPGRADE CARTRIDGE | [ESC / E] CLOSE WORKBENCH",
modalX + 30, modalY + modalH - 32, 13, RAYWHITE);
Rectangle btnClose = getWorkbenchCloseButtonRect(screenW, screenH);
bool isHoverClose = CheckCollisionPointRec(mouse, btnClose);
DrawRectangleRec(btnClose, isHoverClose ? Color{ 45, 25, 30, 255 } : Color{ 24, 16, 20, 255 });
DrawRectangleLinesEx(btnClose, isHoverClose ? 2.0f : 1.0f, isHoverClose ? RED : Color{ 180, 80, 90, 255 });
DrawText("[ESC] CLOSE", static_cast<int>(btnClose.x + 18), static_cast<int>(btnClose.y + 9), 13, isHoverClose ? RED : RAYWHITE);
}
void UIRenderer::drawPauseMenu(bool hasSavedMessage) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 225 });
int menuW = 480;
int menuH = 430;
int menuX = (GetScreenWidth() - menuW) / 2;
int menuY = (GetScreenHeight() - menuH) / 2;
DrawRectangle(menuX, menuY, menuW, menuH, Color{ 10, 16, 28, 255 });
DrawRectangleLines(menuX, menuY, menuW, menuH, CyberColors::NeonCyan);
DrawRectangle(menuX, menuY, menuW, 50, Color{ 16, 26, 44, 255 });
DrawText("// PROTOCOL PAUSED", menuX + 25, menuY + 16, 20, CyberColors::NeonCyan);
if (hasSavedMessage) {
DrawText(">>> RUN SAVED TO DISK (savegame.dat) <<<", menuX + 45, menuY + 62, 14, CyberColors::NeonGreen);
} else {
DrawText("SESSION SUSPENDED // SELECT ACTION", menuX + 60, menuY + 62, 13, GRAY);
}
Vector2 mouse = GetMousePosition();
const char* labels[] = {
"[1 / ESC] RESUME SESSION",
"[2] AUDIO & MUSIC SETTINGS",
"[3] SAVE PROGRESS TO DISK",
"[4] RETURN TO ROOM HUB",
"[5] EXIT TO DESKTOP"
};
int startY = menuY + 90;
for (int i = 0; i < 5; ++i) {
Rectangle btnRect = { static_cast<float>(menuX + 30), static_cast<float>(startY + i * 58), static_cast<float>(menuW - 60), 48.0f };
bool isHover = CheckCollisionPointRec(mouse, btnRect);
DrawRectangleRec(btnRect, isHover ? Color{ 30, 55, 90, 255 } : Color{ 16, 25, 40, 255 });
DrawRectangleLinesEx(btnRect, isHover ? 2.0f : 1.0f, isHover ? CyberColors::NeonGreen : (i == 4 ? RED : CyberColors::NeonCyan));
DrawText(labels[i], menuX + 50, startY + i * 58 + 15, 16, isHover ? CyberColors::NeonYellow : (i == 4 ? Color{ 255, 100, 100, 255 } : RAYWHITE));
}
}
void UIRenderer::drawMainMenu(bool canContinue) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 255 });
drawMatrixDigitalRain(static_cast<float>(GetTime()));
int cx = GetScreenWidth() / 2;
int cy = GetScreenHeight() / 2;
DrawText("DOORNROLL", cx - 180, cy - 200, 54, CyberColors::NeonCyan);
DrawText("// CYBERSPACE PROTOCOL & PHYSICAL REALITY", cx - 210, cy - 135, 16, CyberColors::NeonGreen);
Vector2 mouse = GetMousePosition();
Rectangle btnContinue = { static_cast<float>(cx - 190), static_cast<float>(cy - 50), 380.0f, 48.0f };
Rectangle btnNewRun = { static_cast<float>(cx - 190), static_cast<float>(cy + 10), 380.0f, 48.0f };
Rectangle btnSettings = { static_cast<float>(cx - 190), static_cast<float>(cy + 70), 380.0f, 48.0f };
Rectangle btnExit = { static_cast<float>(cx - 190), static_cast<float>(cy + 130), 380.0f, 48.0f };
bool hoverCont = CheckCollisionPointRec(mouse, btnContinue) && canContinue;
bool hoverNew = CheckCollisionPointRec(mouse, btnNewRun);
bool hoverSet = CheckCollisionPointRec(mouse, btnSettings);
bool hoverExit = CheckCollisionPointRec(mouse, btnExit);
if (canContinue) {
DrawRectangleRec(btnContinue, hoverCont ? Color{ 30, 60, 95, 255 } : Color{ 16, 28, 48, 255 });
DrawRectangleLinesEx(btnContinue, hoverCont ? 2.0f : 1.0f, hoverCont ? CyberColors::NeonGreen : CyberColors::NeonCyan);
DrawText("[1] CONTINUE SAVED RUN", cx - 130, cy - 35, 18, hoverCont ? CyberColors::NeonYellow : RAYWHITE);
} else {
DrawRectangleRec(btnContinue, Color{ 12, 18, 28, 255 });
DrawRectangleLinesEx(btnContinue, 1.0f, DARKGRAY);
DrawText("CONTINUE (NO SAVE DATA)", cx - 130, cy - 35, 16, DARKGRAY);
}
DrawRectangleRec(btnNewRun, hoverNew ? Color{ 30, 60, 95, 255 } : Color{ 16, 28, 48, 255 });
DrawRectangleLinesEx(btnNewRun, hoverNew ? 2.0f : 1.0f, hoverNew ? CyberColors::NeonGreen : CyberColors::NeonCyan);
DrawText("[2] NEW RUN // REALITY ROOM", cx - 145, cy + 25, 18, hoverNew ? CyberColors::NeonYellow : RAYWHITE);
DrawRectangleRec(btnSettings, hoverSet ? Color{ 30, 60, 95, 255 } : Color{ 16, 28, 48, 255 });
DrawRectangleLinesEx(btnSettings, hoverSet ? 2.0f : 1.0f, hoverSet ? CyberColors::NeonGreen : CyberColors::NeonCyan);
DrawText("[3] AUDIO & MUSIC SETTINGS", cx - 145, cy + 85, 18, hoverSet ? CyberColors::NeonYellow : RAYWHITE);
DrawRectangleRec(btnExit, hoverExit ? Color{ 50, 20, 30, 255 } : Color{ 20, 14, 22, 255 });
DrawRectangleLinesEx(btnExit, hoverExit ? 2.0f : 1.0f, hoverExit ? RED : Color{ 120, 40, 50, 255 });
DrawText("[4] QUIT TO DESKTOP", cx - 105, cy + 145, 18, hoverExit ? RED : GRAY);
drawMatrixOverlay(static_cast<float>(GetTime()));
}
void UIRenderer::drawSettingsMenu(float masterVol, float musicVol, float sfxVol, int selectedSlider) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 230 });
int menuW = 580;
int menuH = 430;
int menuX = (GetScreenWidth() - menuW) / 2;
int menuY = (GetScreenHeight() - menuH) / 2;
DrawRectangle(menuX, menuY, menuW, menuH, Color{ 10, 16, 28, 255 });
DrawRectangleLines(menuX, menuY, menuW, menuH, CyberColors::NeonCyan);
// Title Bar
DrawRectangle(menuX, menuY, menuW, 52, Color{ 16, 26, 44, 255 });
DrawText("// 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 sliderW = 340;
int sliderH = 26;
int sliderX = menuX + 180;
for (int i = 0; i < 3; ++i) {
int itemY = startY + i * 78;
bool isSel = (i == selectedSlider);
Rectangle itemBox = { static_cast<float>(menuX + 20), static_cast<float>(itemY - 10), static_cast<float>(menuW - 40), 65.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(titles[i], menuX + 30, itemY + 4, 15, 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 });
DrawRectangleLinesEx(sBar, 1.0f, isSel ? CyberColors::NeonGreen : Color{ 50, 75, 110, 255 });
int fillW = static_cast<int>(vols[i] * sliderW);
Color fillCol = (i == 0) ? CyberColors::NeonCyan : ((i == 1) ? CyberColors::NeonPurple : CyberColors::NeonGreen);
DrawRectangle(sliderX + 2, itemY + 2, fillW - 4 > 0 ? fillW - 4 : 0, sliderH - 4, fillCol);
int knobX = sliderX + fillW;
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);
}
Rectangle btnBack = { static_cast<float>(menuX + (menuW - 240) / 2), static_cast<float>(menuY + menuH - 65), 240.0f, 44.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("[W/S] SELECT SLIDER | [A/D] ADJUST +/-5% | CLICK & DRAG SLIDERS",
menuX + 40, menuY + menuH - 18, 12, GRAY);
}
} // namespace dnr