feat: Cyber-noir hub room lighting, pixel wave transitions, tactical UI & ammo overhaul

- Replace procedural city with Room_window.jpg panoramic backdrop
- Add volumetric violet godrays, floor ambient pools, and cyan monitor glow
- Fix bed occlusion bug: reorder renderHub pipeline with rlDrawRenderBatchActive()
- Darken hub materials (smoked walnut parquet, slate plaster, midnight quilt)
- Enable cyber-noir postprocessing shader with vignette in hub room
- Implement diagonal matrix pixel wave screen transitions (ScreenTransition.hpp)
- Add tactical ammo palette (KIN, EMP, THM, ACD, GLT, NAN) & 3D combat laser tracers
- Center [ 12:00 FIRING PIN ] indicator and add drawWrappedText word-wrapping in UI
- Maintain strict zero-rounding constraint across the entire codebase
This commit is contained in:
Игорь Федосеев 2026-10-06 21:15:02 +03:00
parent 1e3b05d5c8
commit 4cad719290
14 changed files with 1230 additions and 770 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View file

@ -19,17 +19,17 @@ void main()
{
vec2 uv = fragTexCoord;
// 1. Digital Glitch / Chromatic Aberration
// 1. Digital Glitch / Tactical Chromatic Aberration
float glitch = clamp(u_glitchIntensity, 0.0, 1.0);
if (glitch > 0.001)
{
// Horizontal line slice displacement
float slice = step(0.65, sin(uv.y * 35.0 + u_time * 25.0));
float sliceOffset = (slice * 2.0 - 1.0) * 0.018 * glitch;
float slice = step(0.68, sin(uv.y * 42.0 + u_time * 30.0));
float sliceOffset = (slice * 2.0 - 1.0) * 0.015 * glitch;
uv.x += sliceOffset;
// Chromatic RGB separation
float split = 0.012 * glitch;
float split = 0.010 * glitch;
float r = texture(texture0, vec2(uv.x + split, uv.y)).r;
float g = texture(texture0, uv).g;
float b = texture(texture0, vec2(uv.x - split, uv.y)).b;
@ -40,43 +40,34 @@ void main()
finalColor = texture(texture0, uv);
}
// 2. Neon Bloom / Emissive Halo
vec2 texel = 1.0 / u_resolution;
vec3 bloom = vec3(0.0);
float bloomWeight = 0.0;
// 9-tap bright-pass sample
vec2 offsets[8] = vec2[](
vec2(-2.0, 0.0), vec2( 2.0, 0.0),
vec2( 0.0, -2.0), vec2( 0.0, 2.0),
vec2(-1.5, -1.5), vec2( 1.5, -1.5),
vec2(-1.5, 1.5), vec2( 1.5, 1.5)
);
for (int i = 0; i < 8; ++i)
// 2. Tactical Color Grading (High Contrast Monochrome + Vivid Crimson Accent)
// Desaturate non-red tones toward tactical gunmetal/monochrome
float luma = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
// Check if the color is an intentional tactical accent (Crimson, Cobalt EMP, Amber, Acid, Violet)
float maxC = max(finalColor.r, max(finalColor.g, finalColor.b));
float minC = min(finalColor.r, min(finalColor.g, finalColor.b));
float sat = (maxC > 0.05) ? ((maxC - minC) / maxC) : 0.0;
bool isTacticalAccent = (sat > 0.36) || (finalColor.r > 0.35 && finalColor.r > finalColor.g * 1.3);
if (!isTacticalAccent)
{
vec3 col = texture(texture0, uv + offsets[i] * texel * 1.8).rgb;
float brightness = dot(col, vec3(0.2126, 0.7152, 0.0722));
if (brightness > 0.55)
{
bloom += (col - 0.45) * 1.4;
bloomWeight += 1.0;
}
// Pull low-saturation environmental surfaces toward high-contrast monochrome steel
vec3 mono = vec3(luma);
finalColor.rgb = mix(finalColor.rgb, mono, 0.80);
}
if (bloomWeight > 0.0)
{
bloom /= 8.0;
finalColor.rgb += bloom * 0.75;
}
// Enhance contrast (crush deep blacks, crispen bright highlights)
finalColor.rgb = (finalColor.rgb - 0.5) * 1.18 + 0.5;
finalColor.rgb = clamp(finalColor.rgb, 0.0, 1.0);
// 3. Vignette (Smooth cinematic edge darkening)
// 3. Cinematic Corner Vignette (Tactical Helmet Visor Shadow)
float d = distance(uv, vec2(0.5, 0.5));
float vignette = smoothstep(0.82, 0.38, d);
finalColor.rgb *= mix(0.58, 1.0, vignette);
float vignette = smoothstep(0.78, 0.32, d);
finalColor.rgb *= mix(0.40, 1.0, vignette);
// 4. Subtle Cyber Scanlines
float scanline = sin(uv.y * u_resolution.y * 1.8) * 0.035;
// 4. Subtle Tactical CRT Scanlines
float scanline = sin(uv.y * u_resolution.y * 1.5) * 0.025;
finalColor.rgb -= scanline;
finalColor.a = 1.0;

View file

@ -14,6 +14,7 @@ void CombatSystem::startCombat(Player& player, std::unique_ptr<Enemy> enemy) {
m_ghostBonusDmg = false;
m_defeatedEnemyWasElite = false;
m_tripleCooldown = 0;
m_lastFiredAmmo = AmmoType::EMPTY;
m_combatLog.clear();
m_floatingTexts.clear();
@ -249,6 +250,7 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
void CombatSystem::resolveBurstShot(Player& player, int shotNum) {
AmmoType chamberAmmo = player.getRevolver().fire();
m_lastFiredAmmo = chamberAmmo;
const auto& info = getAmmoInfo(chamberAmmo);
float offsetX = (shotNum - 2) * 45.0f;
@ -291,6 +293,7 @@ void CombatSystem::resolveBurstShot(Player& player, int shotNum) {
void CombatSystem::resolveShotImpact(Player& player) {
AmmoType chamberAmmo = player.getRevolver().fire();
m_lastFiredAmmo = chamberAmmo;
const auto& info = getAmmoInfo(chamberAmmo);
if (chamberAmmo == AmmoType::EMPTY) {

View file

@ -56,6 +56,9 @@ public:
void addLog(const std::string& msg);
void addFloatingText(const std::string& text, Vector2 pos, Color color);
AmmoType getLastFiredAmmo() const { return m_lastFiredAmmo; }
float getPhaseTimer() const { return m_phaseTimer; }
private:
void resolveShotImpact(Player& player);
void resolveBurstShot(Player& player, int shotNum);
@ -74,6 +77,7 @@ private:
bool m_shieldBoosted = false;
bool m_ghostBonusDmg = false;
bool m_defeatedEnemyWasElite = false;
AmmoType m_lastFiredAmmo = AmmoType::EMPTY;
std::vector<std::string> m_combatLog;
std::vector<FloatingText> m_floatingTexts;

View file

@ -12,7 +12,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 12 + level * 3;
m_attackElement = AmmoType::OVERHEAT;
m_weakness = AmmoType::EMP;
m_color = CyberColors::NeonOrange;
m_color = TacticalColors::PureWhite;
break;
case EnemyType::ICE_SENTINEL:
@ -22,7 +22,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 14 + level * 3;
m_attackElement = AmmoType::EMP;
m_weakness = AmmoType::CORROSIVE;
m_color = CyberColors::NeonCyan;
m_color = TacticalColors::Crimson;
break;
case EnemyType::GLITCH_WRAITH:
@ -32,7 +32,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 18 + level * 4;
m_attackElement = AmmoType::GLITCH;
m_weakness = AmmoType::KINETIC;
m_color = CyberColors::NeonPurple;
m_color = TacticalColors::BrightRed;
break;
case EnemyType::VIRUS_CONSTRUCT:
@ -42,7 +42,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 15 + level * 3;
m_attackElement = AmmoType::CORROSIVE;
m_weakness = AmmoType::OVERHEAT;
m_color = CyberColors::NeonGreen;
m_color = TacticalColors::DarkRed;
break;
}

View file

@ -44,13 +44,13 @@ struct AmmoInfo {
};
inline const AmmoInfo& getAmmoInfo(AmmoType type) {
static const AmmoInfo infoEmpty { AmmoType::EMPTY, "Empty Shell", "BLANK", "Empty chamber - no round.", DARKGRAY, 0, 0.0f };
static const AmmoInfo infoKinetic { AmmoType::KINETIC, "Kinetic Pierce", "KINET", "Heavy kinetic sabot. High raw impact.", { 240, 240, 240, 255 }, 35, 0.25f };
static const AmmoInfo infoEMP { AmmoType::EMP, "EMP Shock", "EMP", "Electro-magnetic pulse. Destroys shields, stuns machine.", { 0, 220, 255, 255 }, 20, 0.70f };
static const AmmoInfo infoOverheat { AmmoType::OVERHEAT, "Overheat Core", "THERM", "Thermal incendiary round. Ignites target CPU for 3 turns.", { 255, 80, 20, 255 }, 18, 1.0f };
static const AmmoInfo infoCorrosive { AmmoType::CORROSIVE, "Acid Corrosive", "ACID", "Sub-routine dissolver. Destroys armor, targets take +50% dmg.", { 120, 255, 0, 255 }, 15, 1.0f };
static const AmmoInfo infoGlitch { AmmoType::GLITCH, "Glitch Cryo", "GLITCH", "Corrupting code payload. Drops enemy accuracy drastically.", { 220, 50, 255, 255 }, 22, 0.80f };
static const AmmoInfo infoNanite { AmmoType::NANITE, "Nanite Siphon", "NANITE", "Vampiric micro-drones. Deals damage & restores Player HP/Shield.", { 0, 255, 180, 255 }, 16, 1.0f };
static const AmmoInfo infoEmpty { AmmoType::EMPTY, "Empty Shell", "---", "Empty chamber - no round.", { 50, 60, 75, 255 }, 0, 0.0f };
static const AmmoInfo infoKinetic { AmmoType::KINETIC, "Kinetic Pierce", "KIN", "Heavy kinetic sabot. High raw impact.", { 240, 245, 255, 255 }, 35, 0.25f };
static const AmmoInfo infoEMP { AmmoType::EMP, "EMP Shock", "EMP", "Electro-magnetic pulse. Destroys shields, stuns machine.", { 70, 150, 255, 255 }, 20, 0.70f };
static const AmmoInfo infoOverheat { AmmoType::OVERHEAT, "Overheat Core", "THM", "Thermal incendiary round. Ignites target CPU for 3 turns.", { 245, 130, 32, 255 }, 18, 1.0f };
static const AmmoInfo infoCorrosive { AmmoType::CORROSIVE, "Acid Corrosive", "ACD", "Sub-routine dissolver. Destroys armor, targets take +50% dmg.", { 95, 185, 75, 255 }, 15, 1.0f };
static const AmmoInfo infoGlitch { AmmoType::GLITCH, "Glitch Cryo", "GLT", "Corrupting code payload. Drops enemy accuracy drastically.", { 175, 95, 245, 255 }, 22, 0.80f };
static const AmmoInfo infoNanite { AmmoType::NANITE, "Nanite Siphon", "NAN", "Vampiric micro-drones. Deals damage & restores Player HP/Shield.", { 225, 25, 55, 255 }, 16, 1.0f };
switch (type) {
case AmmoType::KINETIC: return infoKinetic;
@ -246,58 +246,94 @@ struct ImplantData {
std::string code;
};
// Tactical Military / Arasaka Brutalism color palette (Monochrome + Crimson Red)
namespace TacticalColors {
// Backgrounds & Base Panels
inline const Color PitchBlack = { 6, 8, 12, 255 }; // Deepest background
inline const Color DarkPanel = { 12, 16, 22, 240 }; // Modal & panel backgrounds
inline const Color SurfacePlate = { 20, 26, 36, 255 }; // Button & card backgrounds
inline const Color SteelMuted = { 45, 55, 70, 255 }; // Dividers, disabled frames
// Monochrome Typography & Contours
inline const Color PureWhite = { 255, 255, 255, 255 }; // High-contrast headers, active labels
inline const Color SilverText = { 210, 218, 230, 255 }; // Normal body text
inline const Color MutedText = { 120, 132, 148, 255 }; // Subtitles, inactive hints
inline const Color BorderMuted = { 60, 72, 90, 255 }; // Unfocused border
inline const Color BorderWhite = { 220, 230, 245, 255 }; // Focused / hover border
// Crimson & Tactical Red Accents
inline const Color Crimson = { 230, 25, 45, 255 }; // Primary red accent, reticles, striker
inline const Color BrightRed = { 255, 60, 75, 255 }; // Critical hit, warnings, alerts
inline const Color DarkRed = { 70, 15, 22, 255 }; // Warning backing, alarm panels
inline const Color CrimsonGlow = { 230, 25, 45, 90 }; // Low alpha red accent
}
// Backward-compatibility alias namespace for gradual migration
namespace CyberColors {
inline const Color NeonCyan = TacticalColors::BorderWhite;
inline const Color NeonPink = TacticalColors::Crimson;
inline const Color NeonGreen = TacticalColors::PureWhite;
inline const Color NeonYellow = TacticalColors::Crimson;
inline const Color NeonOrange = TacticalColors::BrightRed;
inline const Color NeonPurple = TacticalColors::SilverText;
inline const Color DarkGrid = TacticalColors::PitchBlack;
inline const Color TerminalBg = TacticalColors::DarkPanel;
inline const Color WallBorder = TacticalColors::SteelMuted;
inline const Color WallGlow = TacticalColors::CrimsonGlow;
}
inline ImplantData getImplantData(ImplantType type) {
switch (type) {
case ImplantType::GHOST_CHAMBER:
return { type, "GHOST CHAMBER", "ROULETTE", ImplantArchetype::CYLINDER,
"Empty chamber gives +20 Shield and +50% damage on next shot.",
Color{ 0, 240, 255, 255 }, "[GHOST]", "GHOST" };
TacticalColors::PureWhite, "[GHOST]", "GHOST" };
case ImplantType::ADRENALINE_DRUM:
return { type, "ADRENALINE DRUM", "ROULETTE", ImplantArchetype::CYLINDER,
"Killing a drone instantly loads +1 random ammo & grants a free turn.",
Color{ 255, 230, 0, 255 }, "[ADREN]", "ADREN" };
TacticalColors::Crimson, "[ADREN]", "ADREN" };
case ImplantType::HEAVY_HAMMER:
return { type, "HEAVY HAMMER", "ROULETTE", ImplantArchetype::CYLINDER,
"Single shots deal +30% damage. Triple burst cooldown reduced to 1 turn.",
Color{ 255, 140, 30, 255 }, "[HAMMR]", "HAMMR" };
TacticalColors::SilverText, "[HAMMR]", "HAMMR" };
case ImplantType::CATALYST_MATRIX:
return { type, "CATALYST MATRIX", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
"Attacking Burned or Corroded foes detonates for +25 bonus direct damage.",
Color{ 255, 40, 130, 255 }, "[CATLY]", "CATLY" };
TacticalColors::Crimson, "[CATLY]", "CATLY" };
case ImplantType::CONDUCTIVE_SALVO:
return { type, "CONDUCTIVE SALVO", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
"EMP shots deal damage through enemy shields; Stun duration +1 turn.",
Color{ 80, 220, 255, 255 }, "[CONDC]", "CONDC" };
TacticalColors::PureWhite, "[CONDC]", "CONDC" };
case ImplantType::CORROSIVE_LEECH:
return { type, "CORROSIVE LEECH", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
"Nanite hits on Corroded targets restore 100% more HP & Shield.",
Color{ 57, 255, 20, 255 }, "[LEECH]", "LEECH" };
TacticalColors::BrightRed, "[LEECH]", "LEECH" };
case ImplantType::DEFLECTOR_FEEDBACK:
return { type, "DEFLECTOR FEEDBACK", "DEFENSE", ImplantArchetype::DEFENSE,
"When Shield breaks, a shock pulse deals 20 retaliatory damage to drone.",
Color{ 0, 210, 255, 255 }, "[DEFLC]", "DEFLC" };
TacticalColors::SilverText, "[DEFLC]", "DEFLC" };
case ImplantType::AUTO_DEFIBRILLATOR:
return { type, "AUTO-DEFIBRILLATOR", "DEFENSE", ImplantArchetype::DEFENSE,
"Once per run, lethal damage is prevented: grants 1 HP, 50 Shield, and stuns.",
Color{ 100, 255, 150, 255 }, "[DEFIB]", "DEFIB" };
TacticalColors::PureWhite, "[DEFIB]", "DEFIB" };
case ImplantType::NANITE_WEAVER:
return { type, "NANITE WEAVER", "DEFENSE", ImplantArchetype::DEFENSE,
"Start of turn in combat: restore +5 HP and +8 Shield if enemy has debuffs.",
Color{ 0, 255, 180, 255 }, "[WEAVR]", "WEAVR" };
TacticalColors::Crimson, "[WEAVR]", "WEAVR" };
case ImplantType::LOGIC_OVERCLOCK:
return { type, "LOGIC OVERCLOCK", "HACKER", ImplantArchetype::HACKER,
"Skill check Great zone is 2x wider; Critical hacking awards 2x Core Chips.",
Color{ 200, 80, 255, 255 }, "[LOGIC]", "LOGIC" };
TacticalColors::BrightRed, "[LOGIC]", "LOGIC" };
case ImplantType::SALVAGE_PROTOCOL:
return { type, "SALVAGE PROTOCOL", "HACKER", ImplantArchetype::HACKER,
"+50% Core Chips dropped from all sources; 20% ammo chance on door bypass.",
Color{ 255, 210, 40, 255 }, "[SALVG]", "SALVG" };
TacticalColors::PureWhite, "[SALVG]", "SALVG" };
case ImplantType::PRECOGNITION_OPTICS:
return { type, "PRECOGNITION OPTICS", "HACKER", ImplantArchetype::HACKER,
"Scans 5 tiles ahead, revealing Elites, Mines, Lasers & Caches on minimap.",
Color{ 120, 240, 255, 255 }, "[OPTIC]", "OPTIC" };
TacticalColors::SilverText, "[OPTIC]", "OPTIC" };
default:
return { type, "UNKNOWN IMPLANT", "GENERAL", ImplantArchetype::DEFENSE, "Undocumented firmware.", WHITE, "[CHIP]", "CHIP" };
return { type, "UNKNOWN IMPLANT", "GENERAL", ImplantArchetype::DEFENSE, "Undocumented firmware.", TacticalColors::PureWhite, "[CHIP]", "CHIP" };
}
}
@ -328,19 +364,4 @@ struct CyberShrine {
bool used = false;
};
// Cyberpunk color palette
namespace CyberColors {
inline const Color NeonCyan = { 0, 240, 255, 255 };
inline const Color NeonPink = { 255, 0, 128, 255 };
inline const Color NeonGreen = { 57, 255, 20, 255 };
inline const Color NeonYellow = { 255, 230, 0, 255 };
inline const Color NeonOrange = { 255, 120, 0, 255 };
inline const Color NeonPurple = { 180, 0, 255, 255 };
inline const Color DarkGrid = { 10, 14, 26, 255 };
inline const Color TerminalBg = { 6, 8, 15, 240 };
inline const Color WallBorder = { 30, 70, 120, 255 };
inline const Color WallGlow = { 0, 180, 255, 80 };
}
} // namespace dnr

