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

View file

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

View file

@ -56,6 +56,9 @@ public:
void addLog(const std::string& msg); void addLog(const std::string& msg);
void addFloatingText(const std::string& text, Vector2 pos, Color color); void addFloatingText(const std::string& text, Vector2 pos, Color color);
AmmoType getLastFiredAmmo() const { return m_lastFiredAmmo; }
float getPhaseTimer() const { return m_phaseTimer; }
private: private:
void resolveShotImpact(Player& player); void resolveShotImpact(Player& player);
void resolveBurstShot(Player& player, int shotNum); void resolveBurstShot(Player& player, int shotNum);
@ -74,6 +77,7 @@ private:
bool m_shieldBoosted = false; bool m_shieldBoosted = false;
bool m_ghostBonusDmg = false; bool m_ghostBonusDmg = false;
bool m_defeatedEnemyWasElite = false; bool m_defeatedEnemyWasElite = false;
AmmoType m_lastFiredAmmo = AmmoType::EMPTY;
std::vector<std::string> m_combatLog; std::vector<std::string> m_combatLog;
std::vector<FloatingText> m_floatingTexts; 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_baseDamage = 12 + level * 3;
m_attackElement = AmmoType::OVERHEAT; m_attackElement = AmmoType::OVERHEAT;
m_weakness = AmmoType::EMP; m_weakness = AmmoType::EMP;
m_color = CyberColors::NeonOrange; m_color = TacticalColors::PureWhite;
break; break;
case EnemyType::ICE_SENTINEL: 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_baseDamage = 14 + level * 3;
m_attackElement = AmmoType::EMP; m_attackElement = AmmoType::EMP;
m_weakness = AmmoType::CORROSIVE; m_weakness = AmmoType::CORROSIVE;
m_color = CyberColors::NeonCyan; m_color = TacticalColors::Crimson;
break; break;
case EnemyType::GLITCH_WRAITH: 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_baseDamage = 18 + level * 4;
m_attackElement = AmmoType::GLITCH; m_attackElement = AmmoType::GLITCH;
m_weakness = AmmoType::KINETIC; m_weakness = AmmoType::KINETIC;
m_color = CyberColors::NeonPurple; m_color = TacticalColors::BrightRed;
break; break;
case EnemyType::VIRUS_CONSTRUCT: 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_baseDamage = 15 + level * 3;
m_attackElement = AmmoType::CORROSIVE; m_attackElement = AmmoType::CORROSIVE;
m_weakness = AmmoType::OVERHEAT; m_weakness = AmmoType::OVERHEAT;
m_color = CyberColors::NeonGreen; m_color = TacticalColors::DarkRed;
break; break;
} }

View file

