Compare commits

...

2 commits

Author SHA1 Message Date
4cad719290 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
2026-10-06 21:15:02 +03:00
1e3b05d5c8 feat: Roguelike Neural Implants system, Elite Drones & Corridor Hazards 2026-10-05 15:46:19 +03:00
26 changed files with 2640 additions and 739 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View file

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

View file

@ -11,11 +11,17 @@ void CombatSystem::startCombat(Player& player, std::unique_ptr<Enemy> enemy) {
m_phase = CombatPhase::PLAYER_TURN;
m_phaseTimer = 0.0f;
m_shieldBoosted = false;
m_ghostBonusDmg = false;
m_defeatedEnemyWasElite = false;
m_tripleCooldown = 0;
m_lastFiredAmmo = AmmoType::EMPTY;
m_combatLog.clear();
m_floatingTexts.clear();
addLog("--- HOSTILE INTERFACE DETECTED ---");
if (m_enemy->isElite()) {
addLog(">>> WARNING: ELITE DRONE CLASS DETECTED! <<<");
}
addLog(m_enemy->getName() + " attacks with [" + getAmmoInfo(m_enemy->getAttackElement()).shortName + "]!");
addLog("WEAKNESS: [" + getAmmoInfo(m_enemy->getWeakness()).shortName + "] | Ready your revolver!");
}
@ -35,6 +41,26 @@ void CombatSystem::update(float dt, Player& player) {
}
}
auto handleVictory = [&](Player& p) {
m_phase = CombatPhase::VICTORY;
m_defeatedEnemyWasElite = m_enemy->isElite();
int chips = 20 + (std::rand() % 16);
if (m_enemy->isElite()) chips = static_cast<int>(chips * 2.0f);
if (p.hasImplant(ImplantType::SALVAGE_PROTOCOL)) chips = static_cast<int>(chips * 1.5f);
p.addTechScraps(chips);
addLog(">>> TARGET TERMINATED! Protocol cleared <<<");
addLog("CHIPS HARVESTED: +" + std::to_string(chips) + " CORE CHIPS");
addFloatingText("+" + std::to_string(chips) + " CHIPS", { 640, 140 }, CyberColors::NeonYellow);
if (p.hasImplant(ImplantType::ADRENALINE_DRUM)) {
AmmoType randomAmmo = static_cast<AmmoType>(std::rand() % 6);
p.addAmmo(randomAmmo, 1);
addLog("ADRENALINE DRUM: Loaded +1 emergency [" + getAmmoInfo(randomAmmo).shortName + "] cartridge!");
}
SoundManager::get().playPickup();
};
if (m_phase == CombatPhase::SPINNING_SHOT) {
// Wait until cylinder spin decelerates and lands on a chamber
if (!player.getRevolver().isSpinning()) {
@ -55,13 +81,7 @@ void CombatSystem::update(float dt, Player& player) {
m_burstShotsFired = 3;
} else if (m_burstTimer >= 1.65f) {
if (!m_enemy->isAlive()) {
m_phase = CombatPhase::VICTORY;
int chips = 20 + (std::rand() % 16);
player.addTechScraps(chips);
addLog(">>> TARGET TERMINATED! Protocol cleared <<<");
addLog("CHIPS HARVESTED: +" + std::to_string(chips) + " CORE CHIPS");
addFloatingText("+" + std::to_string(chips) + " CHIPS", { 640, 140 }, CyberColors::NeonYellow);
SoundManager::get().playPickup();
handleVictory(player);
} else {
m_phase = CombatPhase::ENEMY_TURN;
executeEnemyTurn(player);
@ -71,13 +91,7 @@ void CombatSystem::update(float dt, Player& player) {
m_phaseTimer += dt;
if (m_phaseTimer >= 0.75f) {
if (!m_enemy->isAlive()) {
m_phase = CombatPhase::VICTORY;
int chips = 20 + (std::rand() % 16);
player.addTechScraps(chips);
addLog(">>> TARGET TERMINATED! Protocol cleared <<<");
addLog("CHIPS HARVESTED: +" + std::to_string(chips) + " CORE CHIPS");
addFloatingText("+" + std::to_string(chips) + " CHIPS", { 640, 140 }, CyberColors::NeonYellow);
SoundManager::get().playPickup();
handleVictory(player);
} else {
m_phase = CombatPhase::ENEMY_TURN;
executeEnemyTurn(player);
@ -87,20 +101,51 @@ void CombatSystem::update(float dt, Player& player) {
m_phaseTimer += dt;
if (m_phaseTimer >= 0.85f) {
if (!player.isAlive()) {
m_phase = CombatPhase::DEFEAT;
addLog("FLATLINE! System destroyed.");
SoundManager::get().playDamage();
} else {
m_phase = CombatPhase::PLAYER_TURN;
if (m_tripleCooldown > 0) {
--m_tripleCooldown;
if (player.hasImplant(ImplantType::AUTO_DEFIBRILLATOR) && !player.isDefibrillatorUsed()) {
player.setDefibrillatorUsed(true);
player.heal(1 - player.getHp());
player.restoreShield(50);
m_enemy->applyStatus(StatusEffect::SHOCKED, 1, 0);
addLog(">>> AUTO-DEFIBRILLATOR DISCHARGED! Lethal shock absorbed, 50 Shield restored! <<<");
addFloatingText("DEFIB RESCUE!", { 300, 460 }, CyberColors::NeonGreen);
SoundManager::get().playSkillGreat();
} else {
m_phase = CombatPhase::DEFEAT;
addLog("FLATLINE! System destroyed.");
SoundManager::get().playDamage();
return;
}
player.updateTurnStart(m_combatLog);
if (!player.isAlive()) {
}
m_phase = CombatPhase::PLAYER_TURN;
if (m_tripleCooldown > 0) {
--m_tripleCooldown;
}
player.updateTurnStart(m_combatLog);
if (!player.isAlive()) {
if (player.hasImplant(ImplantType::AUTO_DEFIBRILLATOR) && !player.isDefibrillatorUsed()) {
player.setDefibrillatorUsed(true);
player.heal(1 - player.getHp());
player.restoreShield(50);
m_enemy->applyStatus(StatusEffect::SHOCKED, 1, 0);
addLog(">>> AUTO-DEFIBRILLATOR DISCHARGED! Status damage averted, 50 Shield restored! <<<");
addFloatingText("DEFIB RESCUE!", { 300, 460 }, CyberColors::NeonGreen);
SoundManager::get().playSkillGreat();
} else {
m_phase = CombatPhase::DEFEAT;
addLog("FLATLINE! System collapsed from status damage.");
return;
}
}
// NANITE WEAVER perk
if (player.hasImplant(ImplantType::NANITE_WEAVER) && !m_enemy->getStatuses().empty()) {
player.heal(5);
player.restoreShield(8);
addLog("NANITE WEAVER: Status siphon (+5 HP, +8 Shield).");
addFloatingText("+5 HP / +8 SHIELD", { 300, 500 }, CyberColors::NeonGreen);
}
}
}
}
@ -123,7 +168,7 @@ void CombatSystem::actionTripleShot(Player& player) {
m_phase = CombatPhase::TRIPLE_SHOT_BURST;
m_burstTimer = 0.0f;
m_burstShotsFired = 0;
m_tripleCooldown = 3;
m_tripleCooldown = player.hasImplant(ImplantType::HEAVY_HAMMER) ? 2 : 3;
}
void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& player) {
@ -132,9 +177,17 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
switch (chamberAmmo) {
case AmmoType::EMP: {
int duration = (tier >= 3) ? 2 : 1;
if (player.hasImplant(ImplantType::CONDUCTIVE_SALVO)) {
duration += 1;
int pierceDmg = m_enemy->takeDamage(15, AmmoType::EMP);
addLog("CONDUCTIVE SALVO: EMP pierced shield for " + std::to_string(pierceDmg) + " core damage!");
addFloatingText("-" + std::to_string(pierceDmg) + " PIERCE", { 640, 200 }, CyberColors::NeonCyan);
}
float chance = (tier >= 3) ? 1.0f : ((tier == 2) ? 0.85f : info.effectChance);
if ((std::rand() % 100) < static_cast<int>(chance * 100)) {
m_enemy->applyStatus(StatusEffect::SHOCKED, (tier >= 3 ? 2 : 1), 0);
m_enemy->applyStatus(StatusEffect::SHOCKED, duration, 0);
addLog(m_enemy->getName() + " is SHOCKED! Reboot delayed.");
addFloatingText((tier >= 3) ? "EMP OVERLOAD!" : "STUNNED!", { 640, 180 }, CyberColors::NeonCyan);
}
@ -171,6 +224,19 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
case AmmoType::NANITE: {
float ratio = (tier >= 3) ? 0.85f : ((tier == 2) ? 0.70f : 0.50f);
int siphon = std::max(1, static_cast<int>(dealt * ratio));
if (player.hasImplant(ImplantType::CORROSIVE_LEECH)) {
bool isCorroded = false;
for (const auto& st : m_enemy->getStatuses()) {
if (st.effect == StatusEffect::CORRODED) isCorroded = true;
}
if (isCorroded) {
siphon *= 2;
addLog("CORROSIVE LEECH: Nanite swarm siphoned 2x from corroded armor!");
addFloatingText("+2X VAMP", { 300, 450 }, CyberColors::NeonGreen);
}
}
player.heal(siphon);
player.restoreShield(tier >= 3 ? siphon : siphon / 2);
addLog("Nanites siphoned +" + std::to_string(siphon) + " HP!");
@ -184,6 +250,7 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
void CombatSystem::resolveBurstShot(Player& player, int shotNum) {
AmmoType chamberAmmo = player.getRevolver().fire();
m_lastFiredAmmo = chamberAmmo;
const auto& info = getAmmoInfo(chamberAmmo);
float offsetX = (shotNum - 2) * 45.0f;
@ -208,18 +275,38 @@ void CombatSystem::resolveBurstShot(Player& player, int shotNum) {
addLog("[SHOT " + std::to_string(shotNum) + "] Hit with [" + info.name + " " + tierData.tierBadge + "] for " + std::to_string(dealt) + " damage!");
addFloatingText("-" + std::to_string(dealt), { 640.0f + offsetX, 240.0f }, info.color);
if (player.hasImplant(ImplantType::CATALYST_MATRIX)) {
bool hasDebuff = false;
for (const auto& st : m_enemy->getStatuses()) {
if (st.effect == StatusEffect::BURNING || st.effect == StatusEffect::CORRODED) hasDebuff = true;
}
if (hasDebuff) {
int catDmg = m_enemy->takeDamage(25, AmmoType::OVERHEAT);
addLog("CATALYST MATRIX: Status detonated for +" + std::to_string(catDmg) + " bonus damage!");
addFloatingText("+25 CATALYST", { 640.0f + offsetX, 170.0f }, CyberColors::NeonPink);
}
}
applyAmmoStatus(chamberAmmo, dealt, player);
}
}
void CombatSystem::resolveShotImpact(Player& player) {
AmmoType chamberAmmo = player.getRevolver().fire();
m_lastFiredAmmo = chamberAmmo;
const auto& info = getAmmoInfo(chamberAmmo);
if (chamberAmmo == AmmoType::EMPTY) {
SoundManager::get().playEmptyClick();
addLog("*CLICK!* Blank chamber misfire! No shot.");
addFloatingText("MISFIRE!", { 640, 240 }, GRAY);
if (player.hasImplant(ImplantType::GHOST_CHAMBER)) {
player.restoreShield(20);
m_ghostBonusDmg = true;
addLog("GHOST CHAMBER: Ghost shield pulse +20! Next shot deals +50% damage.");
addFloatingText("+20 SHIELD", { 300, 480 }, CyberColors::NeonCyan);
}
} else {
SoundManager::get().playGunshot();
@ -227,6 +314,15 @@ void CombatSystem::resolveShotImpact(Player& player) {
int tier = player.getAmmoTier(chamberAmmo);
auto tierData = getAmmoTierData(chamberAmmo, tier);
if (player.hasImplant(ImplantType::HEAVY_HAMMER)) {
dmg = static_cast<int>(dmg * 1.3f);
}
if (m_ghostBonusDmg) {
dmg = static_cast<int>(dmg * 1.5f);
m_ghostBonusDmg = false;
addLog("GHOST SURGE: +50% damage applied!");
}
bool isWeakness = (chamberAmmo == m_enemy->getWeakness());
if (isWeakness) {
addLog("WEAKNESS EXPLOITED! Critical damage surge!");
@ -239,6 +335,18 @@ void CombatSystem::resolveShotImpact(Player& player) {
addLog("Hit with [" + info.name + " " + tierData.tierBadge + "] for " + std::to_string(dealt) + " damage!");
addFloatingText("-" + std::to_string(dealt), { 640, 240 }, info.color);
if (player.hasImplant(ImplantType::CATALYST_MATRIX)) {
bool hasDebuff = false;
for (const auto& st : m_enemy->getStatuses()) {
if (st.effect == StatusEffect::BURNING || st.effect == StatusEffect::CORRODED) hasDebuff = true;
}
if (hasDebuff) {
int catDmg = m_enemy->takeDamage(25, AmmoType::OVERHEAT);
addLog("CATALYST MATRIX: Status detonated for +" + std::to_string(catDmg) + " bonus damage!");
addFloatingText("+25 CATALYST", { 640, 160 }, CyberColors::NeonPink);
}
}
applyAmmoStatus(chamberAmmo, dealt, player);
}
@ -291,10 +399,17 @@ void CombatSystem::executeEnemyTurn(Player& player) {
m_shieldBoosted = false;
}
int preShield = player.getShield();
AmmoType elem = m_enemy->getAttackElement();
int dealt = player.takeDamage(rawDmg, elem);
SoundManager::get().playDamage();
if (preShield > 0 && player.getShield() == 0 && player.hasImplant(ImplantType::DEFLECTOR_FEEDBACK)) {
int refDmg = m_enemy->takeDamage(20, AmmoType::EMP);
addLog("DEFLECTOR FEEDBACK: Barrier shattered! Discharged 20 retaliatory shock damage!");
addFloatingText("-20 SHOCK", { 640, 200 }, CyberColors::NeonCyan);
}
addLog(m_enemy->getName() + " attacks with [" + getAmmoInfo(elem).shortName + "] for " + std::to_string(dealt) + " dmg!");
addFloatingText("-" + std::to_string(dealt), { 300, 480 }, RED);

View file

@ -50,10 +50,15 @@ public:
return m_phase == CombatPhase::VICTORY || m_phase == CombatPhase::DEFEAT;
}
bool isVictory() const { return m_phase == CombatPhase::VICTORY; }
bool wasDefeatedEnemyElite() const { return m_defeatedEnemyWasElite; }
bool wasElite() const { return m_defeatedEnemyWasElite; }
void addLog(const std::string& msg);
void addFloatingText(const std::string& text, Vector2 pos, Color color);
AmmoType getLastFiredAmmo() const { return m_lastFiredAmmo; }
float getPhaseTimer() const { return m_phaseTimer; }
private:
void resolveShotImpact(Player& player);
void resolveBurstShot(Player& player, int shotNum);
@ -70,6 +75,10 @@ private:
int m_burstShotsFired = 0;
bool m_shieldBoosted = false;
bool m_ghostBonusDmg = false;
bool m_defeatedEnemyWasElite = false;
AmmoType m_lastFiredAmmo = AmmoType::EMPTY;
std::vector<std::string> m_combatLog;
std::vector<FloatingText> m_floatingTexts;
};

View file

@ -12,7 +12,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 12 + level * 3;
m_attackElement = AmmoType::OVERHEAT;
m_weakness = AmmoType::EMP;
m_color = CyberColors::NeonOrange;
m_color = TacticalColors::PureWhite;
break;
case EnemyType::ICE_SENTINEL:
@ -22,7 +22,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 14 + level * 3;
m_attackElement = AmmoType::EMP;
m_weakness = AmmoType::CORROSIVE;
m_color = CyberColors::NeonCyan;
m_color = TacticalColors::Crimson;
break;
case EnemyType::GLITCH_WRAITH:
@ -32,7 +32,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 18 + level * 4;
m_attackElement = AmmoType::GLITCH;
m_weakness = AmmoType::KINETIC;
m_color = CyberColors::NeonPurple;
m_color = TacticalColors::BrightRed;
break;
case EnemyType::VIRUS_CONSTRUCT:
@ -42,7 +42,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
m_baseDamage = 15 + level * 3;
m_attackElement = AmmoType::CORROSIVE;
m_weakness = AmmoType::OVERHEAT;
m_color = CyberColors::NeonGreen;
m_color = TacticalColors::DarkRed;
break;
}
@ -137,4 +137,17 @@ bool Enemy::isStunned() const {
return false;
}
void Enemy::setElite(bool elite) {
if (m_isElite == elite) return;
m_isElite = elite;
if (m_isElite) {
m_name = "[ELITE] " + m_name;
m_maxHp = static_cast<int>(m_maxHp * 1.5f);
m_hp = m_maxHp;
m_maxShield = static_cast<int>(m_maxShield * 1.3f);
m_shield = m_maxShield;
m_baseDamage = static_cast<int>(m_baseDamage * 1.25f);
}
}
} // namespace dnr

View file

@ -29,6 +29,8 @@ public:
Color getColor() const { return m_color; }
bool isAlive() const { return m_hp > 0; }
bool isElite() const { return m_isElite; }
void setElite(bool elite);
// Combat interactions
int takeDamage(int amount, AmmoType element);
@ -53,6 +55,7 @@ private:
AmmoType m_attackElement = AmmoType::OVERHEAT;
AmmoType m_weakness = AmmoType::EMP;
Color m_color = RED;
bool m_isElite = false;
std::vector<ActiveStatus> m_statuses;
};

View file

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

View file

@ -23,6 +23,12 @@ void Game::init() {
void Game::startNewRun() {
m_dungeon.setFloor(1);
m_player.reset(3, 4, Direction::NORTH);
m_player.resetImplants();
m_showImplantDraft = false;
m_showShrineUI = false;
m_activeShrine = nullptr;
m_isDefusingMine = false;
m_activeMine = nullptr;
m_hubRoom.init(m_player);
m_state = GameState::HUB;
SoundManager::get().playTrack(MusicTrack::MENU_HUB);
@ -86,6 +92,19 @@ bool Game::loadSavedProgress() {
m_player.setAmmoTier(static_cast<AmmoType>(i), data.ammoTiers[i]);
}
// Restore Neural Implants & Defibrillator state
std::vector<ImplantType> imps;
for (int val : data.implants) {
imps.push_back(static_cast<ImplantType>(val));
}
m_player.setImplants(imps);
m_player.setDefibrillatorUsed(data.defibrillatorUsed);
m_showImplantDraft = false;
m_showShrineUI = false;
m_activeShrine = nullptr;
m_isDefusingMine = false;
m_activeMine = nullptr;
if (data.inHub) {
m_hubRoom.init(m_player);
m_state = GameState::HUB;
@ -108,6 +127,12 @@ void Game::update(float dt) {
m_saveFeedbackTimer -= dt;
}
// Update screen transition wave
m_transition.update(dt);
if (m_transition.isBusy()) {
return;
}
// Update dynamic music streaming engine every frame
SoundManager::get().updateMusic(dt);
@ -141,12 +166,12 @@ void Game::update(float dt) {
break;
case GameState::VICTORY_LEVEL:
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
enterRestRoom();
m_transition.start([this]() { enterRestRoom(); });
}
break;
case GameState::GAME_OVER:
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
startNewRun();
m_transition.start([this]() { startNewRun(); });
}
break;
}
@ -166,9 +191,9 @@ void Game::updateMainMenu(float dt) {
bool hasSave = SaveSystem::hasSaveFile();
if ((IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btnContinue))) && hasSave) {
loadSavedProgress();
m_transition.start([this]() { loadSavedProgress(); });
} else if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btnNewRun))) {
startNewRun();
m_transition.start([this]() { startNewRun(); });
} else if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btnSettings))) {
m_previousState = GameState::MAIN_MENU;
m_state = GameState::SETTINGS;
@ -238,7 +263,7 @@ void Game::tryMoveHub(int dx, int dy) {
void Game::tryInteractHub() {
if (m_hubRoom.isFacingPortal(m_player)) {
enterCyberspace();
m_transition.start([this]() { enterCyberspace(); });
} else if (m_hubRoom.isFacingTerminal(m_player)) {
saveCurrentProgress();
}
@ -367,7 +392,7 @@ void Game::tryMoveRestRoom(int dx, int dy) {
void Game::tryInteractRestRoom() {
if (m_restRoom.isFacingGateway(m_player)) {
nextFloor();
m_transition.start([this]() { nextFloor(); });
} else if (m_restRoom.isFacingRechargeMatrix(m_player)) {
if (!m_restRoom.isRecharged()) {
m_restRoom.triggerRecharge(m_player);
@ -381,10 +406,183 @@ void Game::tryInteractRestRoom() {
}
}
void Game::triggerImplantDraft() {
std::vector<ImplantType> available;
for (int i = 0; i < 12; ++i) {
ImplantType t = static_cast<ImplantType>(i);
if (!m_player.hasImplant(t)) {
available.push_back(t);
}
}
if (available.empty()) {
m_player.addTechScraps(50);
return;
}
for (size_t i = available.size() - 1; i > 0; --i) {
size_t j = std::rand() % (i + 1);
std::swap(available[i], available[j]);
}
m_draftChoices.clear();
for (size_t i = 0; i < available.size() && i < 3; ++i) {
m_draftChoices.push_back(available[i]);
}
m_showImplantDraft = true;
m_draftReplaceSlot = -1;
SoundManager::get().playSkillGreat();
}
void Game::handleImplantDraftInput() {
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
if (m_player.getImplants().size() >= 4) {
int modalW = 960;
int modalH = 550;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
int mountY = modalY + 332;
int slotW = 212;
int slotH = 68;
int slotGap = 16;
int startSlotX = modalX + 30;
for (int s = 0; s < 4; ++s) {
int slX = startSlotX + s * (slotW + slotGap);
int slY = mountY + 20;
Rectangle slotRect = { static_cast<float>(slX), static_cast<float>(slY), static_cast<float>(slotW), static_cast<float>(slotH) };
if (mouseClicked && CheckCollisionPointRec(mouse, slotRect)) {
m_draftReplaceSlot = s;
SoundManager::get().playTumblerClick();
return;
}
}
}
for (size_t i = 0; i < m_draftChoices.size() && i < 3; ++i) {
Rectangle btn = UIRenderer::getDraftCardButtonRect(static_cast<int>(i), screenW, screenH);
bool keyPressed = (i == 0 && IsKeyPressed(KEY_ONE)) ||
(i == 1 && IsKeyPressed(KEY_TWO)) ||
(i == 2 && IsKeyPressed(KEY_THREE));
if (keyPressed || (mouseClicked && CheckCollisionPointRec(mouse, btn))) {
ImplantType chosen = m_draftChoices[i];
if (m_player.getImplants().size() < 4) {
m_player.addImplant(chosen);
m_showImplantDraft = false;
SoundManager::get().playPickup();
return;
} else {
if (m_draftReplaceSlot >= 0 && m_draftReplaceSlot < 4) {
m_player.replaceImplant(m_draftReplaceSlot, chosen);
m_showImplantDraft = false;
m_draftReplaceSlot = -1;
SoundManager::get().playPickup();
return;
} else {
SoundManager::get().playDamage();
}
}
}
}
Rectangle scrapBtn = UIRenderer::getDraftScrapButtonRect(screenW, screenH);
if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, scrapBtn))) {
m_player.addTechScraps(35);
m_showImplantDraft = false;
m_draftReplaceSlot = -1;
SoundManager::get().playTumblerClick();
}
}
void Game::handleShrineInput() {
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
Rectangle btn0 = UIRenderer::getShrineOptionButtonRect(0, screenW, screenH);
if (IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btn0))) {
if (m_player.getHp() > 25) {
m_player.takeDamage(25);
m_player.addTechScraps(60);
m_player.addAmmo(AmmoType::KINETIC, 1);
m_player.addAmmo(AmmoType::GLITCH, 1);
if (m_activeShrine) m_activeShrine->used = true;
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playSkillGreat();
return;
} else {
SoundManager::get().playDamage();
}
}
Rectangle btn1 = UIRenderer::getShrineOptionButtonRect(1, screenW, screenH);
if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btn1))) {
if (m_player.getCyberPicks() >= 1) {
m_player.usePick();
m_player.increaseMaxShield(15);
m_player.rechargeShield(m_player.getMaxShield());
if (m_activeShrine) m_activeShrine->used = true;
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playSkillGreat();
return;
} else {
SoundManager::get().playDamage();
}
}
Rectangle btn2 = UIRenderer::getShrineOptionButtonRect(2, screenW, screenH);
if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btn2))) {
if (m_player.getAmmoCount(AmmoType::KINETIC) >= 2) {
m_player.consumeAmmo(AmmoType::KINETIC);
m_player.consumeAmmo(AmmoType::KINETIC);
m_player.addAmmo(AmmoType::NANITE, 1);
m_player.addAmmo(AmmoType::GLITCH, 1);
if (m_activeShrine) m_activeShrine->used = true;
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playSkillGreat();
return;
} else {
SoundManager::get().playDamage();
}
}
Rectangle btnClose = UIRenderer::getShrineCloseButtonRect(screenW, screenH);
if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, btnClose))) {
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playTumblerClick();
}
}
void Game::updateExploration(float dt) {
if (m_showImplantDraft) {
handleImplantDraftInput();
return;
}
if (m_showShrineUI) {
handleShrineInput();
return;
}
m_player.update(dt);
m_dungeon.reveal(m_player.getGridX(), m_player.getGridY(), 3);
// Update laser barrier timers
for (auto& laser : m_dungeon.getLasers()) {
if (laser.disarmed) continue;
laser.timer += dt;
if (laser.timer >= laser.period) {
laser.timer -= laser.period;
}
laser.active = (laser.timer < 2.0f);
}
Vector2 dirVec = getDirectionOffset(m_player.getFacing());
int frontX = m_player.getGridX() + static_cast<int>(dirVec.x);
int frontY = m_player.getGridY() + static_cast<int>(dirVec.y);
@ -392,12 +590,25 @@ void Game::updateExploration(float dt) {
m_contextPrompt = "";
if (m_dungeon.isDoorLocked(frontX, frontY)) {
m_contextPrompt = "[SPACE / E] Bypass Door ICE [Picks: " + std::to_string(m_player.getCyberPicks()) + "]";
} else if (m_dungeon.getTile(frontX, frontY) == TileType::LOOT_CACHE ||
m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::LOOT_CACHE) {
m_contextPrompt = "[SPACE / E] Extract Data Cache";
} else if (m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::EXIT_PORTAL ||
m_dungeon.getTile(frontX, frontY) == TileType::EXIT_PORTAL) {
m_contextPrompt = "[SPACE / E] Jack Out To Safe Haven Rest Node";
} else {
LaserBarrier* frontLaser = m_dungeon.getLaserAt(frontX, frontY);
ProximityMine* frontMine = m_dungeon.getMineAt(frontX, frontY);
CyberShrine* frontShrine = m_dungeon.getShrineAt(frontX, frontY);
if (!frontShrine) frontShrine = m_dungeon.getShrineAt(m_player.getGridX(), m_player.getGridY());
if (frontLaser && !frontLaser->disarmed) {
m_contextPrompt = "[SPACE / E] Disarm Laser Barrier (Cost: 1 ICE Pick)";
} else if (frontMine && frontMine->isArmed()) {
m_contextPrompt = "[SPACE / E] Defuse Proximity Mine (Quick DBD Timing Check)";
} else if (frontShrine && !frontShrine->used) {
m_contextPrompt = "[SPACE / E] Access Darknet Cyber Shrine";
} else if (m_dungeon.getTile(frontX, frontY) == TileType::LOOT_CACHE ||
m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::LOOT_CACHE) {
m_contextPrompt = "[SPACE / E] Extract Data Cache";
} else if (m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::EXIT_PORTAL ||
m_dungeon.getTile(frontX, frontY) == TileType::EXIT_PORTAL) {
m_contextPrompt = "[SPACE / E] Jack Out To Safe Haven Rest Node";
}
}
if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) {
@ -448,7 +659,9 @@ void Game::updateExploration(float dt) {
m_showCombatCodex = false;
m_lastPlayerHp = m_player.getHp();
SoundManager::get().playTrack(MusicTrack::COMBAT);
m_combat.startCombat(m_player, std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor()));
auto enemyPtr = std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor());
if (me->enemy.isElite()) enemyPtr->setElite(true);
m_combat.startCombat(m_player, std::move(enemyPtr));
m_dungeon.removeEnemyAt(me->x, me->y);
}
}
@ -466,8 +679,36 @@ void Game::tryMoveDungeon(int dx, int dy) {
m_showCombatCodex = false;
m_lastPlayerHp = m_player.getHp();
SoundManager::get().playTrack(MusicTrack::COMBAT);
m_combat.startCombat(m_player, std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor()));
auto enemyPtr = std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor());
if (me->enemy.isElite()) enemyPtr->setElite(true);
m_combat.startCombat(m_player, std::move(enemyPtr));
m_dungeon.removeEnemyAt(nx, ny);
return;
}
// Check if stepped on laser barrier
LaserBarrier* lb = m_dungeon.getLaserAt(nx, ny);
if (lb && !lb->disarmed && lb->active) {
m_player.takeDamage(15, AmmoType::EMPTY);
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
SoundManager::get().playDamage();
if (m_player.isDead()) {
m_state = GameState::GAME_OVER;
return;
}
}
// Check if stepped on proximity mine
ProximityMine* pm = m_dungeon.getMineAt(nx, ny);
if (pm && pm->isArmed()) {
pm->triggered = true;
m_player.takeDamage(25, AmmoType::EMPTY);
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
SoundManager::get().playDamage();
if (m_player.isDead()) {
m_state = GameState::GAME_OVER;
return;
}
}
} else if (m_dungeon.isDoorLocked(nx, ny)) {
m_targetDoorX = nx;
@ -489,7 +730,8 @@ void Game::tryInteractDungeon() {
m_targetDoorY = fy;
DoorData* dd = m_dungeon.getDoorAt(fx, fy);
int sec = dd ? dd->securityLevel : 1;
m_hacking.start(sec);
bool expanded = m_player.hasImplant(ImplantType::LOGIC_OVERCLOCK);
m_hacking.start(sec, expanded);
m_state = GameState::HACKING;
SoundManager::get().playTumblerClick();
} else {
@ -498,6 +740,38 @@ void Game::tryInteractDungeon() {
return;
}
LaserBarrier* lb = m_dungeon.getLaserAt(fx, fy);
if (lb && !lb->disarmed) {
if (m_player.getCyberPicks() > 0) {
m_player.usePick();
lb->disarmed = true;
SoundManager::get().playDoorOpen();
} else {
SoundManager::get().playDamage();
}
return;
}
ProximityMine* pm = m_dungeon.getMineAt(fx, fy);
if (pm && pm->isArmed()) {
m_activeMine = pm;
m_isDefusingMine = true;
bool expanded = m_player.hasImplant(ImplantType::LOGIC_OVERCLOCK);
m_hacking.startSkillCheckOnly(1, expanded);
m_state = GameState::HACKING;
SoundManager::get().playTumblerClick();
return;
}
CyberShrine* cs = m_dungeon.getShrineAt(fx, fy);
if (!cs) cs = m_dungeon.getShrineAt(px, py);
if (cs && !cs->used) {
m_activeShrine = cs;
m_showShrineUI = true;
SoundManager::get().playSkillGreat();
return;
}
LootData* ld = m_dungeon.getLootAt(fx, fy);
if (!ld) ld = m_dungeon.getLootAt(px, py);
@ -515,7 +789,7 @@ void Game::tryInteractDungeon() {
if (m_dungeon.getTile(px, py) == TileType::EXIT_PORTAL ||
m_dungeon.getTile(fx, fy) == TileType::EXIT_PORTAL) {
enterRestRoom();
m_transition.start([this]() { enterRestRoom(); });
return;
}
}
@ -532,6 +806,37 @@ void Game::updateHacking(float dt) {
m_hacking.handleInput();
if (m_hacking.isFinished() && m_hacking.getResultTimer() > 1.2f) {
if (m_isDefusingMine) {
m_isDefusingMine = false;
if (m_hacking.isSuccess()) {
if (m_activeMine) {
m_activeMine->defused = true;
}
m_player.addTechScraps(20);
m_player.addAmmo(AmmoType::KINETIC, 1);
if (m_hacking.isCritical()) {
m_player.addTechScraps(15);
m_player.addAmmo(AmmoType::EMP, 1);
}
SoundManager::get().playPickup();
} else {
if (m_activeMine) {
m_activeMine->triggered = true;
}
m_player.takeDamage(25, AmmoType::EMPTY);
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
SoundManager::get().playDamage();
if (m_player.isDead()) {
m_activeMine = nullptr;
m_state = GameState::GAME_OVER;
return;
}
}
m_activeMine = nullptr;
m_state = GameState::EXPLORATION;
return;
}
if (m_hacking.isSuccess()) {
m_dungeon.unlockDoorAt(m_targetDoorX, m_targetDoorY);
SoundManager::get().playDoorOpen();
@ -540,6 +845,9 @@ void Game::updateHacking(float dt) {
m_player.addAmmo(AmmoType::NANITE, 1);
m_player.addPicks(1);
m_player.addTechScraps(40);
if (m_hacking.getSecurityLevel() >= 3) {
triggerImplantDraft();
}
} else {
m_player.addTechScraps(15);
}
@ -628,11 +936,15 @@ void Game::updateCombat(float dt) {
}
} else if (m_combat.getPhase() == CombatPhase::VICTORY) {
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) || mouseClicked) {
bool wasElite = m_combat.wasElite();
m_player.heal(25);
m_player.addAmmo(AmmoType::KINETIC, 1);
m_player.addAmmo(AmmoType::EMP, 1);
m_state = GameState::EXPLORATION;
SoundManager::get().playTrack(MusicTrack::EXPLORATION, true);
if (wasElite) {
triggerImplantDraft();
}
}
} else if (m_combat.getPhase() == CombatPhase::DEFEAT) {
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) || mouseClicked) {
@ -795,6 +1107,11 @@ void Game::draw() {
case GameState::EXPLORATION:
m_renderer.render3D(m_dungeon, m_player, m_animTime);
m_uiRenderer.drawExplorationHUD(m_dungeon, m_player, m_contextPrompt);
if (m_showImplantDraft) {
m_uiRenderer.drawImplantDraftModal(m_draftChoices, m_player, m_draftReplaceSlot);
} else if (m_showShrineUI) {
m_uiRenderer.drawShrineModal(m_player);
}
m_uiRenderer.drawMatrixOverlay(m_animTime);
break;
@ -863,6 +1180,9 @@ void Game::draw() {
break;
}
// Render screen transition wave on top of all 2D/3D elements
m_transition.draw();
EndDrawing();
}

View file

@ -8,6 +8,7 @@
#include "inventory/InventoryUI.hpp"
#include "render/CyberRenderer.hpp"
#include "render/UIRenderer.hpp"
#include "render/ScreenTransition.hpp"
#include "hub/HubRoom.hpp"
#include "hub/RestRoom.hpp"
#include "core/SaveSystem.hpp"
@ -64,6 +65,7 @@ private:
CyberRenderer m_renderer;
UIRenderer m_uiRenderer;
ScreenTransition m_transition;
float m_animTime = 0.0f;
float m_saveFeedbackTimer = 0.0f;
@ -77,9 +79,23 @@ private:
bool m_showCombatCodex = false;
bool m_showWorkbenchUI = false;
int m_lastPlayerHp = 100;
void triggerImplantDraft();
void handleImplantDraftInput();
void handleShrineInput();
int m_lastPlayerHp = 100;
bool m_shouldQuit = false;
// Roguelike Implants, Shrines & Hazard states
bool m_showImplantDraft = false;
std::vector<ImplantType> m_draftChoices;
int m_draftReplaceSlot = -1;
bool m_showShrineUI = false;
CyberShrine* m_activeShrine = nullptr;
bool m_isDefusingMine = false;
ProximityMine* m_activeMine = nullptr;
};
} // namespace dnr