View file

@ -127,6 +127,12 @@ void Game::update(float dt) {
m_saveFeedbackTimer -= dt;
}
// Update screen transition wave
m_transition.update(dt);
if (m_transition.isBusy()) {
return;
}
// Update dynamic music streaming engine every frame
SoundManager::get().updateMusic(dt);
@ -160,12 +166,12 @@ void Game::update(float dt) {
break;
case GameState::VICTORY_LEVEL:
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
enterRestRoom();
m_transition.start([this]() { enterRestRoom(); });
}
break;
case GameState::GAME_OVER:
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
startNewRun();
m_transition.start([this]() { startNewRun(); });
}
break;
}
@ -185,9 +191,9 @@ void Game::updateMainMenu(float dt) {
bool hasSave = SaveSystem::hasSaveFile();
if ((IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btnContinue))) && hasSave) {
loadSavedProgress();
m_transition.start([this]() { loadSavedProgress(); });
} else if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btnNewRun))) {
startNewRun();
m_transition.start([this]() { startNewRun(); });
} else if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btnSettings))) {
m_previousState = GameState::MAIN_MENU;
m_state = GameState::SETTINGS;
@ -257,7 +263,7 @@ void Game::tryMoveHub(int dx, int dy) {
void Game::tryInteractHub() {
if (m_hubRoom.isFacingPortal(m_player)) {
enterCyberspace();
m_transition.start([this]() { enterCyberspace(); });
} else if (m_hubRoom.isFacingTerminal(m_player)) {
saveCurrentProgress();
}
@ -386,7 +392,7 @@ void Game::tryMoveRestRoom(int dx, int dy) {
void Game::tryInteractRestRoom() {
if (m_restRoom.isFacingGateway(m_player)) {
nextFloor();
m_transition.start([this]() { nextFloor(); });
} else if (m_restRoom.isFacingRechargeMatrix(m_player)) {
if (!m_restRoom.isRecharged()) {
m_restRoom.triggerRecharge(m_player);
@ -783,7 +789,7 @@ void Game::tryInteractDungeon() {
if (m_dungeon.getTile(px, py) == TileType::EXIT_PORTAL ||
m_dungeon.getTile(fx, fy) == TileType::EXIT_PORTAL) {
enterRestRoom();
m_transition.start([this]() { enterRestRoom(); });
return;
}
}
@ -1174,6 +1180,9 @@ void Game::draw() {
break;
}
// Render screen transition wave on top of all 2D/3D elements
m_transition.draw();
EndDrawing();
}

View file

@ -8,6 +8,7 @@
#include "inventory/InventoryUI.hpp"
#include "render/CyberRenderer.hpp"
#include "render/UIRenderer.hpp"
#include "render/ScreenTransition.hpp"
#include "hub/HubRoom.hpp"
#include "hub/RestRoom.hpp"
#include "core/SaveSystem.hpp"
@ -64,6 +65,7 @@ private:
CyberRenderer m_renderer;
UIRenderer m_uiRenderer;
ScreenTransition m_transition;
float m_animTime = 0.0f;
float m_saveFeedbackTimer = 0.0f;

View file

@ -1,4 +1,5 @@
#include "InventoryUI.hpp"
#include "render/UIRenderer.hpp"
#include "core/SoundManager.hpp"
#include <cmath>
@ -117,65 +118,79 @@ void InventoryUI::draw(const Player& player) {
Vector2 mouse = GetMousePosition();
// Dark semi-transparent cyber overlay
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 245 });
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 5, 5, 5, 245 });
// Header
DrawRectangle(80, 35, GetScreenWidth() - 160, 52, Color{ 12, 22, 38, 255 });
DrawRectangleLines(80, 35, GetScreenWidth() - 160, 52, CyberColors::NeonCyan);
DrawText("CYBER-WEAPON ARMORY // CYLINDER CONFIGURATION", 105, 50, 20, CyberColors::NeonCyan);
DrawText("[TAB / ESC] RETURN TO GAME", GetScreenWidth() - 340, 51, 16, CyberColors::NeonYellow);
DrawRectangle(80, 35, GetScreenWidth() - 160, 52, TacticalColors::DarkPanel);
DrawRectangleLines(80, 35, GetScreenWidth() - 160, 52, TacticalColors::BorderWhite);
DrawText("CYBER-WEAPON ARMORY // CYLINDER CONFIGURATION", 105, 50, 20, TacticalColors::PureWhite);
DrawText("[TAB / ESC] RETURN TO GAME", GetScreenWidth() - 340, 51, 16, TacticalColors::Crimson);
// Left Panel: Revolver Cylinder
// Left Panel: Revolver Cylinder (Strict 0-rounding)
int cylCenterX = 350;
int cylCenterY = 320;
float cylRadius = 115.0f;
float chamberOrbitR = cylRadius - 35.0f;
float chamberR = 24.0f;
// 1. Heavy gunmetal outer housing
DrawCircle(cylCenterX, cylCenterY, cylRadius + 18.0f, Color{ 8, 14, 24, 250 });
DrawCircleLines(cylCenterX, cylCenterY, cylRadius + 18.0f, Color{ 30, 50, 75, 255 });
// Metallic cylinder body
DrawCircle(cylCenterX, cylCenterY, cylRadius, Color{ 20, 28, 42, 255 });
DrawCircleLines(cylCenterX, cylCenterY, cylRadius, CyberColors::NeonCyan);
// 6 Decorative flutes between chambers
float chamberOrbitR = 58.0f;
float visAngle = rev.getVisualAngle();
for (int i = 0; i < 6; ++i) {
float fluteAngle = (-visAngle + i * 60.0f + 30.0f - 90.0f) * (3.14159265f / 180.0f);
int fluteX = cylCenterX + static_cast<int>(std::cos(fluteAngle) * (cylRadius + 2.0f));
int fluteY = cylCenterY + static_cast<int>(std::sin(fluteAngle) * (cylRadius + 2.0f));
DrawCircle(fluteX, fluteY, 14.0f, Color{ 8, 14, 24, 255 });
DrawCircleLines(fluteX, fluteY, 14.0f, Color{ 40, 65, 95, 255 });
}
// 2. Extractor Ratchet & Hub
DrawCircle(cylCenterX, cylCenterY, 36.0f, Color{ 14, 20, 32, 255 });
DrawCircleLines(cylCenterX, cylCenterY, 36.0f, Color{ 70, 100, 140, 255 });
DrawCircle(cylCenterX, cylCenterY, 16.0f, Color{ 220, 175, 45, 255 });
DrawCircleLines(cylCenterX, cylCenterY, 16.0f, CyberColors::NeonYellow);
DrawText(".357", cylCenterX - 16, cylCenterY - 7, 13, CyberColors::NeonYellow);
// 1. Faceted Octagonal Heavy Outer Housing
DrawRectangle(cylCenterX - 110, cylCenterY - 110, 220, 220, TacticalColors::DarkPanel);
DrawRectangleLines(cylCenterX - 110, cylCenterY - 110, 220, 220, TacticalColors::BorderMuted);
// 3. Golden Striker Hammer / Firing Pin at 12:00 (-90 deg)
int strikerTipY = cylCenterY - static_cast<int>(cylRadius) - 4;
int strikerBaseY = cylCenterY - static_cast<int>(cylRadius) - 28;
// 45-degree chamfers on outer corners
DrawTriangle({ (float)(cylCenterX - 110), (float)(cylCenterY - 110) },
{ (float)(cylCenterX - 78), (float)(cylCenterY - 110) },
{ (float)(cylCenterX - 110), (float)(cylCenterY - 78) }, TacticalColors::PitchBlack);
DrawTriangle({ (float)(cylCenterX + 110), (float)(cylCenterY - 110) },
{ (float)(cylCenterX + 78), (float)(cylCenterY - 110) },
{ (float)(cylCenterX + 110), (float)(cylCenterY - 78) }, TacticalColors::PitchBlack);
DrawTriangle({ (float)(cylCenterX - 110), (float)(cylCenterY + 110) },
{ (float)(cylCenterX - 78), (float)(cylCenterY + 110) },
{ (float)(cylCenterX - 110), (float)(cylCenterY + 78) }, TacticalColors::PitchBlack);
DrawTriangle({ (float)(cylCenterX + 110), (float)(cylCenterY + 110) },
{ (float)(cylCenterX + 78), (float)(cylCenterY + 110) },
{ (float)(cylCenterX + 110), (float)(cylCenterY + 78) }, TacticalColors::PitchBlack);
DrawTriangle(
{ static_cast<float>(cylCenterX), static_cast<float>(strikerTipY) },
{ static_cast<float>(cylCenterX - 14), static_cast<float>(strikerBaseY) },
{ static_cast<float>(cylCenterX + 14), static_cast<float>(strikerBaseY) },
Color{ 255, 210, 50, 255 }
);
DrawRectangle(cylCenterX - 20, strikerBaseY - 14, 40, 14, Color{ 210, 160, 30, 255 });
DrawRectangleLines(cylCenterX - 20, strikerBaseY - 14, 40, 14, CyberColors::NeonYellow);
DrawCircle(cylCenterX, strikerTipY, 3.0f, CyberColors::NeonYellow);
// 2. Inner Chamfered Drum Plate
DrawRectangle(cylCenterX - 96, cylCenterY - 96, 192, 192, Color{ 16, 22, 32, 255 });
DrawRectangleLines(cylCenterX - 96, cylCenterY - 96, 192, 192, TacticalColors::BorderWhite);
DrawTriangle({ (float)(cylCenterX - 96), (float)(cylCenterY - 96) },
{ (float)(cylCenterX - 70), (float)(cylCenterY - 96) },
{ (float)(cylCenterX - 96), (float)(cylCenterY - 70) }, TacticalColors::DarkPanel);
DrawTriangle({ (float)(cylCenterX + 96), (float)(cylCenterY - 96) },
{ (float)(cylCenterX + 70), (float)(cylCenterY - 96) },
{ (float)(cylCenterX + 96), (float)(cylCenterY - 70) }, TacticalColors::DarkPanel);
DrawTriangle({ (float)(cylCenterX - 96), (float)(cylCenterY + 96) },
{ (float)(cylCenterX - 70), (float)(cylCenterY + 96) },
{ (float)(cylCenterX - 96), (float)(cylCenterY + 70) }, TacticalColors::DarkPanel);
DrawTriangle({ (float)(cylCenterX + 96), (float)(cylCenterY + 96) },
{ (float)(cylCenterX + 70), (float)(cylCenterY + 96) },
{ (float)(cylCenterX + 96), (float)(cylCenterY + 70) }, TacticalColors::DarkPanel);
DrawRectangle(cylCenterX - 65, strikerBaseY - 32, 130, 16, Color{ 10, 16, 26, 230 });
DrawRectangleLines(cylCenterX - 65, strikerBaseY - 32, 130, 16, Color{ 180, 150, 40, 200 });
DrawText("FIRING PIN [12:00]", cylCenterX - 58, strikerBaseY - 30, 11, CyberColors::NeonYellow);
// 3. Center Extractor Ratchet & Hub
DrawRectangle(cylCenterX - 26, cylCenterY - 26, 52, 52, Color{ 12, 16, 24, 255 });
DrawRectangleLines(cylCenterX - 26, cylCenterY - 26, 52, 52, TacticalColors::BorderMuted);
DrawRectangle(cylCenterX - 16, cylCenterY - 16, 32, 32, TacticalColors::SurfacePlate);
DrawRectangleLines(cylCenterX - 16, cylCenterY - 16, 32, 32, TacticalColors::Crimson);
DrawText(".357", cylCenterX - 15, cylCenterY - 7, 13, TacticalColors::PureWhite);
// 4. Six Chambers (Rotates with rev.getVisualAngle())
// 4. Aggressive Red Striker Firing Pin at 12:00 (-90 deg / top)
int strikerW = 56;
int strikerH = 16;
int strikerX = cylCenterX - strikerW / 2;
int strikerBaseY = cylCenterY - 110 - strikerH - 6;
DrawRectangle(strikerX, strikerBaseY, strikerW, strikerH, TacticalColors::DarkRed);
DrawRectangleLines(strikerX, strikerBaseY, strikerW, strikerH, TacticalColors::BrightRed);
DrawTriangle({ (float)cylCenterX, (float)(cylCenterY - 106) },
{ (float)(cylCenterX - 9), (float)(strikerBaseY + strikerH) },
{ (float)(cylCenterX + 9), (float)(strikerBaseY + strikerH) },
TacticalColors::BrightRed);
const char* pinTxt = "[ 12:00 FIRING PIN ]";
int pinW = MeasureText(pinTxt, 11);
DrawText(pinTxt, cylCenterX - pinW / 2, strikerBaseY - 18, 11, TacticalColors::BrightRed);
// 5. Six Chambers (Rotates with rev.getVisualAngle())
for (int i = 0; i < 6; ++i) {
float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f);
int chX = cylCenterX + static_cast<int>(std::cos(angleRad) * chamberOrbitR);
@ -186,42 +201,45 @@ void InventoryUI::draw(const Player& player) {
bool isSelected = (i == m_selectedChamber);
bool isCurrentReady = (i == rev.getCurrentIndex());
DrawCircle(chX, chY, chamberR, Color{ 16, 22, 34, 255 });
DrawCircleLines(chX, chY, chamberR, isSelected ? CyberColors::NeonYellow : Color{ 55, 80, 120, 255 });
int chW = 36;
int chH = 36;
Rectangle chRect = { (float)(chX - chW / 2), (float)(chY - chH / 2), (float)chW, (float)chH };
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);
DrawRectangleRec(chRect, TacticalColors::PitchBlack);
DrawRectangleLinesEx(chRect, isSelected ? 2.0f : 1.0f, isSelected ? TacticalColors::Crimson : (isCurrentReady ? TacticalColors::BorderWhite : TacticalColors::BorderMuted));
DrawText("---", chX - 9, chY - 5, 10, TacticalColors::MutedText);
} 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 - 4, chY - 4, 3.0f, Color{ 255, 255, 255, 200 });
DrawText(info.shortName.substr(0, 3).c_str(), chX - 9, chY - 5, 11, BLACK);
DrawRectangleRec(chRect, TacticalColors::DarkPanel);
DrawRectangleLinesEx(chRect, isSelected ? 2.0f : 1.0f, isSelected ? TacticalColors::Crimson : info.color);
// Tactical cartridge primer block
DrawRectangle(chX - 7, chY - 7, 14, 14, Color{ info.color.r, info.color.g, info.color.b, 200 });
DrawRectangleLines(chX - 7, chY - 7, 14, 14, info.color);
DrawText(info.shortName.substr(0, 3).c_str(), chX - 10, chY - 5, 10, TacticalColors::PureWhite);
}
// Selection Reticle
// Selection Reticle (Square brackets)
if (isSelected) {
DrawRectangleLines(chX - static_cast<int>(chamberR) - 4, chY - static_cast<int>(chamberR) - 4,
static_cast<int>(chamberR * 2) + 8, static_cast<int>(chamberR * 2) + 8,
CyberColors::NeonYellow);
DrawRectangleLinesEx({ chRect.x - 4, chRect.y - 4, chRect.width + 8, chRect.height + 8 }, 2.0f, TacticalColors::Crimson);
DrawLine(chX, (int)chRect.y - 8, chX, (int)chRect.y - 3, TacticalColors::Crimson);
DrawLine(chX, (int)(chRect.y + chRect.height + 3), chX, (int)(chRect.y + chRect.height + 8), TacticalColors::Crimson);
DrawLine((int)chRect.x - 8, chY, (int)chRect.x - 3, chY, TacticalColors::Crimson);
DrawLine((int)(chRect.x + chRect.width + 3), chY, (int)(chRect.x + chRect.width + 8), chY, TacticalColors::Crimson);
}
// Active Chamber Marker (Under firing pin)
if (isCurrentReady) {
DrawCircleLines(chX, chY, chamberR + 6.0f, CyberColors::NeonGreen);
DrawText("READY", chX - 16, chY + 28, 10, CyberColors::NeonGreen);
DrawText("READY", chX - 16, chY + 24, 10, TacticalColors::PureWhite);
}
}
// Selected chamber info box below cylinder
// Selected chamber info box below cylinder (Strict 0-rounding)
int boxX = 110;
int boxY = 480;
int boxW = 480;
int boxH = 100;
DrawRectangle(boxX, boxY, boxW, boxH, Color{ 10, 18, 30, 255 });
DrawRectangleLines(boxX, boxY, boxW, boxH, CyberColors::NeonCyan);
DrawRectangle(boxX, boxY, boxW, boxH, TacticalColors::DarkPanel);
DrawRectangleLines(boxX, boxY, boxW, boxH, TacticalColors::BorderMuted);
AmmoType selChamberAmmo = rev.getChamber(m_selectedChamber);
const auto& selChamberInfo = getAmmoInfo(selChamberAmmo);
@ -230,17 +248,17 @@ void InventoryUI::draw(const Player& player) {
if (m_selectedChamber == rev.getCurrentIndex()) {
selHeader += " [ACTIVE @ 12:00 FIRING PIN]";
}
DrawText(selHeader.c_str(), boxX + 15, boxY + 12, 15, CyberColors::NeonYellow);
DrawText(selHeader.c_str(), boxX + 15, boxY + 12, 15, TacticalColors::Crimson);
DrawText(("LOADED: " + selChamberInfo.name).c_str(), boxX + 15, boxY + 36, 15, selChamberInfo.color);
DrawText(("IMPACT: " + std::to_string(selChamberInfo.baseDamage) + " DMG // " + selChamberInfo.description).c_str(), boxX + 15, boxY + 58, 12, RAYWHITE);
DrawText("[1-6 / CLICK] SELECT | [U] UNLOAD | [R] SPIN ROULETTE", boxX + 15, boxY + 78, 11, CyberColors::NeonCyan);
DrawText(("IMPACT: " + std::to_string(selChamberInfo.baseDamage) + " DMG // " + selChamberInfo.description).c_str(), boxX + 15, boxY + 58, 12, TacticalColors::SilverText);
DrawText("[1-6 / CLICK] SELECT | [U] UNLOAD | [R] SPIN ROULETTE", boxX + 15, boxY + 78, 11, TacticalColors::MutedText);
// Ammo Probability Breakdown under info box
int oddsY = boxY + boxH + 12;
int oddsH = 65;
DrawRectangle(boxX, oddsY, boxW, oddsH, Color{ 8, 14, 24, 255 });
DrawRectangleLines(boxX, oddsY, boxW, oddsH, Color{ 35, 60, 95, 255 });
DrawText("// AMMO PROBABILITY ROULETTE", boxX + 15, oddsY + 8, 10, GRAY);
DrawRectangle(boxX, oddsY, boxW, oddsH, TacticalColors::DarkPanel);
DrawRectangleLines(boxX, oddsY, boxW, oddsH, TacticalColors::BorderMuted);
DrawText("// AMMO PROBABILITY ROULETTE", boxX + 15, oddsY + 8, 10, TacticalColors::MutedText);
int loadedCount = 0;
int typeCounts[7] = { 0 };
@ -253,10 +271,10 @@ void InventoryUI::draw(const Player& player) {
int blankChance = 100 - hitChance;
std::string oddsStr = "LOADED: " + std::to_string(loadedCount) + "/6 (" + std::to_string(hitChance) + "% HIT)";
DrawText(oddsStr.c_str(), boxX + 15, oddsY + 24, 13, hitChance >= 50 ? CyberColors::NeonGreen : CyberColors::NeonYellow);
DrawText(oddsStr.c_str(), boxX + 15, oddsY + 24, 13, hitChance >= 50 ? TacticalColors::PureWhite : TacticalColors::Crimson);
std::string blankStr = "MISFIRE RISK: " + std::to_string(blankChance) + "%";
DrawText(blankStr.c_str(), boxX + 220, oddsY + 24, 13, blankChance == 0 ? CyberColors::NeonGreen : (blankChance > 33 ? RED : CyberColors::NeonYellow));
DrawText(blankStr.c_str(), boxX + 220, oddsY + 24, 13, blankChance == 0 ? TacticalColors::PureWhite : TacticalColors::Crimson);
std::string chipSummary = "";
for (int t = 1; t <= 6; ++t) {
@ -267,7 +285,7 @@ void InventoryUI::draw(const Player& player) {
}
}
if (chipSummary.empty()) chipSummary = "ALL CHAMBERS EMPTY - LOAD CARTRIDGES FROM STACK";
DrawText(chipSummary.c_str(), boxX + 15, oddsY + 44, 11, Color{ 170, 195, 225, 255 });
DrawText(chipSummary.c_str(), boxX + 15, oddsY + 44, 11, TacticalColors::SilverText);
// Right Panel: Ammo Pouch / Storage + Neural Implants Matrix
int pouchX = 660;
@ -275,12 +293,12 @@ void InventoryUI::draw(const Player& player) {
int pouchW = 520;
int pouchH = 580;
DrawRectangle(pouchX, pouchY, pouchW, pouchH, Color{ 10, 16, 26, 255 });
DrawRectangleLines(pouchX, pouchY, pouchW, pouchH, CyberColors::NeonCyan);
DrawText("TACTICAL CARTRIDGE STACK // INVENTORY", pouchX + 20, pouchY + 12, 16, CyberColors::NeonCyan);
DrawRectangle(pouchX, pouchY, pouchW, pouchH, TacticalColors::DarkPanel);
DrawRectangleLines(pouchX, pouchY, pouchW, pouchH, TacticalColors::BorderWhite);
DrawText("TACTICAL CARTRIDGE STACK // INVENTORY", pouchX + 20, pouchY + 12, 16, TacticalColors::PureWhite);
DrawText(("[W/S / CLICK] SELECT [ENTER / E / SPACE] LOAD INTO CHAMBER #" + std::to_string(m_selectedChamber + 1)).c_str(),
pouchX + 20, pouchY + 36, 12, CyberColors::NeonYellow);
pouchX + 20, pouchY + 36, 12, TacticalColors::Crimson);
int startY = pouchY + 58;
for (int i = 0; i < 6; ++i) {
@ -292,29 +310,31 @@ void InventoryUI::draw(const Player& player) {
Rectangle rowRect = { static_cast<float>(pouchX + 15), static_cast<float>(startY + i * 42), static_cast<float>(pouchW - 30), 38.0f };
bool isHover = CheckCollisionPointRec(mouse, rowRect);
Color boxCol = isSel ? Color{ 30, 50, 80, 255 } : (isHover ? Color{ 20, 32, 52, 255 } : Color{ 14, 20, 32, 255 });
Color borderCol = isSel ? CyberColors::NeonYellow : (isHover ? CyberColors::NeonCyan : Color{ 30, 50, 75, 255 });
Color boxCol = isSel ? TacticalColors::SurfacePlate : (isHover ? Color{ 24, 24, 24, 255 } : TacticalColors::PitchBlack);
Color borderCol = isSel ? TacticalColors::Crimson : (isHover ? TacticalColors::BorderWhite : TacticalColors::BorderMuted);
DrawRectangleRec(rowRect, boxCol);
DrawRectangleLinesEx(rowRect, isSel ? 2.0f : 1.0f, borderCol);
// Icon
DrawCircle(pouchX + 32, startY + i * 42 + 19, 10.0f, count > 0 ? itemInfo.color : DARKGRAY);
// Tactical rectangular badge (Strict 0-rounding)
DrawRectangle(pouchX + 22, startY + i * 42 + 9, 22, 20, count > 0 ? TacticalColors::SurfacePlate : TacticalColors::PitchBlack);
DrawRectangleLines(pouchX + 22, startY + i * 42 + 9, 22, 20, count > 0 ? itemInfo.color : TacticalColors::BorderMuted);
DrawText(itemInfo.shortName.substr(0, 3).c_str(), pouchX + 24, startY + i * 42 + 14, 9, count > 0 ? itemInfo.color : TacticalColors::MutedText);
// Name
DrawText(itemInfo.name.c_str(), pouchX + 50, startY + i * 42 + 11, 14, count > 0 ? (isSel ? CyberColors::NeonYellow : RAYWHITE) : GRAY);
DrawText(itemInfo.name.c_str(), pouchX + 50, startY + i * 42 + 11, 14, count > 0 ? (isSel ? TacticalColors::PureWhite : TacticalColors::SilverText) : TacticalColors::MutedText);
// Tier badge
int tier = player.getAmmoTier(aType);
std::string tStr = tier == 1 ? "T1" : (tier == 2 ? "T2" : "T3");
DrawText(tStr.c_str(), pouchX + 230, startY + i * 42 + 12, 12, tier >= 3 ? CyberColors::NeonYellow : (tier == 2 ? CyberColors::NeonGreen : GRAY));
DrawText(tStr.c_str(), pouchX + 230, startY + i * 42 + 12, 12, tier >= 3 ? TacticalColors::Crimson : TacticalColors::SilverText);
// Damage Tag
DrawText(("DMG: " + std::to_string(itemInfo.baseDamage)).c_str(), pouchX + 270, startY + i * 42 + 12, 13, CyberColors::NeonCyan);
DrawText(("DMG: " + std::to_string(itemInfo.baseDamage)).c_str(), pouchX + 270, startY + i * 42 + 12, 13, TacticalColors::PureWhite);
// Stack Count
std::string countStr = "[x " + std::to_string(count) + "]";
Color countColor = (count > 0) ? (isSel ? CyberColors::NeonYellow : CyberColors::NeonGreen) : RED;
Color countColor = (count > 0) ? (isSel ? TacticalColors::PureWhite : TacticalColors::SilverText) : TacticalColors::Crimson;
DrawText(countStr.c_str(), pouchX + 430, startY + i * 42 + 11, 15, countColor);
}
@ -325,25 +345,25 @@ void InventoryUI::draw(const Player& player) {
int detailY = startY + 6 * 42 + 4;
int detailH = 64;
DrawRectangle(pouchX + 15, detailY, pouchW - 30, detailH, Color{ 14, 24, 40, 255 });
DrawRectangleLines(pouchX + 15, detailY, pouchW - 30, detailH, CyberColors::NeonGreen);
DrawRectangle(pouchX + 15, detailY, pouchW - 30, detailH, TacticalColors::SurfacePlate);
DrawRectangleLines(pouchX + 15, detailY, pouchW - 30, detailH, TacticalColors::BorderWhite);
DrawText(curSelInfo.name.c_str(), pouchX + 26, detailY + 8, 14, curSelInfo.color);
DrawText(("IN STOCK: " + std::to_string(curSelCount)).c_str(), pouchX + 370, detailY + 8, 12, curSelCount > 0 ? CyberColors::NeonGreen : RED);
DrawText(curSelInfo.description.c_str(), pouchX + 26, detailY + 28, 11, RAYWHITE);
DrawText("[ENTER / E / SPACE] INSERT INTO SELECTED CHAMBER", pouchX + 26, detailY + 46, 11, CyberColors::NeonYellow);
DrawText(("IN STOCK: " + std::to_string(curSelCount)).c_str(), pouchX + 370, detailY + 8, 12, curSelCount > 0 ? TacticalColors::PureWhite : TacticalColors::Crimson);
UIRenderer::drawWrappedText(curSelInfo.description, pouchX + 26, detailY + 28, pouchW - 55, 11, 2, TacticalColors::SilverText);
DrawText("[ENTER / E / SPACE] INSERT INTO SELECTED CHAMBER", pouchX + 26, detailY + 46, 11, TacticalColors::Crimson);
// Neural Implants Matrix Panel (2x2 grid)
int impPanelY = detailY + detailH + 8;
int impPanelH = pouchY + pouchH - impPanelY - 10;
DrawRectangle(pouchX + 15, impPanelY, pouchW - 30, impPanelH, Color{ 12, 18, 30, 255 });
DrawRectangleLines(pouchX + 15, impPanelY, pouchW - 30, impPanelH, CyberColors::NeonCyan);
DrawRectangle(pouchX + 15, impPanelY, pouchW - 30, impPanelH, TacticalColors::DarkPanel);
DrawRectangleLines(pouchX + 15, impPanelY, pouchW - 30, impPanelH, TacticalColors::BorderMuted);
std::string impHeader = "// NEURAL MATRIX RIG // SLOTS [" + std::to_string(player.getImplants().size()) + " / 4]";
DrawText(impHeader.c_str(), pouchX + 25, impPanelY + 8, 12, CyberColors::NeonCyan);
DrawText(impHeader.c_str(), pouchX + 25, impPanelY + 8, 12, TacticalColors::PureWhite);
std::string chipTxt = "[ ❖ " + std::to_string(player.getTechScraps()) + " ]";
DrawText(chipTxt.c_str(), pouchX + pouchW - 95, impPanelY + 8, 12, CyberColors::NeonYellow);
std::string chipTxt = "[ SCRAP: " + std::to_string(player.getTechScraps()) + " ]";
DrawText(chipTxt.c_str(), pouchX + pouchW - 120, impPanelY + 8, 12, TacticalColors::Crimson);
const auto& implants = player.getImplants();
int slotW = 236;
@ -365,28 +385,28 @@ void InventoryUI::draw(const Player& player) {
Color archCol = data.color;
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
archCol = RED;
archCol = TacticalColors::Crimson;
}
DrawRectangleRec(sRect, Color{ 16, 24, 38, 255 });
DrawRectangleLinesEx(sRect, isHoverImp ? 2.0f : 1.0f, isHoverImp ? WHITE : archCol);
DrawRectangleRec(sRect, TacticalColors::SurfacePlate);
DrawRectangleLinesEx(sRect, isHoverImp ? 2.0f : 1.0f, isHoverImp ? TacticalColors::PureWhite : archCol);
std::string sTitle = "SLOT " + std::to_string(s + 1) + ": " + data.name;
DrawText(sTitle.c_str(), sX + 8, sY + 6, 11, archCol);
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
DrawText("[STATUS: REVIVE DEPLETED]", sX + 8, sY + 24, 10, RED);
DrawText("[STATUS: REVIVE DEPLETED]", sX + 8, sY + 24, 10, TacticalColors::Crimson);
} else {
std::string shortDesc = data.description;
if (shortDesc.length() > 32) shortDesc = shortDesc.substr(0, 30) + "..";
DrawText(shortDesc.c_str(), sX + 8, sY + 24, 10, RAYWHITE);
DrawText(shortDesc.c_str(), sX + 8, sY + 24, 10, TacticalColors::SilverText);
}
} else {
DrawRectangleRec(sRect, Color{ 8, 12, 20, 200 });
DrawRectangleLinesEx(sRect, 1.0f, Color{ 30, 45, 65, 200 });
DrawRectangleRec(sRect, TacticalColors::PitchBlack);
DrawRectangleLinesEx(sRect, 1.0f, TacticalColors::BorderMuted);
std::string emptyTitle = "SLOT " + std::to_string(s + 1) + ": [EMPTY SOCKET]";
DrawText(emptyTitle.c_str(), sX + 8, sY + 8, 11, DARKGRAY);
DrawText("Draft from Elites / ICE", sX + 8, sY + 26, 10, Color{ 60, 80, 110, 255 });
DrawText(emptyTitle.c_str(), sX + 8, sY + 8, 11, TacticalColors::MutedText);
DrawText("Draft from Elites / ICE", sX + 8, sY + 26, 10, TacticalColors::BorderMuted);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,7 @@ private:
Texture2D m_texMonitorRadar{};
Texture2D m_texBedQuilt{};
Texture2D m_texCityNight{};
Texture2D m_texWindowView{};
Texture2D m_texCyberGate{};
// Ancient Palace Storeroom Procedural Textures

View file

@ -0,0 +1,144 @@
#pragma once
#include "raylib.h"
#include "core/Common.hpp"
#include <functional>
#include <algorithm>
#include <cmath>
namespace dnr {
class ScreenTransition {
public:
enum class Phase {
IDLE,
WIPE_IN,
HOLD_MIDPOINT,
WIPE_OUT
};
ScreenTransition() = default;
void start(std::function<void()> onMidpoint) {
m_onMidpoint = onMidpoint;
m_phase = Phase::WIPE_IN;
m_timer = 0.0f;
}
bool isBusy() const {
return m_phase != Phase::IDLE;
}
void update(float dt) {
if (m_phase == Phase::IDLE) return;
m_timer += dt;
const float wipeInDuration = 0.35f;
const float holdDuration = 0.05f;
const float wipeOutDuration = 0.35f;
if (m_phase == Phase::WIPE_IN) {
if (m_timer >= wipeInDuration) {
m_phase = Phase::HOLD_MIDPOINT;
m_timer = 0.0f;
if (m_onMidpoint) {
m_onMidpoint();
}
}
} else if (m_phase == Phase::HOLD_MIDPOINT) {
if (m_timer >= holdDuration) {
m_phase = Phase::WIPE_OUT;
m_timer = 0.0f;
}
} else if (m_phase == Phase::WIPE_OUT) {
if (m_timer >= wipeOutDuration) {
m_phase = Phase::IDLE;
m_timer = 0.0f;
m_onMidpoint = nullptr;
}
}
}
void draw() {
if (m_phase == Phase::IDLE) return;
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
const int blockSize = 32;
int cols = (screenW + blockSize - 1) / blockSize;
int rows = (screenH + blockSize - 1) / blockSize;
int maxDist = (cols - 1) + (rows - 1);
if (maxDist <= 0) maxDist = 1;
if (m_phase == Phase::HOLD_MIDPOINT) {
// Full opaque PitchBlack screen
DrawRectangle(0, 0, screenW, screenH, TacticalColors::PitchBlack);
return;
}
const float wipeInDuration = 0.35f;
const float wipeOutDuration = 0.35f;
const float waveSpread = 0.32f;
bool isWipeIn = (m_phase == Phase::WIPE_IN);
float progress = isWipeIn ? std::clamp(m_timer / wipeInDuration, 0.0f, 1.0f)
: std::clamp(m_timer / wipeOutDuration, 0.0f, 1.0f);
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
float dist = static_cast<float>(c + r) / static_cast<float>(maxDist);
float cellProgress = 0.0f;
if (isWipeIn) {
// Blocks expand along wave: starts when progress >= dist * (1 - waveSpread)
float startThreshold = dist * (1.0f - waveSpread);
cellProgress = std::clamp((progress - startThreshold) / waveSpread, 0.0f, 1.0f);
} else {
// Blocks shrink along wave: dissolved as progress advances
float startThreshold = dist * (1.0f - waveSpread);
float dissolve = std::clamp((progress - startThreshold) / waveSpread, 0.0f, 1.0f);
cellProgress = 1.0f - dissolve;
}
if (cellProgress <= 0.001f) continue;
int posX = c * blockSize;
int posY = r * blockSize;
if (cellProgress >= 0.999f) {
// Full solid black block (Strict 0-rounding)
DrawRectangle(posX, posY, blockSize, blockSize, TacticalColors::PitchBlack);
} else {
// Partial block growing / shrinking from center
int curSize = static_cast<int>(blockSize * cellProgress);
int offset = (blockSize - curSize) / 2;
Rectangle bRect = {
static_cast<float>(posX + offset),
static_cast<float>(posY + offset),
static_cast<float>(curSize),
static_cast<float>(curSize)
};
DrawRectangleRec(bRect, TacticalColors::PitchBlack);
// Tactical Crimson Edge on wave frontier
DrawRectangleLinesEx(bRect, 1.0f, TacticalColors::Crimson);
// Glitch binary glyph on active frontline
if (curSize > 14 && ((c * 7 + r * 13) % 4 == 0)) {
const char* glyph = ((c + r) % 2 == 0) ? "1" : "0";
DrawText(glyph, posX + blockSize / 2 - 3, posY + blockSize / 2 - 5, 10, TacticalColors::Crimson);
}
}
}
}
}
private:
Phase m_phase = Phase::IDLE;
float m_timer = 0.0f;
std::function<void()> m_onMidpoint;
};
} // namespace dnr

View file

@ -1,6 +1,7 @@
#include "UIRenderer.hpp"
#include <cmath>
#include <algorithm>
#include <sstream>
namespace dnr {
@ -13,15 +14,36 @@ void UIRenderer::drawProgressBar(int x, int y, int width, int height, float rati
DrawRectangleLines(x, y, width, height, Color{ 40, 80, 120, 255 });
}
void UIRenderer::drawWrappedText(const std::string& text, int posX, int posY, int maxW, int fontSize, int lineSpacing, Color color) {
std::istringstream words(text);
std::string word;
std::string currentLine = "";
int curY = posY;
while (words >> word) {
std::string testLine = currentLine.empty() ? word : currentLine + " " + word;
if (MeasureText(testLine.c_str(), fontSize) > maxW && !currentLine.empty()) {
DrawText(currentLine.c_str(), posX, curY, fontSize, color);
curY += fontSize + lineSpacing;
currentLine = word;
} else {
currentLine = testLine;
}
}
if (!currentLine.empty()) {
DrawText(currentLine.c_str(), posX, curY, fontSize, color);
}
}
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);
// Frame (Strict 0-rounding)
DrawRectangle(startX - 4, startY - 4, size + 8, size + 8, TacticalColors::DarkPanel);
DrawRectangleLines(startX - 4, startY - 4, size + 8, size + 8, TacticalColors::BorderMuted);
DrawText("TACTICAL MAP // 2D", startX, startY - 18, 11, TacticalColors::PureWhite);
bool hasPrecog = player.hasImplant(ImplantType::PRECOGNITION_OPTICS);
@ -38,22 +60,22 @@ void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int s
TileType t = dungeon.getTile(x, y);
switch (t) {
case TileType::WALL:
DrawRectangle(px, py, cs, cs, Color{ 25, 45, 75, 255 });
DrawRectangle(px, py, cs, cs, Color{ 30, 36, 48, 255 });
break;
case TileType::FLOOR:
DrawRectangle(px, py, cs, cs, Color{ 10, 20, 35, 255 });
DrawRectangle(px, py, cs, cs, Color{ 10, 14, 20, 255 });
break;
case TileType::DOOR_LOCKED:
DrawRectangle(px, py, cs, cs, CyberColors::NeonOrange);
DrawRectangle(px, py, cs, cs, TacticalColors::Crimson);
break;
case TileType::DOOR_UNLOCKED:
DrawRectangle(px, py, cs, cs, CyberColors::NeonGreen);
DrawRectangle(px, py, cs, cs, TacticalColors::PureWhite);
break;
case TileType::LOOT_CACHE:
DrawRectangle(px, py, cs, cs, CyberColors::NeonYellow);
DrawRectangle(px, py, cs, cs, TacticalColors::BorderWhite);
break;
case TileType::EXIT_PORTAL:
DrawRectangle(px, py, cs, cs, CyberColors::NeonCyan);
DrawRectangle(px, py, cs, cs, TacticalColors::Crimson);
break;
default:
break;
@ -68,34 +90,35 @@ void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int s
int lx = startX + static_cast<int>(laser.x * cellSize);
int ly = startY + static_cast<int>(laser.y * cellSize);
int cs = static_cast<int>(cellSize) + 1;
Color lCol = laser.disarmed ? DARKGRAY : (laser.active ? RED : CyberColors::NeonCyan);
Color lCol = laser.disarmed ? DARKGRAY : (laser.active ? TacticalColors::BrightRed : TacticalColors::BorderMuted);
DrawRectangle(lx + 1, ly + 1, std::max(2, cs - 2), std::max(2, cs - 2), lCol);
}
}
// Mines on minimap
// Mines on minimap (Sharp square)
for (const auto& mine : dungeon.getMines()) {
int dist = std::abs(mine.x - player.getGridX()) + std::abs(mine.y - player.getGridY());
if (dungeon.isExplored(mine.x, mine.y) || (hasPrecog && dist <= 5)) {
int mx = startX + static_cast<int>((mine.x + 0.5f) * cellSize);
int my = startY + static_cast<int>((mine.y + 0.5f) * cellSize);
Color mCol = mine.defused ? DARKGRAY : (mine.isArmed() ? CyberColors::NeonOrange : RED);
DrawCircle(mx, my, std::max(2.0f, cellSize * 0.35f), mCol);
Color mCol = mine.defused ? DARKGRAY : (mine.isArmed() ? TacticalColors::Crimson : TacticalColors::BrightRed);
DrawRectangle(mx - 2, my - 2, 5, 5, mCol);
}
}
// Cyber Shrines on minimap
// Cyber Shrines on minimap (Sharp square mark)
for (const auto& shrine : dungeon.getShrines()) {
int dist = std::abs(shrine.x - player.getGridX()) + std::abs(shrine.y - player.getGridY());
if (dungeon.isExplored(shrine.x, shrine.y) || (hasPrecog && dist <= 5)) {
int sx = startX + static_cast<int>((shrine.x + 0.5f) * cellSize);
int sy = startY + static_cast<int>((shrine.y + 0.5f) * cellSize);
Color sCol = shrine.used ? DARKGRAY : CyberColors::NeonPink;
DrawCircle(sx, sy, std::max(2.5f, cellSize * 0.45f), sCol);
Color sCol = shrine.used ? DARKGRAY : TacticalColors::PureWhite;
DrawRectangle(sx - 3, sy - 3, 6, 6, sCol);
DrawRectangleLines(sx - 3, sy - 3, 6, 6, TacticalColors::Crimson);
}
}
// Enemies on minimap
// Enemies on minimap (Sharp tactical diamonds/squares)
for (const auto& me : dungeon.getEnemies()) {
if (!me.alive) continue;
int dist = std::abs(me.x - player.getGridX()) + std::abs(me.y - player.getGridY());
@ -103,21 +126,26 @@ void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int s
int ex = startX + static_cast<int>((me.x + 0.5f) * cellSize);
int ey = startY + static_cast<int>((me.y + 0.5f) * cellSize);
if (me.enemy.isElite()) {
DrawCircle(ex, ey, std::max(3.0f, cellSize * 0.5f), CyberColors::NeonYellow);
DrawCircleLines(ex, ey, std::max(4.0f, cellSize * 0.65f), CyberColors::NeonPink);
DrawRectangle(ex - 4, ey - 4, 9, 9, TacticalColors::Crimson);
DrawRectangle(ex - 2, ey - 2, 5, 5, TacticalColors::PureWhite);
} else {
DrawCircle(ex, ey, std::max(2.0f, cellSize * 0.35f), RED);
DrawRectangle(ex - 2, ey - 2, 5, 5, TacticalColors::BrightRed);
}
}
}
// Player icon and direction on minimap
// Player Chevron Arrow on minimap (Strict 0-rounding)
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);
float pSize = cellSize * 0.75f;
Vector2 tip = { plX + dirVec.x * pSize, plY + dirVec.y * pSize };
Vector2 left = { plX - dirVec.y * (pSize * 0.55f) - dirVec.x * (pSize * 0.35f),
plY + dirVec.x * (pSize * 0.55f) - dirVec.y * (pSize * 0.35f) };
Vector2 right = { plX + dirVec.y * (pSize * 0.55f) - dirVec.x * (pSize * 0.35f),
plY - dirVec.x * (pSize * 0.55f) - dirVec.y * (pSize * 0.35f) };
DrawTriangle(tip, left, right, TacticalColors::PureWhite);
DrawRectangle(plX - 1, plY - 1, 3, 3, TacticalColors::Crimson);
}
void UIRenderer::drawMatrixDigitalRain(float animTime) {
@ -131,65 +159,75 @@ void UIRenderer::drawMatrixDigitalRain(float animTime) {
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 });
DrawText(txt, xPos, static_cast<int>(yPos), 12, Color{ 140, 150, 165, 30 });
DrawText(txt, xPos, static_cast<int>(yPos - 20), 11, Color{ 140, 150, 165, 15 });
}
}
void UIRenderer::drawMatrixOverlay(float animTime) {
// Subtle Matrix CRT scanlines
// Subtle 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 });
DrawLine(0, y, screenW, y, Color{ 180, 195, 215, 10 });
}
// 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 });
DrawText("[SYS::TACTICAL_OS] [ARASAKA_NET] [PROTOCOL::357_MAGNUM] [SECURITY::ACTIVE]",
15, screenH - 18, 11, TacticalColors::MutedText);
}
void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player, const std::string& prompt) {
// Top-Left: Player Vital Stats
// Top-Left: Player Vital Stats (Strict 0-rounding)
int hudX = 20;
int hudY = 20;
DrawRectangle(hudX, hudY, 330, 145, Color{ 6, 12, 22, 220 });
DrawRectangleLines(hudX, hudY, 330, 145, CyberColors::NeonCyan);
DrawRectangle(hudX, hudY, 330, 145, TacticalColors::DarkPanel);
DrawRectangleLines(hudX, hudY, 330, 145, TacticalColors::BorderMuted);
DrawText(("SECTOR LAYER // " + std::to_string(dungeon.getFloor())).c_str(), hudX + 15, hudY + 12, 16, CyberColors::NeonCyan);
// Tactical corner bracket accents
DrawRectangle(hudX, hudY, 6, 2, TacticalColors::Crimson);
DrawRectangle(hudX, hudY, 2, 6, TacticalColors::Crimson);
DrawRectangle(hudX + 324, hudY, 6, 2, TacticalColors::Crimson);
DrawRectangle(hudX + 328, hudY, 2, 6, TacticalColors::Crimson);
DrawRectangle(hudX, hudY + 143, 6, 2, TacticalColors::Crimson);
DrawRectangle(hudX, hudY + 139, 2, 6, TacticalColors::Crimson);
DrawRectangle(hudX + 324, hudY + 143, 6, 2, TacticalColors::Crimson);
DrawRectangle(hudX + 328, hudY + 139, 2, 6, TacticalColors::Crimson);
// HP Bar
DrawText(("SECTOR LAYER // " + std::to_string(dungeon.getFloor())).c_str(), hudX + 15, hudY + 12, 16, TacticalColors::PureWhite);
// HP Bar (Crimson fill)
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);
drawProgressBar(hudX + 60, hudY + 40, 250, 18, hpRatio, TacticalColors::Crimson, TacticalColors::DarkRed);
DrawText("CORE", hudX + 15, hudY + 40, 14, TacticalColors::Crimson);
DrawText((std::to_string(player.getHp()) + " / " + std::to_string(player.getMaxHp())).c_str(), hudX + 145, hudY + 42, 13, TacticalColors::PureWhite);
// Shield Bar
// Shield Bar (High-contrast pure white / silver)
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);
drawProgressBar(hudX + 60, hudY + 68, 250, 18, shRatio, TacticalColors::PureWhite, Color{ 24, 30, 42, 255 });
DrawText("SHLD", hudX + 15, hudY + 68, 14, TacticalColors::PureWhite);
DrawText((std::to_string(player.getShield()) + " / " + std::to_string(player.getMaxShield())).c_str(), hudX + 145, hudY + 70, 13, TacticalColors::PitchBlack);
// Cyber-Picks & Statuses
DrawText(("ICE PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 102, 15, CyberColors::NeonYellow);
// ICE-Picks & Statuses
DrawText(("ICE PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 102, 14, TacticalColors::SilverText);
std::string statStr = "STATUS: ";
if (player.getStatuses().empty()) {
statStr += "SECURE";
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, CyberColors::NeonGreen);
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, TacticalColors::PureWhite);
} else {
statStr += "MALWARE_DEBUFF";
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, RED);
statStr += "HOSTILE_DEBUFF";
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, TacticalColors::BrightRed);
}
// Neural Implants HUD Bar
// Neural Implants HUD Bar (Strict 0-rounding)
int impBarX = hudX;
int impBarY = hudY + 152;
int impBarW = 330;
int impBarH = 34;
DrawRectangle(impBarX, impBarY, impBarW, impBarH, Color{ 6, 12, 22, 220 });
DrawRectangleLines(impBarX, impBarY, impBarW, impBarH, CyberColors::NeonCyan);
DrawRectangle(impBarX, impBarY, impBarW, impBarH, TacticalColors::DarkPanel);
DrawRectangleLines(impBarX, impBarY, impBarW, impBarH, TacticalColors::BorderMuted);
const auto& implants = player.getImplants();
Vector2 mousePos = GetMousePosition();
@ -207,11 +245,11 @@ void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player
Color archCol = data.color;
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
archCol = RED;
archCol = TacticalColors::DarkRed;
}
DrawRectangleRec(bRect, Color{ 12, 20, 32, 240 });
DrawRectangleLinesEx(bRect, isHover ? 2.0f : 1.0f, isHover ? WHITE : archCol);
DrawRectangleRec(bRect, TacticalColors::SurfacePlate);
DrawRectangleLinesEx(bRect, isHover ? 2.0f : 1.0f, isHover ? TacticalColors::Crimson : archCol);
std::string code = data.code;
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
@ -222,9 +260,9 @@ void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player
if (isHover) hoveredImpIdx = s;
} else {
DrawRectangleRec(bRect, Color{ 8, 14, 22, 180 });
DrawRectangleLinesEx(bRect, 1.0f, Color{ 40, 60, 85, 180 });
DrawText("[--]", bX + 27, bY + 6, 11, DARKGRAY);
DrawRectangleRec(bRect, Color{ 10, 14, 20, 200 });
DrawRectangleLinesEx(bRect, 1.0f, TacticalColors::BorderMuted);
DrawText("[--]", bX + 27, bY + 6, 11, TacticalColors::MutedText);
}
}
@ -234,67 +272,83 @@ void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player
int tipY = impBarY + impBarH + 6;
int tipW = 330;
int tipH = 50;
DrawRectangle(tipX, tipY, tipW, tipH, Color{ 10, 16, 26, 250 });
DrawRectangleLines(tipX, tipY, tipW, tipH, CyberColors::NeonYellow);
DrawText(tipData.name.c_str(), tipX + 10, tipY + 8, 12, CyberColors::NeonYellow);
DrawText(tipData.description.c_str(), tipX + 10, tipY + 26, 10, RAYWHITE);
DrawRectangle(tipX, tipY, tipW, tipH, TacticalColors::DarkPanel);
DrawRectangleLines(tipX, tipY, tipW, tipH, TacticalColors::Crimson);
DrawText(tipData.name.c_str(), tipX + 10, tipY + 8, 12, TacticalColors::PureWhite);
DrawText(tipData.description.c_str(), tipX + 10, tipY + 26, 10, TacticalColors::SilverText);
}
// Top-Right: Holographic Minimap
// Top-Right: Tactical Minimap
drawMinimap(dungeon, player, GetScreenWidth() - 200, 20, 180);
// Bottom-Right: Revolver Ready Ammo preview
// Bottom-Right: Revolver Ready Ammo preview (Strict 0-rounding)
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);
DrawRectangle(revX, revY, 240, 90, TacticalColors::DarkPanel);
DrawRectangleLines(revX, revY, 240, 90, TacticalColors::BorderMuted);
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("REVOLVER READY", revX + 15, revY + 10, 12, TacticalColors::MutedText);
DrawText(("NEXT: " + nextInfo.shortName).c_str(), revX + 15, revY + 65, 12, GRAY);
DrawText("[TAB] ARMORY", revX + 140, revY + 65, 12, CyberColors::NeonYellow);
// Tactical ammo badge (Rectangular, 0-rounding)
DrawRectangle(revX + 15, revY + 34, 46, 22, TacticalColors::SurfacePlate);
DrawRectangleLines(revX + 15, revY + 34, 46, 22, TacticalColors::Crimson);
DrawText(curInfo.shortName.substr(0, 3).c_str(), revX + 21, revY + 39, 12, curInfo.color);
// Bottom-Center: Context Action Prompt
DrawText(curInfo.name.c_str(), revX + 68, revY + 37, 14, curInfo.color);
DrawText(("NEXT: [" + nextInfo.shortName + "]").c_str(), revX + 15, revY + 65, 12, TacticalColors::MutedText);
DrawText("[TAB] ARMORY", revX + 140, revY + 65, 12, TacticalColors::PureWhite);
// Bottom-Center: Context Action Prompt (Strict 0-rounding)
if (!prompt.empty()) {
int promptW = MeasureText(prompt.c_str(), 18) + 40;
int promptW = MeasureText(prompt.c_str(), 16) + 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);
DrawRectangle(promptX, promptY, promptW, 44, TacticalColors::DarkPanel);
DrawRectangleLines(promptX, promptY, promptW, 44, TacticalColors::Crimson);
DrawText(prompt.c_str(), promptX + 20, promptY + 14, 16, TacticalColors::PureWhite);
}
// 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 });
20, GetScreenHeight() - 35, 13, TacticalColors::MutedText);
}
void UIRenderer::drawHackingUI(const HackingGame& hacking) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 235 });
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 6, 10, 240 });
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);
DrawRectangle(panelX, panelY, panelW, panelH, TacticalColors::DarkPanel);
DrawRectangleLines(panelX, panelY, panelW, panelH, TacticalColors::BorderMuted);
// Header
DrawRectangle(panelX, panelY, panelW, 50, Color{ 14, 24, 42, 255 });
// Tactical corner bracket accents
DrawRectangle(panelX, panelY, 8, 2, TacticalColors::Crimson);
DrawRectangle(panelX, panelY, 2, 8, TacticalColors::Crimson);
DrawRectangle(panelX + panelW - 8, panelY, 8, 2, TacticalColors::Crimson);
DrawRectangle(panelX + panelW - 2, panelY, 2, 8, TacticalColors::Crimson);
DrawRectangle(panelX, panelY + panelH - 2, 8, 2, TacticalColors::Crimson);
DrawRectangle(panelX, panelY + panelH - 8, 2, 8, TacticalColors::Crimson);
DrawRectangle(panelX + panelW - 8, panelY + panelH - 2, 8, 2, TacticalColors::Crimson);
DrawRectangle(panelX + panelW - 2, panelY + panelH - 8, 2, 8, TacticalColors::Crimson);
// Header (Strict 0-rounding)
DrawRectangle(panelX, panelY, panelW, 50, TacticalColors::SurfacePlate);
DrawRectangleLines(panelX, panelY, panelW, 50, TacticalColors::BorderMuted);
DrawText(("SECURITY ICE BYPASS TERMINAL // DEFENSE LEVEL 0" + std::to_string(hacking.getSecurityLevel())).c_str(),
panelX + 25, panelY + 16, 18, CyberColors::NeonCyan);
panelX + 25, panelY + 16, 17, TacticalColors::PureWhite);
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);
DrawText("PHASE 1: FREQUENCY TUMBLER ALIGNMENT", panelX + 25, panelY + 65, 16, TacticalColors::PureWhite);
DrawText("Tune each pin into the sweet spot frequency band, then lock it into place.", panelX + 25, panelY + 90, 13, TacticalColors::SilverText);
const auto& tumbler = hacking.getTumbler();
int pinCount = tumbler.getPinCount();
@ -310,150 +364,194 @@ void UIRenderer::drawHackingUI(const HackingGame& hacking) {
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 });
DrawRectangle(px, trackY, pinW, trackH, Color{ 14, 18, 26, 255 });
DrawRectangleLines(px, trackY, pinW, trackH, isSelected ? TacticalColors::Crimson : TacticalColors::BorderMuted);
// 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);
DrawRectangle(px + 2, sweetY - sweetSpan / 2, pinW - 4, sweetSpan, Color{ 220, 230, 245, 45 });
DrawLine(px, sweetY, px + pinW, sweetY, TacticalColors::PureWhite);
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 });
Color pinColor = pin.locked ? TacticalColors::PureWhite :
(pin.isInSweetSpot() ? TacticalColors::Crimson : Color{ 50, 62, 80, 255 });
DrawRectangle(px + 4, pinCurrY, pinW - 8, pinH, pinColor);
DrawRectangleLines(px + 4, pinCurrY, pinW - 8, pinH, WHITE);
DrawRectangleLines(px + 4, pinCurrY, pinW - 8, pinH, TacticalColors::BorderWhite);
if (pin.locked) {
DrawText("LOCKED", px + 8, trackY + trackH + 12, 12, CyberColors::NeonGreen);
DrawText("LOCKED", px + 8, trackY + trackH + 12, 12, TacticalColors::PureWhite);
} else if (pin.isInSweetSpot()) {
DrawText("SWEET", px + 12, trackY + trackH + 12, 12, CyberColors::NeonYellow);
DrawText("SWEET", px + 12, trackY + trackH + 12, 12, TacticalColors::Crimson);
} else {
DrawText(std::to_string(i + 1).c_str(), px + 24, trackY + trackH + 12, 14, GRAY);
DrawText(std::to_string(i + 1).c_str(), px + 24, trackY + trackH + 12, 14, TacticalColors::MutedText);
}
}
DrawRectangle(panelX + 20, panelY + panelH - 65, panelW - 40, 45, Color{ 12, 18, 30, 255 });
DrawRectangle(panelX + 20, panelY + panelH - 65, panelW - 40, 45, TacticalColors::SurfacePlate);
DrawRectangleLines(panelX + 20, panelY + panelH - 65, panelW - 40, 45, TacticalColors::BorderMuted);
if (tumbler.isAllPinsLocked()) {
DrawText("[ENTER / SPACE] ALL PINS LOCKED! INITIATE DEAD-LOCK TIMING CHECK", panelX + 70, panelY + panelH - 52, 16, CyberColors::NeonGreen);
DrawText("[ENTER / SPACE] ALL PINS LOCKED! INITIATE DEAD-LOCK TIMING CHECK", panelX + 70, panelY + panelH - 52, 15, TacticalColors::PureWhite);
} else {
DrawText("[A/D] Select Pin [W/S] Tune Frequency [SPACE] Lock Pin", panelX + 130, panelY + panelH - 52, 14, CyberColors::NeonCyan);
DrawText("[A/D] Select Pin [W/S] Tune Frequency [SPACE] Lock Pin", panelX + 130, panelY + panelH - 52, 14, TacticalColors::SilverText);
}
} 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);
DrawText("PHASE 2: DEAD-LOCK TIMING INJECTION (CALIBRATION RAIL)", panelX + 25, panelY + 65, 16, TacticalColors::Crimson);
DrawText("STRIKE [SPACE] WHEN THE NEEDLE HITS THE TARGET WINDOW!", panelX + 25, panelY + 90, 14, TacticalColors::PureWhite);
const auto& sc = hacking.getSkillCheck();
int dialCenterX = panelX + panelW / 2;
int dialCenterY = panelY + panelH / 2 + 10;
float dialRadius = 130.0f;
int railW = 600;
int railH = 54;
int railX = panelX + (panelW - railW) / 2;
int railY = panelY + panelH / 2 - 20;
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 });
// Outer housing (Strict 0-rounding)
DrawRectangle(railX - 8, railY - 14, railW + 16, railH + 28, Color{ 14, 18, 26, 255 });
DrawRectangleLines(railX - 8, railY - 14, railW + 16, railH + 28, TacticalColors::BorderMuted);
// 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 });
// Rail background
DrawRectangle(railX, railY, railW, railH, Color{ 18, 24, 34, 255 });
DrawRectangleLines(railX, railY, railW, railH, TacticalColors::BorderMuted);
// Great success zone
DrawCircleSector({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) },
dialRadius,
sc.getGreatStart() - 90.0f,
sc.getGreatEnd() - 90.0f,
16, CyberColors::NeonGreen);
// Tick marks every 10%
for (int t = 0; t <= 10; ++t) {
int tx = railX + static_cast<int>(t * (railW / 10.0f));
DrawLine(tx, railY, tx, railY + 8, TacticalColors::BorderMuted);
DrawLine(tx, railY + railH - 8, tx, railY + railH, TacticalColors::BorderMuted);
}
// 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));
// Linear Target Window (Success Zone)
float succStartRatio = std::clamp(sc.getSuccessStart() / 360.0f, 0.0f, 1.0f);
float succEndRatio = std::clamp(sc.getSuccessEnd() / 360.0f, 0.0f, 1.0f);
int sX = railX + static_cast<int>(succStartRatio * railW);
int sW = static_cast<int>((succEndRatio - succStartRatio) * railW);
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);
DrawRectangle(sX, railY + 2, sW, railH - 4, Color{ 220, 230, 245, 50 });
DrawRectangleLines(sX, railY + 2, sW, railH - 4, TacticalColors::PureWhite);
DrawText("HIT [ SPACE ]", dialCenterX - 60, dialCenterY + static_cast<int>(dialRadius) + 30, 20, CyberColors::NeonYellow);
// Critical Hit Zone
float greatStartRatio = std::clamp(sc.getGreatStart() / 360.0f, 0.0f, 1.0f);
float greatEndRatio = std::clamp(sc.getGreatEnd() / 360.0f, 0.0f, 1.0f);
int gX = railX + static_cast<int>(greatStartRatio * railW);
int gW = std::max(8, static_cast<int>((greatEndRatio - greatStartRatio) * railW));
DrawRectangle(gX, railY + 2, gW, railH - 4, TacticalColors::Crimson);
DrawRectangleLines(gX, railY + 2, gW, railH - 4, TacticalColors::BrightRed);
DrawText("CRIT", gX + (gW - MeasureText("CRIT", 10)) / 2, railY + (railH - 10) / 2, 10, TacticalColors::PureWhite);
// Rotating Needle mapped linearly across the rail
float needleRatio = std::clamp(sc.getNeedleAngle() / 360.0f, 0.0f, 1.0f);
int curX = railX + static_cast<int>(needleRatio * railW);
// Vertical reticle cursor (Strict 0-rounding)
DrawLineEx({ static_cast<float>(curX), static_cast<float>(railY - 8) },
{ static_cast<float>(curX), static_cast<float>(railY + railH + 8) },
3.0f, TacticalColors::BrightRed);
DrawTriangle({ (float)curX, (float)(railY - 2) },
{ (float)(curX - 7), (float)(railY - 12) },
{ (float)(curX + 7), (float)(railY - 12) },
TacticalColors::BrightRed);
DrawTriangle({ (float)curX, (float)(railY + railH + 2) },
{ (float)(curX + 7), (float)(railY + railH + 12) },
{ (float)(curX - 7), (float)(railY + railH + 12) },
TacticalColors::BrightRed);
// Range telemetry labels
DrawText("[ 0.00 MS ]", railX, railY - 26, 11, TacticalColors::MutedText);
DrawText("[ TARGET WINDOW ]", sX, railY - 26, 11, TacticalColors::PureWhite);
DrawText("[ 1000 MS ]", railX + railW - MeasureText("[ 1000 MS ]", 11), railY - 26, 11, TacticalColors::MutedText);
// Action Prompt Box
int promptBoxW = 280;
int promptBoxH = 42;
int promptBoxX = panelX + (panelW - promptBoxW) / 2;
int promptBoxY = railY + railH + 34;
DrawRectangle(promptBoxX, promptBoxY, promptBoxW, promptBoxH, TacticalColors::SurfacePlate);
DrawRectangleLines(promptBoxX, promptBoxY, promptBoxW, promptBoxH, TacticalColors::Crimson);
DrawText("STRIKE [ SPACE ] OVERRIDE", promptBoxX + 24, promptBoxY + 12, 16, TacticalColors::PureWhite);
} 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);
DrawText(">>> CRITICAL OVERRIDE SUCCESS! <<<", cx - 210, cy - 40, 24, TacticalColors::Crimson);
DrawText("Subroutines penetrated. Bonus cyber-cartridge recovered!", cx - 200, cy + 10, 16, TacticalColors::PureWhite);
} 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);
DrawText(">>> ACCESS GRANTED <<<", cx - 140, cy - 40, 26, TacticalColors::PureWhite);
DrawText("Security locks bypassed successfully.", cx - 150, cy + 10, 16, TacticalColors::SilverText);
} 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);
DrawText("!!! ICE ALARM TRIGGERED !!!", cx - 180, cy - 40, 26, TacticalColors::BrightRed);
DrawText("Intrusion countermeasures activated. Defense Sentinel deployed!", cx - 240, cy + 10, 16, TacticalColors::Crimson);
}
}
}
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 chamberOrbitR = 56.0f;
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);
// 1. Faceted Octagonal Heavy Outer Housing (Strict 0-rounding)
DrawRectangle(centerX - 100, centerY - 100, 200, 200, TacticalColors::DarkPanel);
DrawRectangleLines(centerX - 100, centerY - 100, 200, 200, TacticalColors::BorderMuted);
// 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;
// 45-degree chamfers on outer corners
DrawTriangle({ (float)(centerX - 100), (float)(centerY - 100) },
{ (float)(centerX - 72), (float)(centerY - 100) },
{ (float)(centerX - 100), (float)(centerY - 72) }, TacticalColors::PitchBlack);
DrawTriangle({ (float)(centerX + 100), (float)(centerY - 100) },
{ (float)(centerX + 72), (float)(centerY - 100) },
{ (float)(centerX + 100), (float)(centerY - 72) }, TacticalColors::PitchBlack);
DrawTriangle({ (float)(centerX - 100), (float)(centerY + 100) },
{ (float)(centerX - 72), (float)(centerY + 100) },
{ (float)(centerX - 100), (float)(centerY + 72) }, TacticalColors::PitchBlack);
DrawTriangle({ (float)(centerX + 100), (float)(centerY + 100) },
{ (float)(centerX + 72), (float)(centerY + 100) },
{ (float)(centerX + 100), (float)(centerY + 72) }, TacticalColors::PitchBlack);
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);
// 2. Inner Chamfered Drum Plate
DrawRectangle(centerX - 88, centerY - 88, 176, 176, Color{ 16, 22, 32, 255 });
DrawRectangleLines(centerX - 88, centerY - 88, 176, 176, TacticalColors::BorderWhite);
DrawTriangle({ (float)(centerX - 88), (float)(centerY - 88) },
{ (float)(centerX - 64), (float)(centerY - 88) },
{ (float)(centerX - 88), (float)(centerY - 64) }, TacticalColors::DarkPanel);
DrawTriangle({ (float)(centerX + 88), (float)(centerY - 88) },
{ (float)(centerX + 64), (float)(centerY - 88) },
{ (float)(centerX + 88), (float)(centerY - 64) }, TacticalColors::DarkPanel);
DrawTriangle({ (float)(centerX - 88), (float)(centerY + 88) },
{ (float)(centerX - 64), (float)(centerY + 88) },
{ (float)(centerX - 88), (float)(centerY + 64) }, TacticalColors::DarkPanel);
DrawTriangle({ (float)(centerX + 88), (float)(centerY + 88) },
{ (float)(centerX + 64), (float)(centerY + 88) },
{ (float)(centerX + 88), (float)(centerY + 64) }, TacticalColors::DarkPanel);
// 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);
// 3. Center Hub & Caliber Stamp
DrawRectangle(centerX - 24, centerY - 24, 48, 48, Color{ 12, 16, 24, 255 });
DrawRectangleLines(centerX - 24, centerY - 24, 48, 48, TacticalColors::BorderMuted);
DrawRectangle(centerX - 14, centerY - 14, 28, 28, TacticalColors::SurfacePlate);
DrawRectangleLines(centerX - 14, centerY - 14, 28, 28, TacticalColors::Crimson);
DrawText(".357", centerX - 14, centerY - 6, 12, TacticalColors::PureWhite);
// 4. Six Chambers (Top chamber at 12:00 aligns with striker when visualAngle == index * 60)
// 4. Aggressive Red Striker Firing Pin at 12:00 (-90 deg / top)
int strikerW = 54;
int strikerH = 16;
int strikerX = centerX - strikerW / 2;
int strikerY = centerY - 100 - strikerH - 6;
DrawRectangle(strikerX, strikerY, strikerW, strikerH, TacticalColors::DarkRed);
DrawRectangleLines(strikerX, strikerY, strikerW, strikerH, TacticalColors::BrightRed);
DrawTriangle({ (float)centerX, (float)(centerY - 96) },
{ (float)(centerX - 8), (float)(strikerY + strikerH) },
{ (float)(centerX + 8), (float)(strikerY + strikerH) },
TacticalColors::BrightRed);
const char* pinTxt = "[ 12:00 FIRING PIN ]";
int pinW = MeasureText(pinTxt, 11);
DrawText(pinTxt, centerX - pinW / 2, strikerY - 18, 11, TacticalColors::BrightRed);
// 5. Six Hard-Surface Chambers (Strict 0-rounding)
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);
@ -463,28 +561,30 @@ void UIRenderer::drawRevolverCylinderCombat(const Revolver& revolver, int center
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 });
int chW = 34;
int chH = 34;
Rectangle chRect = { (float)(chX - chW / 2), (float)(chY - chH / 2), (float)chW, (float)chH };
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);
DrawRectangleRec(chRect, TacticalColors::PitchBlack);
DrawRectangleLinesEx(chRect, isCurrent ? 2.0f : 1.0f, isCurrent ? TacticalColors::BrightRed : TacticalColors::BorderMuted);
DrawText("---", chX - 9, chY - 5, 10, TacticalColors::MutedText);
} 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);
DrawRectangleRec(chRect, TacticalColors::DarkPanel);
DrawRectangleLinesEx(chRect, isCurrent ? 2.0f : 1.0f, isCurrent ? TacticalColors::BrightRed : info.color);
// Tactical cartridge primer block
DrawRectangle(chX - 7, chY - 7, 14, 14, Color{ info.color.r, info.color.g, info.color.b, 200 });
DrawRectangleLines(chX - 7, chY - 7, 14, 14, info.color);
DrawText(info.shortName.substr(0, 3).c_str(), chX - 10, chY - 5, 10, TacticalColors::PureWhite);
}
// Active Chamber Bracket (12:00 lock)
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);
DrawRectangleLinesEx({ chRect.x - 4, chRect.y - 4, chRect.width + 8, chRect.height + 8 }, 2.0f, TacticalColors::BrightRed);
DrawLine(chX, (int)chRect.y - 8, chX, (int)chRect.y - 3, TacticalColors::BrightRed);
DrawLine(chX, (int)(chRect.y + chRect.height + 3), chX, (int)(chRect.y + chRect.height + 8), TacticalColors::BrightRed);
DrawLine((int)chRect.x - 8, chY, (int)chRect.x - 3, chY, TacticalColors::BrightRed);
DrawLine((int)(chRect.x + chRect.width + 3), chY, (int)(chRect.x + chRect.width + 8), chY, TacticalColors::BrightRed);
}
}
}
@ -532,18 +632,19 @@ void UIRenderer::drawCodexModal() {
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);
DrawRectangle(leftX, cardY, 455, 46, TacticalColors::SurfacePlate);
DrawRectangleLines(leftX, cardY, 455, 46, (ammos[i] == AmmoType::EMPTY) ? TacticalColors::BorderMuted : TacticalColors::BorderWhite);
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);
DrawRectangle(leftX + 10, cardY + 12, 40, 22, TacticalColors::DarkPanel);
DrawRectangleLines(leftX + 10, cardY + 12, 40, 22, (ammos[i] == AmmoType::EMPTY) ? TacticalColors::BorderMuted : TacticalColors::Crimson);
DrawText(inf.shortName.substr(0, 3).c_str(), leftX + 15, cardY + 17, 11, inf.color);
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);
DrawText(title.c_str(), leftX + 58, cardY + 8, 13, inf.color);
DrawText(inf.description.c_str(), leftX + 58, cardY + 25, 11, TacticalColors::SilverText);
cardY += 52;
}
@ -648,15 +749,15 @@ void UIRenderer::drawCombatUI(const CombatSystem& combat, const Player& player,
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);
DrawText("WEAKNESS:", scanX + 160, scanY + 112, 12, TacticalColors::MutedText);
DrawRectangle(scanX + 235, scanY + 107, 240, 24, Color{ 14, 18, 26, 240 });
DrawRectangleLines(scanX + 235, scanY + 107, 240, 24, wknInfo.color);
std::string wknText = "[" + wknInfo.shortName + "] -> +100% CRIT SURGE!";
DrawText(wknText.c_str(), scanX + 242, scanY + 112, 12, wknInfo.color);
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;
Color badgeCol = (surgePulse > 0.5f) ? TacticalColors::Crimson : wknInfo.color;
DrawText("[SURGE PRIMED!]", scanX + 482, scanY + 112, 12, badgeCol);
}
@ -862,13 +963,13 @@ void UIRenderer::drawCombatUI(const CombatSystem& combat, const Player& player,
int advW = 370;
int advH = 46;
DrawRectangle(advX, advY, advW, advH, Color{ 8, 16, 30, 240 });
DrawRectangleLines(advX, advY, advW, advH, CyberColors::NeonCyan);
DrawRectangle(advX, advY, advW, advH, TacticalColors::DarkPanel);
DrawRectangleLines(advX, advY, advW, advH, TacticalColors::BorderMuted);
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);
DrawRectangle(advX + 14, advY + 16, 10, 10, (aiPulse > 0.4f) ? TacticalColors::Crimson : TacticalColors::DarkRed);
DrawRectangleLines(advX + 14, advY + 16, 10, 10, TacticalColors::BrightRed);
DrawText("TACTICAL INTEL", advX + 32, advY + 8, 10, TacticalColors::Crimson);
std::string advisorMsg = "";
if (rev.getCurrentAmmo() == enemy->getWeakness()) {
@ -1144,22 +1245,23 @@ void UIRenderer::drawWorkbenchModal(const Player& player) {
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);
// Card container (Strict 0-rounding)
DrawRectangle(cardX, cardY, cardW, cardH, TacticalColors::SurfacePlate);
DrawRectangleLines(cardX, cardY, cardW, cardH, isHoverBtn ? TacticalColors::Crimson : TacticalColors::BorderMuted);
// Header inside card: Ammo icon & Name
DrawCircle(cardX + 22, cardY + 24, 10.0f, info.color);
DrawCircleLines(cardX + 22, cardY + 24, 10.0f, WHITE);
// Header inside card: Ammo icon badge & Name
DrawRectangle(cardX + 12, cardY + 14, 38, 20, TacticalColors::DarkPanel);
DrawRectangleLines(cardX + 12, cardY + 14, 38, 20, TacticalColors::Crimson);
DrawText(info.shortName.substr(0, 3).c_str(), cardX + 17, cardY + 18, 11, info.color);
std::string nameTxt = info.name + " [" + info.shortName + "]";
DrawText(nameTxt.c_str(), cardX + 40, cardY + 15, 13, info.color);
DrawText(nameTxt.c_str(), cardX + 58, cardY + 16, 13, TacticalColors::PureWhite);
// Tier badge
DrawText(curTierData.tierBadge.c_str(), cardX + cardW - 75, cardY + 15, 13, Color{ 255, 215, 60, 255 });
DrawText(curTierData.tierBadge.c_str(), cardX + cardW - 75, cardY + 16, 13, TacticalColors::PureWhite);
// Tier Title (e.g. TIER I: BASE SPEC)
DrawText(curTierData.tierTitle.c_str(), cardX + 16, cardY + 44, 12, Color{ 220, 190, 120, 255 });
DrawText(curTierData.tierTitle.c_str(), cardX + 16, cardY + 44, 12, TacticalColors::SilverText);
// Damage display
int effDmg = player.getEffectiveDamage(ammo);
@ -1175,17 +1277,8 @@ void UIRenderer::drawWorkbenchModal(const Player& player) {
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);
}
DrawText("PERK:", cardX + 18, cardY + 90, 10, TacticalColors::MutedText);
drawWrappedText(curTierData.perkDesc, cardX + 18, cardY + 104, cardW - 36, 11, 3, TacticalColors::SilverText);
// Bottom Action Button / Status
if (curTier >= 3) {
@ -1328,17 +1421,8 @@ void UIRenderer::drawImplantDraftModal(const std::vector<ImplantType>& choices,
DrawLine(cardX + 12, cardY + 68, cardX + cardW - 12, cardY + 68, Color{ 35, 55, 80, 255 });
// Description text wrapped into lines
std::string desc = data.description;
size_t breakPos = desc.find(". ");
if (breakPos != std::string::npos && breakPos < 35) {
std::string line1 = desc.substr(0, breakPos + 1);
std::string line2 = desc.substr(breakPos + 2);
DrawText(line1.c_str(), cardX + 18, cardY + 78, 12, archCol);
DrawText(line2.c_str(), cardX + 18, cardY + 98, 11, RAYWHITE);
} else {
DrawText(desc.c_str(), cardX + 18, cardY + 78, 11, RAYWHITE);
}
// Description text cleanly wrapped into card boundaries (Fixes Screen 1 overflow)
drawWrappedText(data.description, cardX + 18, cardY + 76, cardW - 36, 12, 4, TacticalColors::SilverText);
// Install / Replace button
Color btnBg = isHoverBtn ? Color{ 20, 55, 75, 255 } : Color{ 14, 35, 50, 255 };

View file

@ -18,6 +18,7 @@ public:
void drawWorkbenchModal(const Player& player);
static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH);
static Rectangle getWorkbenchCloseButtonRect(int screenW, int screenH);
static void drawWrappedText(const std::string& text, int posX, int posY, int maxW, int fontSize, int lineSpacing, Color color);
void drawImplantDraftModal(const std::vector<ImplantType>& choices, const Player& player, int selectedReplaceSlot = -1);
static Rectangle getDraftCardButtonRect(int cardIndex, int screenW, int screenH);