@ -44,13 +44,13 @@ struct AmmoInfo {
}; };
inline const AmmoInfo& getAmmoInfo(AmmoType type) { 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 infoEmpty { AmmoType::EMPTY, "Empty Shell", "---", "Empty chamber - no round.", { 50, 60, 75, 255 }, 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 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.", { 0, 220, 255, 255 }, 20, 0.70f }; 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", "THERM", "Thermal incendiary round. Ignites target CPU for 3 turns.", { 255, 80, 20, 255 }, 18, 1.0f }; 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", "ACID", "Sub-routine dissolver. Destroys armor, targets take +50% dmg.", { 120, 255, 0, 255 }, 15, 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", "GLITCH", "Corrupting code payload. Drops enemy accuracy drastically.", { 220, 50, 255, 255 }, 22, 0.80f }; 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", "NANITE", "Vampiric micro-drones. Deals damage & restores Player HP/Shield.", { 0, 255, 180, 255 }, 16, 1.0f }; 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) { switch (type) {
case AmmoType::KINETIC: return infoKinetic; case AmmoType::KINETIC: return infoKinetic;
@ -246,58 +246,94 @@ struct ImplantData {
std::string code; 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) { inline ImplantData getImplantData(ImplantType type) {
switch (type) { switch (type) {
case ImplantType::GHOST_CHAMBER: case ImplantType::GHOST_CHAMBER:
return { type, "GHOST CHAMBER", "ROULETTE", ImplantArchetype::CYLINDER, return { type, "GHOST CHAMBER", "ROULETTE", ImplantArchetype::CYLINDER,
"Empty chamber gives +20 Shield and +50% damage on next shot.", "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: case ImplantType::ADRENALINE_DRUM:
return { type, "ADRENALINE DRUM", "ROULETTE", ImplantArchetype::CYLINDER, return { type, "ADRENALINE DRUM", "ROULETTE", ImplantArchetype::CYLINDER,
"Killing a drone instantly loads +1 random ammo & grants a free turn.", "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: case ImplantType::HEAVY_HAMMER:
return { type, "HEAVY HAMMER", "ROULETTE", ImplantArchetype::CYLINDER, return { type, "HEAVY HAMMER", "ROULETTE", ImplantArchetype::CYLINDER,
"Single shots deal +30% damage. Triple burst cooldown reduced to 1 turn.", "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: case ImplantType::CATALYST_MATRIX:
return { type, "CATALYST MATRIX", "ELEMENTAL", ImplantArchetype::ELEMENTAL, return { type, "CATALYST MATRIX", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
"Attacking Burned or Corroded foes detonates for +25 bonus direct damage.", "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: case ImplantType::CONDUCTIVE_SALVO:
return { type, "CONDUCTIVE SALVO", "ELEMENTAL", ImplantArchetype::ELEMENTAL, return { type, "CONDUCTIVE SALVO", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
"EMP shots deal damage through enemy shields; Stun duration +1 turn.", "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: case ImplantType::CORROSIVE_LEECH:
return { type, "CORROSIVE LEECH", "ELEMENTAL", ImplantArchetype::ELEMENTAL, return { type, "CORROSIVE LEECH", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
"Nanite hits on Corroded targets restore 100% more HP & Shield.", "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: case ImplantType::DEFLECTOR_FEEDBACK:
return { type, "DEFLECTOR FEEDBACK", "DEFENSE", ImplantArchetype::DEFENSE, return { type, "DEFLECTOR FEEDBACK", "DEFENSE", ImplantArchetype::DEFENSE,
"When Shield breaks, a shock pulse deals 20 retaliatory damage to drone.", "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: case ImplantType::AUTO_DEFIBRILLATOR:
return { type, "AUTO-DEFIBRILLATOR", "DEFENSE", ImplantArchetype::DEFENSE, return { type, "AUTO-DEFIBRILLATOR", "DEFENSE", ImplantArchetype::DEFENSE,
"Once per run, lethal damage is prevented: grants 1 HP, 50 Shield, and stuns.", "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: case ImplantType::NANITE_WEAVER:
return { type, "NANITE WEAVER", "DEFENSE", ImplantArchetype::DEFENSE, return { type, "NANITE WEAVER", "DEFENSE", ImplantArchetype::DEFENSE,
"Start of turn in combat: restore +5 HP and +8 Shield if enemy has debuffs.", "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: case ImplantType::LOGIC_OVERCLOCK:
return { type, "LOGIC OVERCLOCK", "HACKER", ImplantArchetype::HACKER, return { type, "LOGIC OVERCLOCK", "HACKER", ImplantArchetype::HACKER,
"Skill check Great zone is 2x wider; Critical hacking awards 2x Core Chips.", "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: case ImplantType::SALVAGE_PROTOCOL:
return { type, "SALVAGE PROTOCOL", "HACKER", ImplantArchetype::HACKER, return { type, "SALVAGE PROTOCOL", "HACKER", ImplantArchetype::HACKER,
"+50% Core Chips dropped from all sources; 20% ammo chance on door bypass.", "+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: case ImplantType::PRECOGNITION_OPTICS:
return { type, "PRECOGNITION OPTICS", "HACKER", ImplantArchetype::HACKER, return { type, "PRECOGNITION OPTICS", "HACKER", ImplantArchetype::HACKER,
"Scans 5 tiles ahead, revealing Elites, Mines, Lasers & Caches on minimap.", "Scans 5 tiles ahead, revealing Elites, Mines, Lasers & Caches on minimap.",
Color{ 120, 240, 255, 255 }, "[OPTIC]", "OPTIC" }; TacticalColors::SilverText, "[OPTIC]", "OPTIC" };
default: 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; 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 } // namespace dnr

View file

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

View file

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

View file

@ -1,4 +1,5 @@
#include "InventoryUI.hpp" #include "InventoryUI.hpp"
#include "render/UIRenderer.hpp"
#include "core/SoundManager.hpp" #include "core/SoundManager.hpp"
#include <cmath> #include <cmath>
@ -117,65 +118,79 @@ void InventoryUI::draw(const Player& player) {
Vector2 mouse = GetMousePosition(); Vector2 mouse = GetMousePosition();
// Dark semi-transparent cyber overlay // 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 // Header
DrawRectangle(80, 35, GetScreenWidth() - 160, 52, Color{ 12, 22, 38, 255 }); DrawRectangle(80, 35, GetScreenWidth() - 160, 52, TacticalColors::DarkPanel);
DrawRectangleLines(80, 35, GetScreenWidth() - 160, 52, CyberColors::NeonCyan); DrawRectangleLines(80, 35, GetScreenWidth() - 160, 52, TacticalColors::BorderWhite);
DrawText("CYBER-WEAPON ARMORY // CYLINDER CONFIGURATION", 105, 50, 20, CyberColors::NeonCyan); DrawText("CYBER-WEAPON ARMORY // CYLINDER CONFIGURATION", 105, 50, 20, TacticalColors::PureWhite);
DrawText("[TAB / ESC] RETURN TO GAME", GetScreenWidth() - 340, 51, 16, CyberColors::NeonYellow); 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 cylCenterX = 350;
int cylCenterY = 320; int cylCenterY = 320;
float cylRadius = 115.0f; float chamberOrbitR = 58.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 visAngle = rev.getVisualAngle(); 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 // 1. Faceted Octagonal Heavy Outer Housing
DrawCircle(cylCenterX, cylCenterY, 36.0f, Color{ 14, 20, 32, 255 }); DrawRectangle(cylCenterX - 110, cylCenterY - 110, 220, 220, TacticalColors::DarkPanel);
DrawCircleLines(cylCenterX, cylCenterY, 36.0f, Color{ 70, 100, 140, 255 }); DrawRectangleLines(cylCenterX - 110, cylCenterY - 110, 220, 220, TacticalColors::BorderMuted);
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);
// 3. Golden Striker Hammer / Firing Pin at 12:00 (-90 deg) // 45-degree chamfers on outer corners
int strikerTipY = cylCenterY - static_cast<int>(cylRadius) - 4; DrawTriangle({ (float)(cylCenterX - 110), (float)(cylCenterY - 110) },
int strikerBaseY = cylCenterY - static_cast<int>(cylRadius) - 28; { (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( // 2. Inner Chamfered Drum Plate
{ static_cast<float>(cylCenterX), static_cast<float>(strikerTipY) }, DrawRectangle(cylCenterX - 96, cylCenterY - 96, 192, 192, Color{ 16, 22, 32, 255 });
{ static_cast<float>(cylCenterX - 14), static_cast<float>(strikerBaseY) }, DrawRectangleLines(cylCenterX - 96, cylCenterY - 96, 192, 192, TacticalColors::BorderWhite);
{ static_cast<float>(cylCenterX + 14), static_cast<float>(strikerBaseY) }, DrawTriangle({ (float)(cylCenterX - 96), (float)(cylCenterY - 96) },
Color{ 255, 210, 50, 255 } { (float)(cylCenterX - 70), (float)(cylCenterY - 96) },
); { (float)(cylCenterX - 96), (float)(cylCenterY - 70) }, TacticalColors::DarkPanel);
DrawRectangle(cylCenterX - 20, strikerBaseY - 14, 40, 14, Color{ 210, 160, 30, 255 }); DrawTriangle({ (float)(cylCenterX + 96), (float)(cylCenterY - 96) },
DrawRectangleLines(cylCenterX - 20, strikerBaseY - 14, 40, 14, CyberColors::NeonYellow); { (float)(cylCenterX + 70), (float)(cylCenterY - 96) },
DrawCircle(cylCenterX, strikerTipY, 3.0f, CyberColors::NeonYellow); { (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 }); // 3. Center Extractor Ratchet & Hub
DrawRectangleLines(cylCenterX - 65, strikerBaseY - 32, 130, 16, Color{ 180, 150, 40, 200 }); DrawRectangle(cylCenterX - 26, cylCenterY - 26, 52, 52, Color{ 12, 16, 24, 255 });
DrawText("FIRING PIN [12:00]", cylCenterX - 58, strikerBaseY - 30, 11, CyberColors::NeonYellow); 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) { for (int i = 0; i < 6; ++i) {
float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f); float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f);
int chX = cylCenterX + static_cast<int>(std::cos(angleRad) * chamberOrbitR); 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 isSelected = (i == m_selectedChamber);
bool isCurrentReady = (i == rev.getCurrentIndex()); bool isCurrentReady = (i == rev.getCurrentIndex());
DrawCircle(chX, chY, chamberR, Color{ 16, 22, 34, 255 }); int chW = 36;
DrawCircleLines(chX, chY, chamberR, isSelected ? CyberColors::NeonYellow : Color{ 55, 80, 120, 255 }); int chH = 36;
Rectangle chRect = { (float)(chX - chW / 2), (float)(chY - chH / 2), (float)chW, (float)chH };
if (ammo == AmmoType::EMPTY) { if (ammo == AmmoType::EMPTY) {
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 8, 12, 18, 255 }); DrawRectangleRec(chRect, TacticalColors::PitchBlack);
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 60, 60, 60, 255 }); DrawRectangleLinesEx(chRect, isSelected ? 2.0f : 1.0f, isSelected ? TacticalColors::Crimson : (isCurrentReady ? TacticalColors::BorderWhite : TacticalColors::BorderMuted));
DrawText("0", chX - 4, chY - 6, 12, DARKGRAY); DrawText("---", chX - 9, chY - 5, 10, TacticalColors::MutedText);
} else { } else {
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 34, 46, 62, 255 }); DrawRectangleRec(chRect, TacticalColors::DarkPanel);
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 180, 140, 50, 255 }); DrawRectangleLinesEx(chRect, isSelected ? 2.0f : 1.0f, isSelected ? TacticalColors::Crimson : info.color);
DrawCircle(chX, chY, chamberR - 8.0f, info.color); // Tactical cartridge primer block
DrawCircle(chX - 4, chY - 4, 3.0f, Color{ 255, 255, 255, 200 }); DrawRectangle(chX - 7, chY - 7, 14, 14, Color{ info.color.r, info.color.g, info.color.b, 200 });
DrawText(info.shortName.substr(0, 3).c_str(), chX - 9, chY - 5, 11, BLACK); 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) { if (isSelected) {
DrawRectangleLines(chX - static_cast<int>(chamberR) - 4, chY - static_cast<int>(chamberR) - 4, DrawRectangleLinesEx({ chRect.x - 4, chRect.y - 4, chRect.width + 8, chRect.height + 8 }, 2.0f, TacticalColors::Crimson);
static_cast<int>(chamberR * 2) + 8, static_cast<int>(chamberR * 2) + 8, DrawLine(chX, (int)chRect.y - 8, chX, (int)chRect.y - 3, TacticalColors::Crimson);
CyberColors::NeonYellow); 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) // Active Chamber Marker (Under firing pin)
if (isCurrentReady) { if (isCurrentReady) {
DrawCircleLines(chX, chY, chamberR + 6.0f, CyberColors::NeonGreen); DrawText("READY", chX - 16, chY + 24, 10, TacticalColors::PureWhite);
DrawText("READY", chX - 16, chY + 28, 10, CyberColors::NeonGreen);
} }
} }
// Selected chamber info box below cylinder // Selected chamber info box below cylinder (Strict 0-rounding)
int boxX = 110; int boxX = 110;
int boxY = 480; int boxY = 480;
int boxW = 480; int boxW = 480;
int boxH = 100; int boxH = 100;
DrawRectangle(boxX, boxY, boxW, boxH, Color{ 10, 18, 30, 255 }); DrawRectangle(boxX, boxY, boxW, boxH, TacticalColors::DarkPanel);
DrawRectangleLines(boxX, boxY, boxW, boxH, CyberColors::NeonCyan); DrawRectangleLines(boxX, boxY, boxW, boxH, TacticalColors::BorderMuted);
AmmoType selChamberAmmo = rev.getChamber(m_selectedChamber); AmmoType selChamberAmmo = rev.getChamber(m_selectedChamber);
const auto& selChamberInfo = getAmmoInfo(selChamberAmmo); const auto& selChamberInfo = getAmmoInfo(selChamberAmmo);
@ -230,17 +248,17 @@ void InventoryUI::draw(const Player& player) {
if (m_selectedChamber == rev.getCurrentIndex()) { if (m_selectedChamber == rev.getCurrentIndex()) {
selHeader += " [ACTIVE @ 12:00 FIRING PIN]"; 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(("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(("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, CyberColors::NeonCyan); DrawText("[1-6 / CLICK] SELECT | [U] UNLOAD | [R] SPIN ROULETTE", boxX + 15, boxY + 78, 11, TacticalColors::MutedText);
// Ammo Probability Breakdown under info box // Ammo Probability Breakdown under info box
int oddsY = boxY + boxH + 12; int oddsY = boxY + boxH + 12;
int oddsH = 65; int oddsH = 65;
DrawRectangle(boxX, oddsY, boxW, oddsH, Color{ 8, 14, 24, 255 }); DrawRectangle(boxX, oddsY, boxW, oddsH, TacticalColors::DarkPanel);
DrawRectangleLines(boxX, oddsY, boxW, oddsH, Color{ 35, 60, 95, 255 }); DrawRectangleLines(boxX, oddsY, boxW, oddsH, TacticalColors::BorderMuted);
DrawText("// AMMO PROBABILITY ROULETTE", boxX + 15, oddsY + 8, 10, GRAY); DrawText("// AMMO PROBABILITY ROULETTE", boxX + 15, oddsY + 8, 10, TacticalColors::MutedText);
int loadedCount = 0; int loadedCount = 0;
int typeCounts[7] = { 0 }; int typeCounts[7] = { 0 };
@ -253,10 +271,10 @@ void InventoryUI::draw(const Player& player) {
int blankChance = 100 - hitChance; int blankChance = 100 - hitChance;
std::string oddsStr = "LOADED: " + std::to_string(loadedCount) + "/6 (" + std::to_string(hitChance) + "% HIT)"; 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) + "%"; 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 = ""; std::string chipSummary = "";
for (int t = 1; t <= 6; ++t) { 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"; 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 // Right Panel: Ammo Pouch / Storage + Neural Implants Matrix
int pouchX = 660; int pouchX = 660;
@ -275,12 +293,12 @@ void InventoryUI::draw(const Player& player) {
int pouchW = 520; int pouchW = 520;
int pouchH = 580; int pouchH = 580;
DrawRectangle(pouchX, pouchY, pouchW, pouchH, Color{ 10, 16, 26, 255 }); DrawRectangle(pouchX, pouchY, pouchW, pouchH, TacticalColors::DarkPanel);
DrawRectangleLines(pouchX, pouchY, pouchW, pouchH, CyberColors::NeonCyan); DrawRectangleLines(pouchX, pouchY, pouchW, pouchH, TacticalColors::BorderWhite);
DrawText("TACTICAL CARTRIDGE STACK // INVENTORY", pouchX + 20, pouchY + 12, 16, CyberColors::NeonCyan); 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(), 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; int startY = pouchY + 58;
for (int i = 0; i < 6; ++i) { 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 }; 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); 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 boxCol = isSel ? TacticalColors::SurfacePlate : (isHover ? Color{ 24, 24, 24, 255 } : TacticalColors::PitchBlack);
Color borderCol = isSel ? CyberColors::NeonYellow : (isHover ? CyberColors::NeonCyan : Color{ 30, 50, 75, 255 }); Color borderCol = isSel ? TacticalColors::Crimson : (isHover ? TacticalColors::BorderWhite : TacticalColors::BorderMuted);
DrawRectangleRec(rowRect, boxCol); DrawRectangleRec(rowRect, boxCol);
DrawRectangleLinesEx(rowRect, isSel ? 2.0f : 1.0f, borderCol); DrawRectangleLinesEx(rowRect, isSel ? 2.0f : 1.0f, borderCol);
// Icon // Tactical rectangular badge (Strict 0-rounding)
DrawCircle(pouchX + 32, startY + i * 42 + 19, 10.0f, count > 0 ? itemInfo.color : DARKGRAY); 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 // 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 // Tier badge
int tier = player.getAmmoTier(aType); int tier = player.getAmmoTier(aType);
std::string tStr = tier == 1 ? "T1" : (tier == 2 ? "T2" : "T3"); 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 // 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 // Stack Count
std::string countStr = "[x " + std::to_string(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); 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 detailY = startY + 6 * 42 + 4;
int detailH = 64; int detailH = 64;
DrawRectangle(pouchX + 15, detailY, pouchW - 30, detailH, Color{ 14, 24, 40, 255 }); DrawRectangle(pouchX + 15, detailY, pouchW - 30, detailH, TacticalColors::SurfacePlate);
DrawRectangleLines(pouchX + 15, detailY, pouchW - 30, detailH, CyberColors::NeonGreen); DrawRectangleLines(pouchX + 15, detailY, pouchW - 30, detailH, TacticalColors::BorderWhite);
DrawText(curSelInfo.name.c_str(), pouchX + 26, detailY + 8, 14, curSelInfo.color); 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(("IN STOCK: " + std::to_string(curSelCount)).c_str(), pouchX + 370, detailY + 8, 12, curSelCount > 0 ? TacticalColors::PureWhite : TacticalColors::Crimson);
DrawText(curSelInfo.description.c_str(), pouchX + 26, detailY + 28, 11, RAYWHITE); 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, CyberColors::NeonYellow); DrawText("[ENTER / E / SPACE] INSERT INTO SELECTED CHAMBER", pouchX + 26, detailY + 46, 11, TacticalColors::Crimson);
// Neural Implants Matrix Panel (2x2 grid) // Neural Implants Matrix Panel (2x2 grid)
int impPanelY = detailY + detailH + 8; int impPanelY = detailY + detailH + 8;
int impPanelH = pouchY + pouchH - impPanelY - 10; int impPanelH = pouchY + pouchH - impPanelY - 10;
DrawRectangle(pouchX + 15, impPanelY, pouchW - 30, impPanelH, Color{ 12, 18, 30, 255 }); DrawRectangle(pouchX + 15, impPanelY, pouchW - 30, impPanelH, TacticalColors::DarkPanel);
DrawRectangleLines(pouchX + 15, impPanelY, pouchW - 30, impPanelH, CyberColors::NeonCyan); DrawRectangleLines(pouchX + 15, impPanelY, pouchW - 30, impPanelH, TacticalColors::BorderMuted);
std::string impHeader = "// NEURAL MATRIX RIG // SLOTS [" + std::to_string(player.getImplants().size()) + " / 4]"; 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()) + " ]"; std::string chipTxt = "[ SCRAP: " + std::to_string(player.getTechScraps()) + " ]";
DrawText(chipTxt.c_str(), pouchX + pouchW - 95, impPanelY + 8, 12, CyberColors::NeonYellow); DrawText(chipTxt.c_str(), pouchX + pouchW - 120, impPanelY + 8, 12, TacticalColors::Crimson);
const auto& implants = player.getImplants(); const auto& implants = player.getImplants();
int slotW = 236; int slotW = 236;
@ -365,28 +385,28 @@ void InventoryUI::draw(const Player& player) {
Color archCol = data.color; Color archCol = data.color;
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
archCol = RED; archCol = TacticalColors::Crimson;
} }
DrawRectangleRec(sRect, Color{ 16, 24, 38, 255 }); DrawRectangleRec(sRect, TacticalColors::SurfacePlate);
DrawRectangleLinesEx(sRect, isHoverImp ? 2.0f : 1.0f, isHoverImp ? WHITE : archCol); DrawRectangleLinesEx(sRect, isHoverImp ? 2.0f : 1.0f, isHoverImp ? TacticalColors::PureWhite : archCol);
std::string sTitle = "SLOT " + std::to_string(s + 1) + ": " + data.name; std::string sTitle = "SLOT " + std::to_string(s + 1) + ": " + data.name;
DrawText(sTitle.c_str(), sX + 8, sY + 6, 11, archCol); DrawText(sTitle.c_str(), sX + 8, sY + 6, 11, archCol);
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { 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 { } else {
std::string shortDesc = data.description; std::string shortDesc = data.description;
if (shortDesc.length() > 32) shortDesc = shortDesc.substr(0, 30) + ".."; 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 { } else {
DrawRectangleRec(sRect, Color{ 8, 12, 20, 200 }); DrawRectangleRec(sRect, TacticalColors::PitchBlack);
DrawRectangleLinesEx(sRect, 1.0f, Color{ 30, 45, 65, 200 }); DrawRectangleLinesEx(sRect, 1.0f, TacticalColors::BorderMuted);
std::string emptyTitle = "SLOT " + std::to_string(s + 1) + ": [EMPTY SOCKET]"; std::string emptyTitle = "SLOT " + std::to_string(s + 1) + ": [EMPTY SOCKET]";
DrawText(emptyTitle.c_str(), sX + 8, sY + 8, 11, DARKGRAY); DrawText(emptyTitle.c_str(), sX + 8, sY + 8, 11, TacticalColors::MutedText);
DrawText("Draft from Elites / ICE", sX + 8, sY + 26, 10, Color{ 60, 80, 110, 255 }); 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_texMonitorRadar{};
Texture2D m_texBedQuilt{}; Texture2D m_texBedQuilt{};
Texture2D m_texCityNight{}; Texture2D m_texCityNight{};
Texture2D m_texWindowView{};
Texture2D m_texCyberGate{}; Texture2D m_texCyberGate{};
// Ancient Palace Storeroom Procedural Textures // 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 "UIRenderer.hpp"
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <sstream>
namespace dnr { 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 }); 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) { void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int startX, int startY, int size) {
int w = dungeon.getWidth(); int w = dungeon.getWidth();
int h = dungeon.getHeight(); int h = dungeon.getHeight();
float cellSize = static_cast<float>(size) / std::max(w, h); float cellSize = static_cast<float>(size) / std::max(w, h);
// Frame // Frame (Strict 0-rounding)
DrawRectangle(startX - 4, startY - 4, size + 8, size + 8, Color{ 4, 10, 18, 230 }); DrawRectangle(startX - 4, startY - 4, size + 8, size + 8, TacticalColors::DarkPanel);
DrawRectangleLines(startX - 4, startY - 4, size + 8, size + 8, CyberColors::NeonCyan); DrawRectangleLines(startX - 4, startY - 4, size + 8, size + 8, TacticalColors::BorderMuted);
DrawText("NET_MAP // 2.5D", startX, startY - 18, 11, CyberColors::NeonCyan); DrawText("TACTICAL MAP // 2D", startX, startY - 18, 11, TacticalColors::PureWhite);
bool hasPrecog = player.hasImplant(ImplantType::PRECOGNITION_OPTICS); 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); TileType t = dungeon.getTile(x, y);
switch (t) { switch (t) {
case TileType::WALL: case TileType::WALL:
DrawRectangle(px, py, cs, cs, Color{ 25, 45, 75, 255 }); DrawRectangle(px, py, cs, cs, Color{ 30, 36, 48, 255 });
break; break;
case TileType::FLOOR: case TileType::FLOOR:
DrawRectangle(px, py, cs, cs, Color{ 10, 20, 35, 255 }); DrawRectangle(px, py, cs, cs, Color{ 10, 14, 20, 255 });
break; break;
case TileType::DOOR_LOCKED: case TileType::DOOR_LOCKED:
DrawRectangle(px, py, cs, cs, CyberColors::NeonOrange); DrawRectangle(px, py, cs, cs, TacticalColors::Crimson);
break; break;
case TileType::DOOR_UNLOCKED: case TileType::DOOR_UNLOCKED:
DrawRectangle(px, py, cs, cs, CyberColors::NeonGreen); DrawRectangle(px, py, cs, cs, TacticalColors::PureWhite);
break; break;
case TileType::LOOT_CACHE: case TileType::LOOT_CACHE:
DrawRectangle(px, py, cs, cs, CyberColors::NeonYellow); DrawRectangle(px, py, cs, cs, TacticalColors::BorderWhite);
break; break;
case TileType::EXIT_PORTAL: case TileType::EXIT_PORTAL:
DrawRectangle(px, py, cs, cs, CyberColors::NeonCyan); DrawRectangle(px, py, cs, cs, TacticalColors::Crimson);
break; break;
default: default:
break; 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 lx = startX + static_cast<int>(laser.x * cellSize);
int ly = startY + static_cast<int>(laser.y * cellSize); int ly = startY + static_cast<int>(laser.y * cellSize);
int cs = static_cast<int>(cellSize) + 1; 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); 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()) { for (const auto& mine : dungeon.getMines()) {
int dist = std::abs(mine.x - player.getGridX()) + std::abs(mine.y - player.getGridY()); int dist = std::abs(mine.x - player.getGridX()) + std::abs(mine.y - player.getGridY());
if (dungeon.isExplored(mine.x, mine.y) || (hasPrecog && dist <= 5)) { if (dungeon.isExplored(mine.x, mine.y) || (hasPrecog && dist <= 5)) {
int mx = startX + static_cast<int>((mine.x + 0.5f) * cellSize); int mx = startX + static_cast<int>((mine.x + 0.5f) * cellSize);
int my = startY + static_cast<int>((mine.y + 0.5f) * cellSize); int my = startY + static_cast<int>((mine.y + 0.5f) * cellSize);
Color mCol = mine.defused ? DARKGRAY : (mine.isArmed() ? CyberColors::NeonOrange : RED); Color mCol = mine.defused ? DARKGRAY : (mine.isArmed() ? TacticalColors::Crimson : TacticalColors::BrightRed);
DrawCircle(mx, my, std::max(2.0f, cellSize * 0.35f), mCol); 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()) { for (const auto& shrine : dungeon.getShrines()) {
int dist = std::abs(shrine.x - player.getGridX()) + std::abs(shrine.y - player.getGridY()); int dist = std::abs(shrine.x - player.getGridX()) + std::abs(shrine.y - player.getGridY());
if (dungeon.isExplored(shrine.x, shrine.y) || (hasPrecog && dist <= 5)) { if (dungeon.isExplored(shrine.x, shrine.y) || (hasPrecog && dist <= 5)) {
int sx = startX + static_cast<int>((shrine.x + 0.5f) * cellSize); int sx = startX + static_cast<int>((shrine.x + 0.5f) * cellSize);
int sy = startY + static_cast<int>((shrine.y + 0.5f) * cellSize); int sy = startY + static_cast<int>((shrine.y + 0.5f) * cellSize);
Color sCol = shrine.used ? DARKGRAY : CyberColors::NeonPink; Color sCol = shrine.used ? DARKGRAY : TacticalColors::PureWhite;
DrawCircle(sx, sy, std::max(2.5f, cellSize * 0.45f), sCol); 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()) { for (const auto& me : dungeon.getEnemies()) {
if (!me.alive) continue; if (!me.alive) continue;
int dist = std::abs(me.x - player.getGridX()) + std::abs(me.y - player.getGridY()); 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 ex = startX + static_cast<int>((me.x + 0.5f) * cellSize);
int ey = startY + static_cast<int>((me.y + 0.5f) * cellSize); int ey = startY + static_cast<int>((me.y + 0.5f) * cellSize);
if (me.enemy.isElite()) { if (me.enemy.isElite()) {
DrawCircle(ex, ey, std::max(3.0f, cellSize * 0.5f), CyberColors::NeonYellow); DrawRectangle(ex - 4, ey - 4, 9, 9, TacticalColors::Crimson);
DrawCircleLines(ex, ey, std::max(4.0f, cellSize * 0.65f), CyberColors::NeonPink); DrawRectangle(ex - 2, ey - 2, 5, 5, TacticalColors::PureWhite);
} else { } 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 plX = startX + static_cast<int>((player.getGridX() + 0.5f) * cellSize);
int plY = startY + static_cast<int>((player.getGridY() + 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()); 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) { void UIRenderer::drawMatrixDigitalRain(float animTime) {
@ -131,65 +159,75 @@ void UIRenderer::drawMatrixDigitalRain(float animTime) {
int xPos = c * colWidth + 10; int xPos = c * colWidth + 10;
const char* txt = glyphs[(c + static_cast<int>(animTime * 2.0f)) % 12]; 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), 12, Color{ 140, 150, 165, 30 });
DrawText(txt, xPos, static_cast<int>(yPos - 20), 11, Color{ 0, 255, 100, 18 }); DrawText(txt, xPos, static_cast<int>(yPos - 20), 11, Color{ 140, 150, 165, 15 });
} }
} }
void UIRenderer::drawMatrixOverlay(float animTime) { void UIRenderer::drawMatrixOverlay(float animTime) {
// Subtle Matrix CRT scanlines // Subtle CRT scanlines
int screenH = GetScreenHeight(); int screenH = GetScreenHeight();
int screenW = GetScreenWidth(); int screenW = GetScreenWidth();
for (int y = 0; y < screenH; y += 3) { 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 // Top terminal status bar
DrawText("[SYS::MATRIX_ONLINE] [SUBNET::0x7F00] [PROTOCOL::CYBER_REVOLVER] [SECURITY::ICE_ACTIVE]", DrawText("[SYS::TACTICAL_OS] [ARASAKA_NET] [PROTOCOL::357_MAGNUM] [SECURITY::ACTIVE]",
15, screenH - 18, 11, Color{ 0, 240, 160, 160 }); 15, screenH - 18, 11, TacticalColors::MutedText);
} }
void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player, const std::string& prompt) { 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 hudX = 20;
int hudY = 20; int hudY = 20;
DrawRectangle(hudX, hudY, 330, 145, Color{ 6, 12, 22, 220 }); DrawRectangle(hudX, hudY, 330, 145, TacticalColors::DarkPanel);
DrawRectangleLines(hudX, hudY, 330, 145, CyberColors::NeonCyan); 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(); float hpRatio = static_cast<float>(player.getHp()) / player.getMaxHp();
drawProgressBar(hudX + 60, hudY + 40, 250, 18, hpRatio, CyberColors::NeonPink, Color{ 30, 10, 20, 255 }); drawProgressBar(hudX + 60, hudY + 40, 250, 18, hpRatio, TacticalColors::Crimson, TacticalColors::DarkRed);
DrawText("CORE", hudX + 15, hudY + 40, 14, CyberColors::NeonPink); 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, RAYWHITE); 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(); float shRatio = static_cast<float>(player.getShield()) / player.getMaxShield();
drawProgressBar(hudX + 60, hudY + 68, 250, 18, shRatio, CyberColors::NeonCyan, Color{ 10, 25, 45, 255 }); drawProgressBar(hudX + 60, hudY + 68, 250, 18, shRatio, TacticalColors::PureWhite, Color{ 24, 30, 42, 255 });
DrawText("SHLD", hudX + 15, hudY + 68, 14, CyberColors::NeonCyan); 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, RAYWHITE); DrawText((std::to_string(player.getShield()) + " / " + std::to_string(player.getMaxShield())).c_str(), hudX + 145, hudY + 70, 13, TacticalColors::PitchBlack);
// Cyber-Picks & Statuses // ICE-Picks & Statuses
DrawText(("ICE PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 102, 15, CyberColors::NeonYellow); DrawText(("ICE PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 102, 14, TacticalColors::SilverText);
std::string statStr = "STATUS: "; std::string statStr = "STATUS: ";
if (player.getStatuses().empty()) { if (player.getStatuses().empty()) {
statStr += "SECURE"; 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 { } else {
statStr += "MALWARE_DEBUFF"; statStr += "HOSTILE_DEBUFF";
DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, RED); 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 impBarX = hudX;
int impBarY = hudY + 152; int impBarY = hudY + 152;
int impBarW = 330; int impBarW = 330;
int impBarH = 34; int impBarH = 34;
DrawRectangle(impBarX, impBarY, impBarW, impBarH, Color{ 6, 12, 22, 220 }); DrawRectangle(impBarX, impBarY, impBarW, impBarH, TacticalColors::DarkPanel);
DrawRectangleLines(impBarX, impBarY, impBarW, impBarH, CyberColors::NeonCyan); DrawRectangleLines(impBarX, impBarY, impBarW, impBarH, TacticalColors::BorderMuted);
const auto& implants = player.getImplants(); const auto& implants = player.getImplants();
Vector2 mousePos = GetMousePosition(); Vector2 mousePos = GetMousePosition();
@ -207,11 +245,11 @@ void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player
Color archCol = data.color; Color archCol = data.color;
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
archCol = RED; archCol = TacticalColors::DarkRed;
} }
DrawRectangleRec(bRect, Color{ 12, 20, 32, 240 }); DrawRectangleRec(bRect, TacticalColors::SurfacePlate);
DrawRectangleLinesEx(bRect, isHover ? 2.0f : 1.0f, isHover ? WHITE : archCol); DrawRectangleLinesEx(bRect, isHover ? 2.0f : 1.0f, isHover ? TacticalColors::Crimson : archCol);
std::string code = data.code; std::string code = data.code;
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
@ -222,9 +260,9 @@ void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player
if (isHover) hoveredImpIdx = s; if (isHover) hoveredImpIdx = s;
} else { } else {
DrawRectangleRec(bRect, Color{ 8, 14, 22, 180 }); DrawRectangleRec(bRect, Color{ 10, 14, 20, 200 });
DrawRectangleLinesEx(bRect, 1.0f, Color{ 40, 60, 85, 180 }); DrawRectangleLinesEx(bRect, 1.0f, TacticalColors::BorderMuted);
DrawText("[--]", bX + 27, bY + 6, 11, DARKGRAY); 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 tipY = impBarY + impBarH + 6;
int tipW = 330; int tipW = 330;
int tipH = 50; int tipH = 50;
DrawRectangle(tipX, tipY, tipW, tipH, Color{ 10, 16, 26, 250 }); DrawRectangle(tipX, tipY, tipW, tipH, TacticalColors::DarkPanel);
DrawRectangleLines(tipX, tipY, tipW, tipH, CyberColors::NeonYellow); DrawRectangleLines(tipX, tipY, tipW, tipH, TacticalColors::Crimson);
DrawText(tipData.name.c_str(), tipX + 10, tipY + 8, 12, CyberColors::NeonYellow); DrawText(tipData.name.c_str(), tipX + 10, tipY + 8, 12, TacticalColors::PureWhite);
DrawText(tipData.description.c_str(), tipX + 10, tipY + 26, 10, RAYWHITE); 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); 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 revX = GetScreenWidth() - 260;
int revY = GetScreenHeight() - 110; int revY = GetScreenHeight() - 110;
DrawRectangle(revX, revY, 240, 90, Color{ 6, 12, 22, 220 }); DrawRectangle(revX, revY, 240, 90, TacticalColors::DarkPanel);
DrawRectangleLines(revX, revY, 240, 90, CyberColors::NeonCyan); DrawRectangleLines(revX, revY, 240, 90, TacticalColors::BorderMuted);
AmmoType curAmmo = player.getRevolver().getCurrentAmmo(); AmmoType curAmmo = player.getRevolver().getCurrentAmmo();
AmmoType nextAmmo = player.getRevolver().getNextAmmo(); AmmoType nextAmmo = player.getRevolver().getNextAmmo();
const auto& curInfo = getAmmoInfo(curAmmo); const auto& curInfo = getAmmoInfo(curAmmo);
const auto& nextInfo = getAmmoInfo(nextAmmo); const auto& nextInfo = getAmmoInfo(nextAmmo);
DrawText("REVOLVER READY", revX + 15, revY + 10, 13, CyberColors::NeonCyan); DrawText("REVOLVER READY", revX + 15, revY + 10, 12, TacticalColors::MutedText);
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); // Tactical ammo badge (Rectangular, 0-rounding)
DrawText("[TAB] ARMORY", revX + 140, revY + 65, 12, CyberColors::NeonYellow); 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()) { 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 promptX = (GetScreenWidth() - promptW) / 2;
int promptY = GetScreenHeight() - 70; int promptY = GetScreenHeight() - 70;
DrawRectangle(promptX, promptY, promptW, 45, Color{ 10, 18, 32, 240 }); DrawRectangle(promptX, promptY, promptW, 44, TacticalColors::DarkPanel);
DrawRectangleLines(promptX, promptY, promptW, 45, CyberColors::NeonYellow); DrawRectangleLines(promptX, promptY, promptW, 44, TacticalColors::Crimson);
DrawText(prompt.c_str(), promptX + 20, promptY + 13, 18, CyberColors::NeonYellow); DrawText(prompt.c_str(), promptX + 20, promptY + 14, 16, TacticalColors::PureWhite);
} }
// Controls hints in bottom-left // Controls hints in bottom-left
DrawText("[W/S] WALK | [A/D] TURN 90 | [Q/E] STRAFE | [SPACE/E] INTERACT | [TAB] ARMORY", 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) { 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 panelW = 760;
int panelH = 520; int panelH = 520;
int panelX = (GetScreenWidth() - panelW) / 2; int panelX = (GetScreenWidth() - panelW) / 2;
int panelY = (GetScreenHeight() - panelH) / 2; int panelY = (GetScreenHeight() - panelH) / 2;
DrawRectangle(panelX, panelY, panelW, panelH, Color{ 8, 14, 26, 255 }); DrawRectangle(panelX, panelY, panelW, panelH, TacticalColors::DarkPanel);
DrawRectangleLines(panelX, panelY, panelW, panelH, CyberColors::NeonCyan); DrawRectangleLines(panelX, panelY, panelW, panelH, TacticalColors::BorderMuted);
// Header // Tactical corner bracket accents
DrawRectangle(panelX, panelY, panelW, 50, Color{ 14, 24, 42, 255 }); 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(), 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) { if (hacking.getPhase() == HackingPhase::PHASE_1_TUMBLER) {
DrawText("PHASE 1: FREQUENCY TUMBLER ALIGNMENT", panelX + 25, panelY + 65, 16, CyberColors::NeonYellow); 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, RAYWHITE); 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(); const auto& tumbler = hacking.getTumbler();
int pinCount = tumbler.getPinCount(); int pinCount = tumbler.getPinCount();
@ -310,150 +364,194 @@ void UIRenderer::drawHackingUI(const HackingGame& hacking) {
int px = startPinX + i * (pinW + pinGap); int px = startPinX + i * (pinW + pinGap);
bool isSelected = (i == tumbler.getSelectedPin()); bool isSelected = (i == tumbler.getSelectedPin());
DrawRectangle(px, trackY, pinW, trackH, Color{ 14, 20, 32, 255 }); DrawRectangle(px, trackY, pinW, trackH, Color{ 14, 18, 26, 255 });
DrawRectangleLines(px, trackY, pinW, trackH, isSelected ? CyberColors::NeonYellow : Color{ 40, 60, 90, 255 }); DrawRectangleLines(px, trackY, pinW, trackH, isSelected ? TacticalColors::Crimson : TacticalColors::BorderMuted);
// Sweet spot target zone // Sweet spot target zone
int sweetY = trackY + static_cast<int>((1.0f - pin.targetHeight) * trackH); int sweetY = trackY + static_cast<int>((1.0f - pin.targetHeight) * trackH);
int sweetSpan = static_cast<int>(pin.tolerance * 2.0f * 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 }); DrawRectangle(px + 2, sweetY - sweetSpan / 2, pinW - 4, sweetSpan, Color{ 220, 230, 245, 45 });
DrawLine(px, sweetY, px + pinW, sweetY, CyberColors::NeonCyan); DrawLine(px, sweetY, px + pinW, sweetY, TacticalColors::PureWhite);
int pinH = 40; int pinH = 40;
int pinCurrY = trackY + static_cast<int>((1.0f - pin.currentHeight) * (trackH - pinH)); int pinCurrY = trackY + static_cast<int>((1.0f - pin.currentHeight) * (trackH - pinH));
Color pinColor = pin.locked ? CyberColors::NeonGreen : Color pinColor = pin.locked ? TacticalColors::PureWhite :
(pin.isInSweetSpot() ? CyberColors::NeonYellow : Color{ 70, 110, 160, 255 }); (pin.isInSweetSpot() ? TacticalColors::Crimson : Color{ 50, 62, 80, 255 });
DrawRectangle(px + 4, pinCurrY, pinW - 8, pinH, pinColor); 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) { 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()) { } 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 { } 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()) { 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 { } 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) { } 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("PHASE 2: DEAD-LOCK TIMING INJECTION (CALIBRATION RAIL)", panelX + 25, panelY + 65, 16, TacticalColors::Crimson);
DrawText("STRIKE [SPACE] WHEN THE NEEDLE HITS THE TARGET ZONE!", panelX + 25, panelY + 90, 14, CyberColors::NeonYellow); DrawText("STRIKE [SPACE] WHEN THE NEEDLE HITS THE TARGET WINDOW!", panelX + 25, panelY + 90, 14, TacticalColors::PureWhite);
const auto& sc = hacking.getSkillCheck(); const auto& sc = hacking.getSkillCheck();
int dialCenterX = panelX + panelW / 2; int railW = 600;
int dialCenterY = panelY + panelH / 2 + 10; int railH = 54;
float dialRadius = 130.0f; int railX = panelX + (panelW - railW) / 2;
int railY = panelY + panelH / 2 - 20;
DrawCircle(dialCenterX, dialCenterY, dialRadius + 12.0f, Color{ 10, 16, 26, 255 }); // Outer housing (Strict 0-rounding)
DrawCircleLines(dialCenterX, dialCenterY, dialRadius + 12.0f, CyberColors::NeonCyan); DrawRectangle(railX - 8, railY - 14, railW + 16, railH + 28, Color{ 14, 18, 26, 255 });
DrawCircle(dialCenterX, dialCenterY, dialRadius - 20.0f, Color{ 6, 10, 18, 255 }); DrawRectangleLines(railX - 8, railY - 14, railW + 16, railH + 28, TacticalColors::BorderMuted);
// Success zone // Rail background
DrawCircleSectorLines({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) }, DrawRectangle(railX, railY, railW, railH, Color{ 18, 24, 34, 255 });
dialRadius, DrawRectangleLines(railX, railY, railW, railH, TacticalColors::BorderMuted);
sc.getSuccessStart() - 90.0f,
sc.getSuccessEnd() - 90.0f,
32, RAYWHITE);
DrawCircleSector({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) },
dialRadius,
sc.getSuccessStart() - 90.0f,
sc.getSuccessEnd() - 90.0f,
32, Color{ 220, 220, 220, 120 });
// Great success zone // Tick marks every 10%
DrawCircleSector({ static_cast<float>(dialCenterX), static_cast<float>(dialCenterY) }, for (int t = 0; t <= 10; ++t) {
dialRadius, int tx = railX + static_cast<int>(t * (railW / 10.0f));
sc.getGreatStart() - 90.0f, DrawLine(tx, railY, tx, railY + 8, TacticalColors::BorderMuted);
sc.getGreatEnd() - 90.0f, DrawLine(tx, railY + railH - 8, tx, railY + railH, TacticalColors::BorderMuted);
16, CyberColors::NeonGreen); }
// Rotating needle // Linear Target Window (Success Zone)
float needleRad = (sc.getNeedleAngle() - 90.0f) * (3.14159265f / 180.0f); float succStartRatio = std::clamp(sc.getSuccessStart() / 360.0f, 0.0f, 1.0f);
int needleEndX = dialCenterX + static_cast<int>(std::cos(needleRad) * (dialRadius + 8.0f)); float succEndRatio = std::clamp(sc.getSuccessEnd() / 360.0f, 0.0f, 1.0f);
int needleEndY = dialCenterY + static_cast<int>(std::sin(needleRad) * (dialRadius + 8.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) }, DrawRectangle(sX, railY + 2, sW, railH - 4, Color{ 220, 230, 245, 50 });
{ static_cast<float>(needleEndX), static_cast<float>(needleEndY) }, DrawRectangleLines(sX, railY + 2, sW, railH - 4, TacticalColors::PureWhite);
4.0f, RED);
DrawCircle(dialCenterX, dialCenterY, 14, RED);
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 { } else {
int cx = panelX + panelW / 2; int cx = panelX + panelW / 2;
int cy = panelY + panelH / 2; int cy = panelY + panelH / 2;
if (hacking.getPhase() == HackingPhase::RESULT_CRITICAL) { if (hacking.getPhase() == HackingPhase::RESULT_CRITICAL) {
DrawText(">>> CRITICAL OVERRIDE SUCCESS! <<<", cx - 210, cy - 40, 24, CyberColors::NeonGreen); DrawText(">>> CRITICAL OVERRIDE SUCCESS! <<<", cx - 210, cy - 40, 24, TacticalColors::Crimson);
DrawText("Subroutines penetrated. Bonus cyber-cartridge recovered!", cx - 200, cy + 10, 16, RAYWHITE); DrawText("Subroutines penetrated. Bonus cyber-cartridge recovered!", cx - 200, cy + 10, 16, TacticalColors::PureWhite);
} else if (hacking.getPhase() == HackingPhase::RESULT_SUCCESS) { } else if (hacking.getPhase() == HackingPhase::RESULT_SUCCESS) {
DrawText(">>> ACCESS GRANTED <<<", cx - 140, cy - 40, 26, CyberColors::NeonCyan); DrawText(">>> ACCESS GRANTED <<<", cx - 140, cy - 40, 26, TacticalColors::PureWhite);
DrawText("Security locks bypassed successfully.", cx - 150, cy + 10, 16, RAYWHITE); DrawText("Security locks bypassed successfully.", cx - 150, cy + 10, 16, TacticalColors::SilverText);
} else { } else {
DrawText("!!! ICE ALARM TRIGGERED !!!", cx - 180, cy - 40, 26, RED); DrawText("!!! ICE ALARM TRIGGERED !!!", cx - 180, cy - 40, 26, TacticalColors::BrightRed);
DrawText("Intrusion countermeasures activated. Defense Sentinel deployed!", cx - 240, cy + 10, 16, CyberColors::NeonOrange); 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) { void UIRenderer::drawRevolverCylinderCombat(const Revolver& revolver, int centerX, int centerY, float animTime, bool isSpinning) {
float cylinderR = 95.0f; float chamberOrbitR = 56.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(); 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 // 1. Faceted Octagonal Heavy Outer Housing (Strict 0-rounding)
DrawCircle(centerX, centerY, 34.0f, Color{ 14, 20, 32, 255 }); DrawRectangle(centerX - 100, centerY - 100, 200, 200, TacticalColors::DarkPanel);
DrawCircleLines(centerX, centerY, 34.0f, Color{ 70, 100, 140, 255 }); DrawRectangleLines(centerX - 100, centerY - 100, 200, 200, TacticalColors::BorderMuted);
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) // 45-degree chamfers on outer corners
float strikerPulse = 0.5f + 0.5f * std::sin(animTime * 6.0f); DrawTriangle({ (float)(centerX - 100), (float)(centerY - 100) },
int strikerTipY = centerY - static_cast<int>(cylinderR) - 4; { (float)(centerX - 72), (float)(centerY - 100) },
int strikerBaseY = centerY - static_cast<int>(cylinderR) - 26; { (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( // 2. Inner Chamfered Drum Plate
{ static_cast<float>(centerX), static_cast<float>(strikerTipY) }, DrawRectangle(centerX - 88, centerY - 88, 176, 176, Color{ 16, 22, 32, 255 });
{ static_cast<float>(centerX - 12), static_cast<float>(strikerBaseY) }, DrawRectangleLines(centerX - 88, centerY - 88, 176, 176, TacticalColors::BorderWhite);
{ static_cast<float>(centerX + 12), static_cast<float>(strikerBaseY) }, DrawTriangle({ (float)(centerX - 88), (float)(centerY - 88) },
Color{ 255, 210, 50, 255 } { (float)(centerX - 64), (float)(centerY - 88) },
); { (float)(centerX - 88), (float)(centerY - 64) }, TacticalColors::DarkPanel);
DrawRectangle(centerX - 18, strikerBaseY - 14, 36, 14, Color{ 210, 160, 30, 255 }); DrawTriangle({ (float)(centerX + 88), (float)(centerY - 88) },
DrawRectangleLines(centerX - 18, strikerBaseY - 14, 36, 14, CyberColors::NeonYellow); { (float)(centerX + 64), (float)(centerY - 88) },
DrawCircle(centerX, strikerTipY, 2.5f + strikerPulse * 1.5f, CyberColors::NeonYellow); { (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 // 3. Center Hub & Caliber Stamp
DrawRectangle(centerX - 65, strikerBaseY - 32, 130, 16, Color{ 10, 16, 26, 230 }); DrawRectangle(centerX - 24, centerY - 24, 48, 48, Color{ 12, 16, 24, 255 });
DrawRectangleLines(centerX - 65, strikerBaseY - 32, 130, 16, Color{ 180, 150, 40, 200 }); DrawRectangleLines(centerX - 24, centerY - 24, 48, 48, TacticalColors::BorderMuted);
DrawText("FIRING PIN [12:00]", centerX - 58, strikerBaseY - 30, 11, CyberColors::NeonYellow); 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) { for (int i = 0; i < 6; ++i) {
float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f); float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f);
int chX = centerX + static_cast<int>(std::cos(angleRad) * chamberOrbitR); 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); const auto& info = getAmmoInfo(ammo);
bool isCurrent = (i == revolver.getCurrentIndex()); bool isCurrent = (i == revolver.getCurrentIndex());
DrawCircle(chX, chY, chamberR, Color{ 16, 22, 34, 255 }); int chW = 34;
DrawCircleLines(chX, chY, chamberR, isCurrent ? CyberColors::NeonYellow : Color{ 55, 80, 120, 255 }); int chH = 34;
Rectangle chRect = { (float)(chX - chW / 2), (float)(chY - chH / 2), (float)chW, (float)chH };
if (ammo == AmmoType::EMPTY) { if (ammo == AmmoType::EMPTY) {
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 8, 12, 18, 255 }); DrawRectangleRec(chRect, TacticalColors::PitchBlack);
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 60, 60, 60, 255 }); DrawRectangleLinesEx(chRect, isCurrent ? 2.0f : 1.0f, isCurrent ? TacticalColors::BrightRed : TacticalColors::BorderMuted);
DrawText("0", chX - 4, chY - 6, 12, DARKGRAY); DrawText("---", chX - 9, chY - 5, 10, TacticalColors::MutedText);
} else { } else {
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 34, 46, 62, 255 }); DrawRectangleRec(chRect, TacticalColors::DarkPanel);
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 180, 140, 50, 255 }); DrawRectangleLinesEx(chRect, isCurrent ? 2.0f : 1.0f, isCurrent ? TacticalColors::BrightRed : info.color);
DrawCircle(chX, chY, chamberR - 8.0f, info.color); // Tactical cartridge primer block
DrawCircle(chX - 3, chY - 3, 2.5f, Color{ 255, 255, 255, 200 }); DrawRectangle(chX - 7, chY - 7, 14, 14, Color{ info.color.r, info.color.g, info.color.b, 200 });
DrawText(info.shortName.substr(0, 3).c_str(), chX - 9, chY - 5, 10, BLACK); 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) { if (isCurrent) {
float reticleR = chamberR + 5.0f + 2.0f * std::sin(animTime * 6.0f); DrawRectangleLinesEx({ chRect.x - 4, chRect.y - 4, chRect.width + 8, chRect.height + 8 }, 2.0f, TacticalColors::BrightRed);
DrawCircleLines(chX, chY, reticleR, CyberColors::NeonYellow); DrawLine(chX, (int)chRect.y - 8, chX, (int)chRect.y - 3, TacticalColors::BrightRed);
DrawLine(chX - static_cast<int>(reticleR) - 3, chY, chX - static_cast<int>(reticleR) + 3, chY, CyberColors::NeonYellow); DrawLine(chX, (int)(chRect.y + chRect.height + 3), chX, (int)(chRect.y + chRect.height + 8), TacticalColors::BrightRed);
DrawLine(chX + static_cast<int>(reticleR) - 3, chY, chX + static_cast<int>(reticleR) + 3, chY, CyberColors::NeonYellow); DrawLine((int)chRect.x - 8, chY, (int)chRect.x - 3, chY, TacticalColors::BrightRed);
DrawLine(chX, chY - static_cast<int>(reticleR) - 3, chX, chY - static_cast<int>(reticleR) + 3, CyberColors::NeonYellow); DrawLine((int)(chRect.x + chRect.width + 3), chY, (int)(chRect.x + chRect.width + 8), chY, TacticalColors::BrightRed);
DrawLine(chX, chY + static_cast<int>(reticleR) - 3, chX, chY + static_cast<int>(reticleR) + 3, CyberColors::NeonYellow);
} }
} }
} }
@ -532,18 +632,19 @@ void UIRenderer::drawCodexModal() {
int cardY = colY + 24; int cardY = colY + 24;
for (int i = 0; i < 7; ++i) { for (int i = 0; i < 7; ++i) {
const auto& inf = getAmmoInfo(ammos[i]); const auto& inf = getAmmoInfo(ammos[i]);
DrawRectangle(leftX, cardY, 455, 46, Color{ 12, 18, 30, 255 }); DrawRectangle(leftX, cardY, 455, 46, TacticalColors::SurfacePlate);
DrawRectangleLines(leftX, cardY, 455, 46, (ammos[i] == AmmoType::EMPTY) ? DARKGRAY : inf.color); 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); DrawRectangle(leftX + 10, cardY + 12, 40, 22, TacticalColors::DarkPanel);
DrawCircleLines(leftX + 20, cardY + 23, 10.0f, WHITE); 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 + "]"; std::string title = inf.name + " [" + inf.shortName + "]";
if (ammos[i] != AmmoType::EMPTY) { if (ammos[i] != AmmoType::EMPTY) {
title += " // DMG: " + std::to_string(inf.baseDamage); title += " // DMG: " + std::to_string(inf.baseDamage);
} }
DrawText(title.c_str(), leftX + 40, cardY + 8, 13, inf.color); DrawText(title.c_str(), leftX + 58, cardY + 8, 13, inf.color);
DrawText(inf.description.c_str(), leftX + 40, cardY + 25, 11, RAYWHITE); DrawText(inf.description.c_str(), leftX + 58, cardY + 25, 11, TacticalColors::SilverText);
cardY += 52; cardY += 52;
} }
@ -648,15 +749,15 @@ void UIRenderer::drawCombatUI(const CombatSystem& combat, const Player& player,
DrawText("WEAPON:", scanX + 25, scanY + 112, 12, GRAY); DrawText("WEAPON:", scanX + 25, scanY + 112, 12, GRAY);
DrawText(atkInfo.shortName.c_str(), scanX + 85, scanY + 112, 12, atkInfo.color); DrawText(atkInfo.shortName.c_str(), scanX + 85, scanY + 112, 12, atkInfo.color);
DrawText("WEAKNESS:", scanX + 160, scanY + 112, 12, GRAY); DrawText("WEAKNESS:", scanX + 160, scanY + 112, 12, TacticalColors::MutedText);
DrawRectangle(scanX + 235, scanY + 107, 240, 24, Color{ 35, 28, 12, 240 }); DrawRectangle(scanX + 235, scanY + 107, 240, 24, Color{ 14, 18, 26, 240 });
DrawRectangleLines(scanX + 235, scanY + 107, 240, 24, CyberColors::NeonYellow); DrawRectangleLines(scanX + 235, scanY + 107, 240, 24, wknInfo.color);
std::string wknText = wknInfo.shortName + " -> +100% CRIT SURGE!"; std::string wknText = "[" + wknInfo.shortName + "] -> +100% CRIT SURGE!";
DrawText(wknText.c_str(), scanX + 242, scanY + 112, 12, CyberColors::NeonYellow); DrawText(wknText.c_str(), scanX + 242, scanY + 112, 12, wknInfo.color);
if (rev.getCurrentAmmo() == enemy->getWeakness()) { if (rev.getCurrentAmmo() == enemy->getWeakness()) {
float surgePulse = 0.5f + 0.5f * std::sin(animTime * 10.0f); 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); 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 advW = 370;
int advH = 46; int advH = 46;
DrawRectangle(advX, advY, advW, advH, Color{ 8, 16, 30, 240 }); DrawRectangle(advX, advY, advW, advH, TacticalColors::DarkPanel);
DrawRectangleLines(advX, advY, advW, advH, CyberColors::NeonCyan); DrawRectangleLines(advX, advY, advW, advH, TacticalColors::BorderMuted);
float aiPulse = 0.5f + 0.5f * std::sin(animTime * 5.0f); float aiPulse = 0.5f + 0.5f * std::sin(animTime * 5.0f);
DrawCircle(advX + 18, advY + 23, 6.0f + aiPulse * 2.0f, CyberColors::NeonCyan); DrawRectangle(advX + 14, advY + 16, 10, 10, (aiPulse > 0.4f) ? TacticalColors::Crimson : TacticalColors::DarkRed);
DrawCircleLines(advX + 18, advY + 23, 10.0f, CyberColors::NeonYellow); DrawRectangleLines(advX + 14, advY + 16, 10, 10, TacticalColors::BrightRed);
DrawText("AI ADVISOR", advX + 32, advY + 8, 10, CyberColors::NeonYellow); DrawText("TACTICAL INTEL", advX + 32, advY + 8, 10, TacticalColors::Crimson);
std::string advisorMsg = ""; std::string advisorMsg = "";
if (rev.getCurrentAmmo() == enemy->getWeakness()) { if (rev.getCurrentAmmo() == enemy->getWeakness()) {
@ -1144,22 +1245,23 @@ void UIRenderer::drawWorkbenchModal(const Player& player) {
Rectangle btnRect = getWorkbenchUpgradeButtonRect(i, screenW, screenH); Rectangle btnRect = getWorkbenchUpgradeButtonRect(i, screenW, screenH);
bool isHoverBtn = CheckCollisionPointRec(mouse, btnRect); bool isHoverBtn = CheckCollisionPointRec(mouse, btnRect);
// Card container // Card container (Strict 0-rounding)
DrawRectangle(cardX, cardY, cardW, cardH, Color{ 14, 20, 32, 255 }); DrawRectangle(cardX, cardY, cardW, cardH, TacticalColors::SurfacePlate);
DrawRectangleLines(cardX, cardY, cardW, cardH, isHoverBtn ? CyberColors::NeonYellow : info.color); DrawRectangleLines(cardX, cardY, cardW, cardH, isHoverBtn ? TacticalColors::Crimson : TacticalColors::BorderMuted);
// Header inside card: Ammo icon & Name // Header inside card: Ammo icon badge & Name
DrawCircle(cardX + 22, cardY + 24, 10.0f, info.color); DrawRectangle(cardX + 12, cardY + 14, 38, 20, TacticalColors::DarkPanel);
DrawCircleLines(cardX + 22, cardY + 24, 10.0f, WHITE); 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 + "]"; 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 // 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) // 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 // Damage display
int effDmg = player.getEffectiveDamage(ammo); 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 }); DrawRectangleLines(cardX + 12, cardY + 84, cardW - 24, 84, Color{ 35, 45, 65, 255 });
// Format perk text wrapping // Format perk text wrapping
DrawText("PERK:", cardX + 18, cardY + 90, 10, GRAY); DrawText("PERK:", cardX + 18, cardY + 90, 10, TacticalColors::MutedText);
std::string perk = curTierData.perkDesc; drawWrappedText(curTierData.perkDesc, cardX + 18, cardY + 104, cardW - 36, 11, 3, TacticalColors::SilverText);
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 // Bottom Action Button / Status
if (curTier >= 3) { 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 }); DrawLine(cardX + 12, cardY + 68, cardX + cardW - 12, cardY + 68, Color{ 35, 55, 80, 255 });
// Description text wrapped into lines // Description text cleanly wrapped into card boundaries (Fixes Screen 1 overflow)
std::string desc = data.description; drawWrappedText(data.description, cardX + 18, cardY + 76, cardW - 36, 12, 4, TacticalColors::SilverText);
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);
}
// Install / Replace button // Install / Replace button
Color btnBg = isHoverBtn ? Color{ 20, 55, 75, 255 } : Color{ 14, 35, 50, 255 }; 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); void drawWorkbenchModal(const Player& player);
static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH); static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH);
static Rectangle getWorkbenchCloseButtonRect(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); void drawImplantDraftModal(const std::vector<ImplantType>& choices, const Player& player, int selectedReplaceSlot = -1);
static Rectangle getDraftCardButtonRect(int cardIndex, int screenW, int screenH); static Rectangle getDraftCardButtonRect(int cardIndex, int screenW, int screenH);