#include "UIRenderer.hpp" #include #include 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(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(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(x * cellSize); int py = startY + static_cast(y * cellSize); int cs = static_cast(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((player.getGridX() + 0.5f) * cellSize); int plY = startY + static_cast((player.getGridY() + 0.5f) * cellSize); DrawCircle(plX, plY, cellSize * 0.7f, CyberColors::NeonYellow); Vector2 dirVec = getDirectionOffset(player.getFacing()); DrawLine(plX, plY, plX + static_cast(dirVec.x * cellSize * 1.5f), plY + static_cast(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(GetScreenHeight() + 100)) - 50.0f; int xPos = c * colWidth + 10; const char* txt = glyphs[(c + static_cast(animTime * 2.0f)) % 12]; DrawText(txt, xPos, static_cast(yPos), 12, Color{ 0, 255, 100, 35 }); DrawText(txt, xPos, static_cast(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(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(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((1.0f - pin.targetHeight) * trackH); int sweetSpan = static_cast(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((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(dialCenterX), static_cast(dialCenterY) }, dialRadius, sc.getSuccessStart() - 90.0f, sc.getSuccessEnd() - 90.0f, 32, RAYWHITE); DrawCircleSector({ static_cast(dialCenterX), static_cast(dialCenterY) }, dialRadius, sc.getSuccessStart() - 90.0f, sc.getSuccessEnd() - 90.0f, 32, Color{ 220, 220, 220, 120 }); // Great success zone DrawCircleSector({ static_cast(dialCenterX), static_cast(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(std::cos(needleRad) * (dialRadius + 8.0f)); int needleEndY = dialCenterY + static_cast(std::sin(needleRad) * (dialRadius + 8.0f)); DrawLineEx({ static_cast(dialCenterX), static_cast(dialCenterY) }, { static_cast(needleEndX), static_cast(needleEndY) }, 4.0f, RED); DrawCircle(dialCenterX, dialCenterY, 14, RED); DrawText("HIT [ SPACE ]", dialCenterX - 60, dialCenterY + static_cast(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(std::cos(fluteAngle) * (cylinderR + 2.0f)); int fluteY = centerY + static_cast(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(cylinderR) - 4; int strikerBaseY = centerY - static_cast(cylinderR) - 26; DrawTriangle( { static_cast(centerX), static_cast(strikerTipY) }, { static_cast(centerX - 12), static_cast(strikerBaseY) }, { static_cast(centerX + 12), static_cast(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(std::cos(angleRad) * chamberOrbitR); int chY = centerY + static_cast(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(reticleR) - 3, chY, chX - static_cast(reticleR) + 3, chY, CyberColors::NeonYellow); DrawLine(chX + static_cast(reticleR) - 3, chY, chX + static_cast(reticleR) + 3, chY, CyberColors::NeonYellow); DrawLine(chX, chY - static_cast(reticleR) - 3, chX, chY - static_cast(reticleR) + 3, CyberColors::NeonYellow); DrawLine(chX, chY + static_cast(reticleR) - 3, chX, chY + static_cast(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(modalX + modalW / 2 - 120), static_cast(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(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(eHpRatio * 100)) + "%)"; DrawText(hpTxt.c_str(), scanX + scanW / 2 - 40, scanY + 56, 13, WHITE); // Shield Segmented Bar float eShRatio = (enemy->getMaxShield() > 0) ? (static_cast(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(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(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(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(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(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(logX + logW - 95), static_cast(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(barX + 15), static_cast(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(barX + 405), static_cast(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(barX + 795), static_cast(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(ft.pos.x), static_cast(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(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(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(cardX + 12), static_cast(cardY + cardH - 42), static_cast(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(modalX + modalW - 140), static_cast(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(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(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(btnClose.x + 18), static_cast(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(menuX + 30), static_cast(startY + i * 58), static_cast(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(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(cx - 190), static_cast(cy - 50), 380.0f, 48.0f }; Rectangle btnNewRun = { static_cast(cx - 190), static_cast(cy + 10), 380.0f, 48.0f }; Rectangle btnSettings = { static_cast(cx - 190), static_cast(cy + 70), 380.0f, 48.0f }; Rectangle btnExit = { static_cast(cx - 190), static_cast(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(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(menuX + 20), static_cast(itemY - 10), static_cast(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(sliderX), static_cast(itemY), static_cast(sliderW), static_cast(sliderH) }; DrawRectangleRec(sBar, Color{ 14, 20, 32, 255 }); DrawRectangleLinesEx(sBar, 1.0f, isSel ? CyberColors::NeonGreen : Color{ 50, 75, 110, 255 }); int fillW = static_cast(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(std::round(vols[i] * 100.0f))) + "%"; DrawText(pctStr.c_str(), sliderX + sliderW + 15, itemY + 4, 16, isSel ? CyberColors::NeonYellow : RAYWHITE); } Rectangle btnBack = { static_cast(menuX + (menuW - 240) / 2), static_cast(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(btnBack.x) + 18, static_cast(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