View file

@ -38,6 +38,14 @@ bool SaveSystem::saveGame(const std::string& path, int floor, bool inHub, const
}
out << "\n";
out << "implants " << player.getImplants().size();
for (auto imp : player.getImplants()) {
out << " " << static_cast<int>(imp);
}
out << "\n";
out << "defibUsed " << (player.isDefibrillatorUsed() ? 1 : 0) << "\n";
return true;
}
@ -77,6 +85,19 @@ bool SaveSystem::loadGame(const std::string& path, SaveData& outData) {
for (size_t i = 0; i < outData.ammoTiers.size(); ++i) {
in >> outData.ammoTiers[i];
}
} else if (key == "implants") {
int count = 0;
in >> count;
outData.implants.clear();
for (int i = 0; i < count; ++i) {
int impVal = 0;
in >> impVal;
outData.implants.push_back(impVal);
}
} else if (key == "defibUsed") {
int du = 0;
in >> du;
outData.defibrillatorUsed = (du != 0);
}
}

View file

@ -20,6 +20,8 @@ struct SaveData {
std::array<int, 6> chambers{};
std::array<int, 7> ammoCounts{};
std::array<int, 7> ammoTiers{ 1, 1, 1, 1, 1, 1, 1 };
std::vector<int> implants{};
bool defibrillatorUsed = false;
};
class SaveSystem {

View file

@ -13,6 +13,9 @@ void Dungeon::clear() {
m_doors.clear();
m_loots.clear();
m_enemies.clear();
m_lasers.clear();
m_mines.clear();
m_shrines.clear();
}
void Dungeon::resize(int width, int height) {
@ -77,8 +80,59 @@ void Dungeon::addLoot(int x, int y, const std::vector<AmmoType>& ammo, int picks
setTile(x, y, TileType::LOOT_CACHE);
}
void Dungeon::addEnemy(int x, int y, EnemyType type, int level) {
m_enemies.push_back({ x, y, Enemy(type, level), true });
void Dungeon::addEnemy(int x, int y, EnemyType type, int level, bool elite) {
Enemy en(type, level);
if (elite) en.setElite(true);
m_enemies.push_back({ x, y, en, true });
}
void Dungeon::addLaser(int x, int y, bool vertical) {
LaserBarrier lb;
lb.x = x;
lb.y = y;
lb.vertical = vertical;
lb.active = true;
lb.timer = 0.0f;
lb.disarmed = false;
m_lasers.push_back(lb);
}
void Dungeon::addMine(int x, int y) {
ProximityMine pm;
pm.x = x;
pm.y = y;
pm.defused = false;
pm.triggered = false;
m_mines.push_back(pm);
}
void Dungeon::addShrine(int x, int y) {
CyberShrine cs;
cs.x = x;
cs.y = y;
cs.used = false;
m_shrines.push_back(cs);
}
LaserBarrier* Dungeon::getLaserAt(int x, int y) {
for (auto& l : m_lasers) {
if (l.x == x && l.y == y && !l.disarmed) return &l;
}
return nullptr;
}
ProximityMine* Dungeon::getMineAt(int x, int y) {
for (auto& m : m_mines) {
if (m.x == x && m.y == y && !m.defused && !m.triggered) return &m;
}
return nullptr;
}
CyberShrine* Dungeon::getShrineAt(int x, int y) {
for (auto& s : m_shrines) {
if (s.x == x && s.y == y) return &s;
}
return nullptr;
}
void Dungeon::removeEnemyAt(int x, int y) {

View file

@ -54,7 +54,15 @@ public:
void addDoor(int x, int y, int secLevel);
void addLoot(int x, int y, const std::vector<AmmoType>& ammo, int picks);
void addEnemy(int x, int y, EnemyType type, int level);
void addEnemy(int x, int y, EnemyType type, int level, bool elite = false);
void addLaser(int x, int y, bool vertical);
void addMine(int x, int y);
void addShrine(int x, int y);
LaserBarrier* getLaserAt(int x, int y);
ProximityMine* getMineAt(int x, int y);
CyberShrine* getShrineAt(int x, int y);
void removeEnemyAt(int x, int y);
void unlockDoorAt(int x, int y);
@ -65,6 +73,13 @@ public:
const std::vector<DoorData>& getDoors() const { return m_doors; }
const std::vector<LootData>& getLoots() const { return m_loots; }
std::vector<MapEnemy>& getEnemies() { return m_enemies; }
const std::vector<MapEnemy>& getEnemies() const { return m_enemies; }
std::vector<LaserBarrier>& getLasers() { return m_lasers; }
const std::vector<LaserBarrier>& getLasers() const { return m_lasers; }
std::vector<ProximityMine>& getMines() { return m_mines; }
const std::vector<ProximityMine>& getMines() const { return m_mines; }
std::vector<CyberShrine>& getShrines() { return m_shrines; }
const std::vector<CyberShrine>& getShrines() const { return m_shrines; }
private:
int m_width;
@ -76,6 +91,9 @@ private:
std::vector<DoorData> m_doors;
std::vector<LootData> m_loots;
std::vector<MapEnemy> m_enemies;
std::vector<LaserBarrier> m_lasers;
std::vector<ProximityMine> m_mines;
std::vector<CyberShrine> m_shrines;
};
} // namespace dnr

View file

@ -224,12 +224,79 @@ DungeonGenResult DungeonGen::generate(Dungeon& dungeon, int floor) {
dungeon.addLoot(lx, ly, rewards, picks);
}
// Place enemy
// Place enemy (at least 1-2 become ELITE)
EnemyType eType = static_cast<EnemyType>(std::rand() % 4);
int ex = cx;
int ey = cy;
if (dungeon.getTile(ex, ey) == TileType::FLOOR && (ex != res.exitX || ey != res.exitY) && (ex != res.startX || ey != res.startY)) {
dungeon.addEnemy(ex, ey, eType, floor);
bool isElite = (i == rooms.size() - 1) || (floor >= 2 && i == 2);
dungeon.addEnemy(ex, ey, eType, floor, isElite);
}
}
// Place 1 Cyber Shrine in a room distinct from start and exit
if (rooms.size() >= 3) {
size_t shrineRoomIdx = (rooms.size() > 3) ? rooms.size() - 2 : 1;
const auto& sr = rooms[shrineRoomIdx];
int sx = sr.x + 1;
int sy = sr.y + 1;
if (dungeon.getTile(sx, sy) == TileType::FLOOR && (sx != res.startX || sy != res.startY) && (sx != res.exitX || sy != res.exitY)) {
dungeon.addShrine(sx, sy);
}
}
// Identify corridor tiles for laser barriers and mines
std::vector<std::pair<int, int>> vertCorridors;
std::vector<std::pair<int, int>> horizCorridors;
for (int y = 2; y < H - 2; ++y) {
for (int x = 2; x < W - 2; ++x) {
if (dungeon.getTile(x, y) == TileType::FLOOR) {
if ((x == res.startX && y == res.startY) || (x == res.exitX && y == res.exitY)) continue;
if (dungeon.getShrineAt(x, y) || dungeon.getEnemyAt(x, y) || dungeon.getDoorAt(x, y) || dungeon.getLootAt(x, y)) continue;
bool wallLeft = (dungeon.getTile(x - 1, y) == TileType::WALL);
bool wallRight = (dungeon.getTile(x + 1, y) == TileType::WALL);
bool wallUp = (dungeon.getTile(x, y - 1) == TileType::WALL);
bool wallDown = (dungeon.getTile(x, y + 1) == TileType::WALL);
if (wallLeft && wallRight && !wallUp && !wallDown) {
vertCorridors.push_back({ x, y });
} else if (wallUp && wallDown && !wallLeft && !wallRight) {
horizCorridors.push_back({ x, y });
}
}
}
}
// Place 2-3 Laser barriers
int lasersPlaced = 0;
while (!vertCorridors.empty() && lasersPlaced < 2) {
int idx = std::rand() % vertCorridors.size();
auto pt = vertCorridors[idx];
vertCorridors.erase(vertCorridors.begin() + idx);
dungeon.addLaser(pt.first, pt.second, false);
lasersPlaced++;
}
while (!horizCorridors.empty() && lasersPlaced < 3) {
int idx = std::rand() % horizCorridors.size();
auto pt = horizCorridors[idx];
horizCorridors.erase(horizCorridors.begin() + idx);
dungeon.addLaser(pt.first, pt.second, true);
lasersPlaced++;
}
// Place 2 Proximity Mines
int minesPlaced = 0;
std::vector<std::pair<int, int>> allCorridors = vertCorridors;
allCorridors.insert(allCorridors.end(), horizCorridors.begin(), horizCorridors.end());
while (!allCorridors.empty() && minesPlaced < 2) {
int idx = std::rand() % allCorridors.size();
auto pt = allCorridors[idx];
allCorridors.erase(allCorridors.begin() + idx);
if (!dungeon.getLaserAt(pt.first, pt.second)) {
dungeon.addMine(pt.first, pt.second);
minesPlaced++;
}
}

View file

@ -27,6 +27,8 @@ void Player::reset(int startX, int startY, Direction startDir) {
m_revolver.reset();
m_statuses.clear();
m_implants.clear();
m_defibrillatorUsed = false;
// Reset ammo pouch counts with a starter kit
m_ammoCounts.fill(0);
@ -233,4 +235,37 @@ int Player::getEffectiveDamage(AmmoType type) const {
return getAmmoTierData(type, t).damage;
}
bool Player::hasImplant(ImplantType type) const {
for (auto imp : m_implants) {
if (imp == type) return true;
}
return false;
}
bool Player::addImplant(ImplantType type) {
if (hasImplant(type)) return false;
if (m_implants.size() >= MAX_IMPLANTS) return false;
m_implants.push_back(type);
return true;
}
void Player::replaceImplant(size_t slotIndex, ImplantType type) {
if (slotIndex < m_implants.size()) {
m_implants[slotIndex] = type;
}
}
void Player::removeImplant(size_t slotIndex) {
if (slotIndex < m_implants.size()) {
m_implants.erase(m_implants.begin() + slotIndex);
}
}
void Player::setImplants(const std::vector<ImplantType>& implants) {
m_implants = implants;
if (m_implants.size() > MAX_IMPLANTS) {
m_implants.resize(MAX_IMPLANTS);
}
}
} // namespace dnr

View file

@ -41,10 +41,12 @@ public:
void addPicks(int amount) { m_cyberPicks += amount; }
bool usePick();
int takeDamage(int amount, AmmoType element);
void applyStatus(StatusEffect effect, int duration, int power);
int takeDamage(int amount, AmmoType element = AmmoType::EMPTY);
void applyStatus(StatusEffect effect, int duration, int power = 1);
void rechargeShield(int amount) { restoreShield(amount); }
void updateTurnStart(std::vector<std::string>& combatLog);
bool isAlive() const { return m_hp > 0; }
bool isDead() const { return m_hp <= 0; }
// Weapon & Ammo
Revolver& getRevolver() { return m_revolver; }
@ -82,6 +84,23 @@ public:
const std::vector<ActiveStatus>& getStatuses() const { return m_statuses; }
// Neural Implants (Max 4 slots)
static constexpr size_t MAX_IMPLANTS = 4;
const std::vector<ImplantType>& getImplants() const { return m_implants; }
bool hasImplant(ImplantType type) const;
bool addImplant(ImplantType type);
void replaceImplant(size_t slotIndex, ImplantType type);
void removeImplant(size_t slotIndex);
void setImplants(const std::vector<ImplantType>& implants);
void resetImplants() { m_implants.clear(); m_defibrillatorUsed = false; }
bool isDefibrillatorUsed() const { return m_defibrillatorUsed; }
void setDefibrillatorUsed(bool used) { m_defibrillatorUsed = used; }
void increaseMaxShield(int bonus) {
m_maxShield += bonus;
m_shield += bonus;
}
private:
int m_gridX = 1;
int m_gridY = 1;
@ -102,6 +121,8 @@ private:
std::array<int, 7> m_ammoTiers{ 1, 1, 1, 1, 1, 1, 1 };
Revolver m_revolver;
std::vector<ActiveStatus> m_statuses;
std::vector<ImplantType> m_implants;
bool m_defibrillatorUsed = false;
};
} // namespace dnr

View file

@ -9,7 +9,7 @@ DbdSkillCheck::DbdSkillCheck() {
init(1.0f);
}
void DbdSkillCheck::init(float speedMultiplier) {
void DbdSkillCheck::init(float speedMultiplier, float greatMultiplier) {
m_needleAngle = 0.0f;
m_speed = (180.0f + static_cast<float>(std::rand() % 40)) * speedMultiplier;
@ -17,8 +17,8 @@ void DbdSkillCheck::init(float speedMultiplier) {
m_successStart = 120.0f + static_cast<float>(std::rand() % 100);
m_successSpan = std::max(25.0f, 48.0f - (speedMultiplier - 1.0f) * 12.0f);
// Great success is at the end or center of the success span
m_greatSpan = std::max(10.0f, m_successSpan * 0.32f);
// Great success is at the end or center of the success span (expanded if greatMultiplier > 1)
m_greatSpan = std::min(m_successSpan * 0.85f, std::max(10.0f, m_successSpan * 0.32f * greatMultiplier));
m_greatStart = m_successStart + m_successSpan - m_greatSpan;
m_warningPlayed = false;

View file

@ -15,7 +15,7 @@ class DbdSkillCheck {
public:
DbdSkillCheck();
void init(float speedMultiplier = 1.0f);
void init(float speedMultiplier = 1.0f, float greatMultiplier = 1.0f);
void update(float dt);
SkillCheckResult strike(); // Triggered on Space press

View file

@ -5,7 +5,7 @@ namespace dnr {
HackingGame::HackingGame() = default;
void HackingGame::start(int securityLevel) {
void HackingGame::start(int securityLevel, bool expandedGreatZone) {
m_securityLevel = securityLevel;
m_phase = HackingPhase::PHASE_1_TUMBLER;
m_resultTimer = 0.0f;
@ -16,7 +16,18 @@ void HackingGame::start(int securityLevel) {
m_tumbler.init(pinCount, difficulty);
float skillSpeed = 0.9f + securityLevel * 0.25f;
m_skillCheck.init(skillSpeed);
float greatMult = expandedGreatZone ? 2.0f : 1.0f;
m_skillCheck.init(skillSpeed, greatMult);
}
void HackingGame::startSkillCheckOnly(int securityLevel, bool expandedGreatZone) {
m_securityLevel = securityLevel;
m_phase = HackingPhase::PHASE_2_SKILLCHECK;
m_resultTimer = 0.0f;
float skillSpeed = 0.85f + securityLevel * 0.20f;
float greatMult = expandedGreatZone ? 2.0f : 1.0f;
m_skillCheck.init(skillSpeed, greatMult);
}
void HackingGame::update(float dt) {

View file

@ -18,7 +18,8 @@ class HackingGame {
public:
HackingGame();
void start(int securityLevel);
void start(int securityLevel, bool expandedGreatZone = false);
void startSkillCheckOnly(int securityLevel, bool expandedGreatZone = false);
void update(float dt);
void handleInput();

View file

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

File diff suppressed because it is too large Load diff

View file

@ -32,7 +32,7 @@ private:
void beginPostProcess();
void endPostProcess(float animTime);
void renderCyberDrone(Vector3 pos, Color coreColor, float scale, float animTime, bool isHit = false, bool isAttacking = false);
void renderCyberDrone(Vector3 pos, Color coreColor, float scale, float animTime, bool isHit = false, bool isAttacking = false, bool isElite = false);
Camera3D m_camera{};
bool m_texturesLoaded = false;
@ -63,6 +63,7 @@ private:
Texture2D m_texMonitorRadar{};
Texture2D m_texBedQuilt{};
Texture2D m_texCityNight{};
Texture2D m_texWindowView{};
Texture2D m_texCyberGate{};
// Ancient Palace Storeroom Procedural Textures

View file

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

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,16 @@ public:
void drawWorkbenchModal(const Player& player);
static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH);
static Rectangle getWorkbenchCloseButtonRect(int screenW, int screenH);
static void drawWrappedText(const std::string& text, int posX, int posY, int maxW, int fontSize, int lineSpacing, Color color);
void drawImplantDraftModal(const std::vector<ImplantType>& choices, const Player& player, int selectedReplaceSlot = -1);
static Rectangle getDraftCardButtonRect(int cardIndex, int screenW, int screenH);
static Rectangle getDraftScrapButtonRect(int screenW, int screenH);
void drawShrineModal(const Player& player);
static Rectangle getShrineOptionButtonRect(int optionIndex, int screenW, int screenH);
static Rectangle getShrineCloseButtonRect(int screenW, int screenH);
void drawHackingUI(const HackingGame& hacking);
void drawCombatUI(const CombatSystem& combat, const Player& player, float animTime, bool showCodex = false);
void drawCodexModal();