diff --git a/src/combat/CombatSystem.cpp b/src/combat/CombatSystem.cpp index 911bce8..902554f 100644 --- a/src/combat/CombatSystem.cpp +++ b/src/combat/CombatSystem.cpp @@ -11,11 +11,16 @@ void CombatSystem::startCombat(Player& player, std::unique_ptr enemy) { m_phase = CombatPhase::PLAYER_TURN; m_phaseTimer = 0.0f; m_shieldBoosted = false; + m_ghostBonusDmg = false; + m_defeatedEnemyWasElite = false; m_tripleCooldown = 0; 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 +40,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(chips * 2.0f); + if (p.hasImplant(ImplantType::SALVAGE_PROTOCOL)) chips = static_cast(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(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 +80,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 +90,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 +100,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 +167,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 +176,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(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 +223,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(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!"); @@ -208,6 +273,18 @@ 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); } } @@ -220,6 +297,13 @@ void CombatSystem::resolveShotImpact(Player& player) { 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 +311,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(dmg * 1.3f); + } + if (m_ghostBonusDmg) { + dmg = static_cast(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 +332,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 +396,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); diff --git a/src/combat/CombatSystem.hpp b/src/combat/CombatSystem.hpp index 3db1475..e70332c 100644 --- a/src/combat/CombatSystem.hpp +++ b/src/combat/CombatSystem.hpp @@ -50,6 +50,8 @@ 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); @@ -70,6 +72,9 @@ private: int m_burstShotsFired = 0; bool m_shieldBoosted = false; + bool m_ghostBonusDmg = false; + bool m_defeatedEnemyWasElite = false; + std::vector m_combatLog; std::vector m_floatingTexts; }; diff --git a/src/combat/Enemy.cpp b/src/combat/Enemy.cpp index 99e3138..5eb9990 100644 --- a/src/combat/Enemy.cpp +++ b/src/combat/Enemy.cpp @@ -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(m_maxHp * 1.5f); + m_hp = m_maxHp; + m_maxShield = static_cast(m_maxShield * 1.3f); + m_shield = m_maxShield; + m_baseDamage = static_cast(m_baseDamage * 1.25f); + } +} + } // namespace dnr diff --git a/src/combat/Enemy.hpp b/src/combat/Enemy.hpp index cdccdbc..4438d03 100644 --- a/src/combat/Enemy.hpp +++ b/src/combat/Enemy.hpp @@ -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 m_statuses; }; diff --git a/src/core/Common.hpp b/src/core/Common.hpp index c800769..51b831c 100644 --- a/src/core/Common.hpp +++ b/src/core/Common.hpp @@ -209,6 +209,126 @@ struct ActiveStatus { int power = 0; }; +// ------------------------------------------------------------- +// 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; +}; + +inline ImplantData getImplantData(ImplantType type) { + switch (type) { + case ImplantType::GHOST_CHAMBER: + return { type, "GHOST CHAMBER", "ROULETTE", ImplantArchetype::CYLINDER, + "Empty chamber gives +20 Shield and +50% damage on next shot.", + Color{ 0, 240, 255, 255 }, "[GHOST]", "GHOST" }; + case ImplantType::ADRENALINE_DRUM: + return { type, "ADRENALINE DRUM", "ROULETTE", ImplantArchetype::CYLINDER, + "Killing a drone instantly loads +1 random ammo & grants a free turn.", + Color{ 255, 230, 0, 255 }, "[ADREN]", "ADREN" }; + case ImplantType::HEAVY_HAMMER: + return { type, "HEAVY HAMMER", "ROULETTE", ImplantArchetype::CYLINDER, + "Single shots deal +30% damage. Triple burst cooldown reduced to 1 turn.", + Color{ 255, 140, 30, 255 }, "[HAMMR]", "HAMMR" }; + case ImplantType::CATALYST_MATRIX: + return { type, "CATALYST MATRIX", "ELEMENTAL", ImplantArchetype::ELEMENTAL, + "Attacking Burned or Corroded foes detonates for +25 bonus direct damage.", + Color{ 255, 40, 130, 255 }, "[CATLY]", "CATLY" }; + case ImplantType::CONDUCTIVE_SALVO: + return { type, "CONDUCTIVE SALVO", "ELEMENTAL", ImplantArchetype::ELEMENTAL, + "EMP shots deal damage through enemy shields; Stun duration +1 turn.", + Color{ 80, 220, 255, 255 }, "[CONDC]", "CONDC" }; + case ImplantType::CORROSIVE_LEECH: + return { type, "CORROSIVE LEECH", "ELEMENTAL", ImplantArchetype::ELEMENTAL, + "Nanite hits on Corroded targets restore 100% more HP & Shield.", + Color{ 57, 255, 20, 255 }, "[LEECH]", "LEECH" }; + case ImplantType::DEFLECTOR_FEEDBACK: + return { type, "DEFLECTOR FEEDBACK", "DEFENSE", ImplantArchetype::DEFENSE, + "When Shield breaks, a shock pulse deals 20 retaliatory damage to drone.", + Color{ 0, 210, 255, 255 }, "[DEFLC]", "DEFLC" }; + case ImplantType::AUTO_DEFIBRILLATOR: + return { type, "AUTO-DEFIBRILLATOR", "DEFENSE", ImplantArchetype::DEFENSE, + "Once per run, lethal damage is prevented: grants 1 HP, 50 Shield, and stuns.", + Color{ 100, 255, 150, 255 }, "[DEFIB]", "DEFIB" }; + case ImplantType::NANITE_WEAVER: + return { type, "NANITE WEAVER", "DEFENSE", ImplantArchetype::DEFENSE, + "Start of turn in combat: restore +5 HP and +8 Shield if enemy has debuffs.", + Color{ 0, 255, 180, 255 }, "[WEAVR]", "WEAVR" }; + case ImplantType::LOGIC_OVERCLOCK: + return { type, "LOGIC OVERCLOCK", "HACKER", ImplantArchetype::HACKER, + "Skill check Great zone is 2x wider; Critical hacking awards 2x Core Chips.", + Color{ 200, 80, 255, 255 }, "[LOGIC]", "LOGIC" }; + case ImplantType::SALVAGE_PROTOCOL: + return { type, "SALVAGE PROTOCOL", "HACKER", ImplantArchetype::HACKER, + "+50% Core Chips dropped from all sources; 20% ammo chance on door bypass.", + Color{ 255, 210, 40, 255 }, "[SALVG]", "SALVG" }; + case ImplantType::PRECOGNITION_OPTICS: + return { type, "PRECOGNITION OPTICS", "HACKER", ImplantArchetype::HACKER, + "Scans 5 tiles ahead, revealing Elites, Mines, Lasers & Caches on minimap.", + Color{ 120, 240, 255, 255 }, "[OPTIC]", "OPTIC" }; + default: + return { type, "UNKNOWN IMPLANT", "GENERAL", ImplantArchetype::DEFENSE, "Undocumented firmware.", WHITE, "[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; +}; + + // Cyberpunk color palette namespace CyberColors { inline const Color NeonCyan = { 0, 240, 255, 255 }; diff --git a/src/core/Game.cpp b/src/core/Game.cpp index 3e2b313..4b86be6 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -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(i), data.ammoTiers[i]); } + // Restore Neural Implants & Defibrillator state + std::vector imps; + for (int val : data.implants) { + imps.push_back(static_cast(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; @@ -381,10 +400,183 @@ void Game::tryInteractRestRoom() { } } +void Game::triggerImplantDraft() { + std::vector available; + for (int i = 0; i < 12; ++i) { + ImplantType t = static_cast(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(slX), static_cast(slY), static_cast(slotW), static_cast(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(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(dirVec.x); int frontY = m_player.getGridY() + static_cast(dirVec.y); @@ -392,12 +584,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 +653,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(me->enemy.getType(), m_dungeon.getFloor())); + auto enemyPtr = std::make_unique(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 +673,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(me->enemy.getType(), m_dungeon.getFloor())); + auto enemyPtr = std::make_unique(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 +724,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 +734,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); @@ -532,6 +800,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 +839,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 +930,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 +1101,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; diff --git a/src/core/Game.hpp b/src/core/Game.hpp index 1d7e025..62857d3 100644 --- a/src/core/Game.hpp +++ b/src/core/Game.hpp @@ -77,9 +77,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 m_draftChoices; + int m_draftReplaceSlot = -1; + + bool m_showShrineUI = false; + CyberShrine* m_activeShrine = nullptr; + + bool m_isDefusingMine = false; + ProximityMine* m_activeMine = nullptr; }; } // namespace dnr diff --git a/src/core/SaveSystem.cpp b/src/core/SaveSystem.cpp index aee03f4..e4963aa 100644 --- a/src/core/SaveSystem.cpp +++ b/src/core/SaveSystem.cpp @@ -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(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); } } diff --git a/src/core/SaveSystem.hpp b/src/core/SaveSystem.hpp index bd18a79..4c9035f 100644 --- a/src/core/SaveSystem.hpp +++ b/src/core/SaveSystem.hpp @@ -20,6 +20,8 @@ struct SaveData { std::array chambers{}; std::array ammoCounts{}; std::array ammoTiers{ 1, 1, 1, 1, 1, 1, 1 }; + std::vector implants{}; + bool defibrillatorUsed = false; }; class SaveSystem { diff --git a/src/dungeon/Dungeon.cpp b/src/dungeon/Dungeon.cpp index 316ef1c..f25e6a8 100644 --- a/src/dungeon/Dungeon.cpp +++ b/src/dungeon/Dungeon.cpp @@ -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& 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) { diff --git a/src/dungeon/Dungeon.hpp b/src/dungeon/Dungeon.hpp index 9087af5..4b3a4aa 100644 --- a/src/dungeon/Dungeon.hpp +++ b/src/dungeon/Dungeon.hpp @@ -54,7 +54,15 @@ public: void addDoor(int x, int y, int secLevel); void addLoot(int x, int y, const std::vector& 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& getDoors() const { return m_doors; } const std::vector& getLoots() const { return m_loots; } std::vector& getEnemies() { return m_enemies; } + const std::vector& getEnemies() const { return m_enemies; } + std::vector& getLasers() { return m_lasers; } + const std::vector& getLasers() const { return m_lasers; } + std::vector& getMines() { return m_mines; } + const std::vector& getMines() const { return m_mines; } + std::vector& getShrines() { return m_shrines; } + const std::vector& getShrines() const { return m_shrines; } private: int m_width; @@ -76,6 +91,9 @@ private: std::vector m_doors; std::vector m_loots; std::vector m_enemies; + std::vector m_lasers; + std::vector m_mines; + std::vector m_shrines; }; } // namespace dnr diff --git a/src/dungeon/DungeonGen.cpp b/src/dungeon/DungeonGen.cpp index b359f29..f1ce284 100644 --- a/src/dungeon/DungeonGen.cpp +++ b/src/dungeon/DungeonGen.cpp @@ -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(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> vertCorridors; + std::vector> 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> 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++; } } diff --git a/src/dungeon/Player.cpp b/src/dungeon/Player.cpp index c858615..57156d3 100644 --- a/src/dungeon/Player.cpp +++ b/src/dungeon/Player.cpp @@ -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& implants) { + m_implants = implants; + if (m_implants.size() > MAX_IMPLANTS) { + m_implants.resize(MAX_IMPLANTS); + } +} + } // namespace dnr diff --git a/src/dungeon/Player.hpp b/src/dungeon/Player.hpp index 279d29c..0a3f315 100644 --- a/src/dungeon/Player.hpp +++ b/src/dungeon/Player.hpp @@ -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& 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& getStatuses() const { return m_statuses; } + // Neural Implants (Max 4 slots) + static constexpr size_t MAX_IMPLANTS = 4; + const std::vector& 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& 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 m_ammoTiers{ 1, 1, 1, 1, 1, 1, 1 }; Revolver m_revolver; std::vector m_statuses; + std::vector m_implants; + bool m_defibrillatorUsed = false; }; } // namespace dnr diff --git a/src/hacking/DbdSkillCheck.cpp b/src/hacking/DbdSkillCheck.cpp index 3840791..e3289c3 100644 --- a/src/hacking/DbdSkillCheck.cpp +++ b/src/hacking/DbdSkillCheck.cpp @@ -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(std::rand() % 40)) * speedMultiplier; @@ -17,8 +17,8 @@ void DbdSkillCheck::init(float speedMultiplier) { m_successStart = 120.0f + static_cast(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; diff --git a/src/hacking/DbdSkillCheck.hpp b/src/hacking/DbdSkillCheck.hpp index d78e461..c368e93 100644 --- a/src/hacking/DbdSkillCheck.hpp +++ b/src/hacking/DbdSkillCheck.hpp @@ -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 diff --git a/src/hacking/HackingGame.cpp b/src/hacking/HackingGame.cpp index 575ade1..2b1cf20 100644 --- a/src/hacking/HackingGame.cpp +++ b/src/hacking/HackingGame.cpp @@ -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) { diff --git a/src/hacking/HackingGame.hpp b/src/hacking/HackingGame.hpp index ba90e76..069e126 100644 --- a/src/hacking/HackingGame.hpp +++ b/src/hacking/HackingGame.hpp @@ -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(); diff --git a/src/inventory/InventoryUI.cpp b/src/inventory/InventoryUI.cpp index 3492f55..eeda50b 100644 --- a/src/inventory/InventoryUI.cpp +++ b/src/inventory/InventoryUI.cpp @@ -66,11 +66,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(pouchX + 15), static_cast(startY + i * 50), static_cast(pouchW - 30), 44.0f }; + Rectangle rowRect = { static_cast(pouchX + 15), static_cast(startY + i * 42), static_cast(pouchW - 30), 38.0f }; if (mouseClicked && CheckCollisionPointRec(mouse, rowRect)) { m_selectedTypeIndex = i; SoundManager::get().playTumblerClick(); @@ -269,27 +269,27 @@ 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 }); - // 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); + DrawText("TACTICAL CARTRIDGE STACK // INVENTORY", pouchX + 20, pouchY + 12, 16, CyberColors::NeonCyan); 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, CyberColors::NeonYellow); - 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(pouchX + 15), static_cast(startY + i * 50), static_cast(pouchW - 30), 44.0f }; + Rectangle rowRect = { static_cast(pouchX + 15), static_cast(startY + i * 42), static_cast(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 }); @@ -299,18 +299,23 @@ void InventoryUI::draw(const Player& player) { DrawRectangleLinesEx(rowRect, isSel ? 2.0f : 1.0f, borderCol); // Icon - DrawCircle(pouchX + 35, startY + i * 50 + 22, 11.0f, count > 0 ? itemInfo.color : DARKGRAY); + DrawCircle(pouchX + 32, startY + i * 42 + 19, 10.0f, count > 0 ? itemInfo.color : DARKGRAY); // 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 ? CyberColors::NeonYellow : RAYWHITE) : GRAY); + + // Tier badge + int tier = player.getAmmoTier(aType); + std::string tStr = tier == 1 ? "T1" : (tier == 2 ? "T2" : "T3"); + DrawText(tStr.c_str(), pouchX + 230, startY + i * 42 + 12, 12, tier >= 3 ? CyberColors::NeonYellow : (tier == 2 ? CyberColors::NeonGreen : GRAY)); // 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, CyberColors::NeonCyan); // 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); + DrawText(countStr.c_str(), pouchX + 430, startY + i * 42 + 11, 15, countColor); } // Selected item detail footer @@ -318,13 +323,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, Color{ 14, 24, 40, 255 }); + DrawRectangleLines(pouchX + 15, detailY, pouchW - 30, detailH, CyberColors::NeonGreen); - 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 ? CyberColors::NeonGreen : RED); + DrawText(curSelInfo.description.c_str(), pouchX + 26, detailY + 28, 11, RAYWHITE); + DrawText("[ENTER / E / SPACE] INSERT INTO SELECTED CHAMBER", pouchX + 26, detailY + 46, 11, CyberColors::NeonYellow); + + // Neural Implants Matrix Panel (2x2 grid) + int impPanelY = detailY + detailH + 8; + int impPanelH = pouchY + pouchH - impPanelY - 10; + DrawRectangle(pouchX + 15, impPanelY, pouchW - 30, impPanelH, Color{ 12, 18, 30, 255 }); + DrawRectangleLines(pouchX + 15, impPanelY, pouchW - 30, impPanelH, CyberColors::NeonCyan); + + 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); + + std::string chipTxt = "[ ❖ " + std::to_string(player.getTechScraps()) + " ]"; + DrawText(chipTxt.c_str(), pouchX + pouchW - 95, impPanelY + 8, 12, CyberColors::NeonYellow); + + 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(sX), static_cast(sY), static_cast(slotW), static_cast(slotH) }; + bool isHoverImp = CheckCollisionPointRec(mouse, sRect); + + if (s < static_cast(implants.size())) { + ImplantType imp = implants[s]; + const auto& data = getImplantData(imp); + Color archCol = data.color; + + if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + archCol = RED; + } + + DrawRectangleRec(sRect, Color{ 16, 24, 38, 255 }); + DrawRectangleLinesEx(sRect, isHoverImp ? 2.0f : 1.0f, isHoverImp ? WHITE : archCol); + + std::string sTitle = "SLOT " + std::to_string(s + 1) + ": " + data.name; + DrawText(sTitle.c_str(), sX + 8, sY + 6, 11, archCol); + + if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + DrawText("[STATUS: REVIVE DEPLETED]", sX + 8, sY + 24, 10, RED); + } else { + std::string shortDesc = data.description; + if (shortDesc.length() > 32) shortDesc = shortDesc.substr(0, 30) + ".."; + DrawText(shortDesc.c_str(), sX + 8, sY + 24, 10, RAYWHITE); + } + } else { + DrawRectangleRec(sRect, Color{ 8, 12, 20, 200 }); + DrawRectangleLinesEx(sRect, 1.0f, Color{ 30, 45, 65, 200 }); + std::string emptyTitle = "SLOT " + std::to_string(s + 1) + ": [EMPTY SOCKET]"; + DrawText(emptyTitle.c_str(), sX + 8, sY + 8, 11, DARKGRAY); + DrawText("Draft from Elites / ICE", sX + 8, sY + 26, 10, Color{ 60, 80, 110, 255 }); + } + } } } // namespace dnr diff --git a/src/render/CyberRenderer.cpp b/src/render/CyberRenderer.cpp index cea5717..5d46474 100644 --- a/src/render/CyberRenderer.cpp +++ b/src/render/CyberRenderer.cpp @@ -1271,7 +1271,7 @@ void CyberRenderer::triggerGlitch(float intensity) { m_glitchIntensity = std::max(m_glitchIntensity, intensity); } -void CyberRenderer::renderCyberDrone(Vector3 pos, Color coreColor, float scale, float animTime, bool isHit, bool isAttacking) { +void CyberRenderer::renderCyberDrone(Vector3 pos, Color coreColor, float scale, float animTime, bool isHit, bool isAttacking, bool isElite) { float bob = std::sin(animTime * 3.5f) * 0.12f; Vector3 basePos = { pos.x, pos.y + bob, pos.z }; @@ -1282,6 +1282,19 @@ void CyberRenderer::renderCyberDrone(Vector3 pos, Color coreColor, float scale, basePos.y += shakeY; } + // Elite Aura and Orbiting Corona + if (isElite) { + float haloY = basePos.y + 0.48f * scale + 0.04f * std::sin(animTime * 4.0f); + DrawCircle3D({ basePos.x, haloY, basePos.z }, 0.55f * scale, { 1.0f, 0.0f, 0.0f }, 90.0f, CyberColors::NeonPurple); + DrawCircle3D({ basePos.x, haloY, basePos.z }, 0.40f * scale, { 1.0f, 0.0f, 0.0f }, 90.0f, CyberColors::NeonYellow); + for (int k = 0; k < 4; ++k) { + float spAngle = animTime * 2.5f + k * (3.14159265f * 0.5f); + Vector3 spPos = { basePos.x + std::cos(spAngle) * 0.48f * scale, haloY, basePos.z + std::sin(spAngle) * 0.48f * scale }; + DrawCube(spPos, 0.08f * scale, 0.18f * scale, 0.08f * scale, CyberColors::NeonYellow); + DrawCubeWires(spPos, 0.09f * scale, 0.19f * scale, 0.09f * scale, CyberColors::NeonPurple); + } + } + // 1. Armored Faceted Chassis DrawSphere(basePos, 0.42f * scale, Color{ 22, 28, 42, 255 }); DrawSphereWires(basePos, 0.44f * scale, 8, 8, Color{ 45, 60, 90, 255 }); @@ -1498,6 +1511,68 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float } } + // Render Laser Barriers + for (const auto& l : dungeon.getLasers()) { + if (l.disarmed) continue; + if (std::abs(l.x - pX) > renderDist || std::abs(l.y - pY) > renderDist) continue; + Vector3 lCenter = { l.x * tileSize, 0.9f, l.y * tileSize }; + + if (l.vertical) { + DrawCube({ lCenter.x, lCenter.y, lCenter.z - 0.95f }, 0.25f, 0.25f, 0.10f, Color{ 60, 65, 80, 255 }); + DrawCube({ lCenter.x, lCenter.y, lCenter.z + 0.95f }, 0.25f, 0.25f, 0.10f, Color{ 60, 65, 80, 255 }); + if (l.active) { + float pulse = 0.5f + 0.5f * std::sin(animTime * 12.0f); + DrawCylinderEx({ lCenter.x, lCenter.y, lCenter.z - 0.95f }, { lCenter.x, lCenter.y, lCenter.z + 0.95f }, 0.04f, 0.04f, 8, RED); + DrawCylinderEx({ lCenter.x, lCenter.y, lCenter.z - 0.95f }, { lCenter.x, lCenter.y, lCenter.z + 0.95f }, 0.12f, 0.12f, 8, Color{ 255, 0, 50, static_cast(70 + pulse * 60) }); + } else { + DrawCylinderEx({ lCenter.x, lCenter.y, lCenter.z - 0.95f }, { lCenter.x, lCenter.y, lCenter.z + 0.95f }, 0.02f, 0.02f, 6, Color{ 0, 255, 120, 90 }); + } + } else { + DrawCube({ lCenter.x - 0.95f, lCenter.y, lCenter.z }, 0.10f, 0.25f, 0.25f, Color{ 60, 65, 80, 255 }); + DrawCube({ lCenter.x + 0.95f, lCenter.y, lCenter.z }, 0.10f, 0.25f, 0.25f, Color{ 60, 65, 80, 255 }); + if (l.active) { + float pulse = 0.5f + 0.5f * std::sin(animTime * 12.0f); + DrawCylinderEx({ lCenter.x - 0.95f, lCenter.y, lCenter.z }, { lCenter.x + 0.95f, lCenter.y, lCenter.z }, 0.04f, 0.04f, 8, RED); + DrawCylinderEx({ lCenter.x - 0.95f, lCenter.y, lCenter.z }, { lCenter.x + 0.95f, lCenter.y, lCenter.z }, 0.12f, 0.12f, 8, Color{ 255, 0, 50, static_cast(70 + pulse * 60) }); + } else { + DrawCylinderEx({ lCenter.x - 0.95f, lCenter.y, lCenter.z }, { lCenter.x + 0.95f, lCenter.y, lCenter.z }, 0.02f, 0.02f, 6, Color{ 0, 255, 120, 90 }); + } + } + } + + // Render Proximity Mines + for (const auto& m : dungeon.getMines()) { + if (m.defused || m.triggered) continue; + if (std::abs(m.x - pX) > renderDist || std::abs(m.y - pY) > renderDist) continue; + Vector3 mCenter = { m.x * tileSize, 0.04f, m.y * tileSize }; + + DrawCylinder(mCenter, 0.38f, 0.42f, 0.08f, 16, Color{ 32, 38, 50, 255 }); + DrawCylinderWires(mCenter, 0.39f, 0.43f, 0.08f, 16, Color{ 80, 90, 110, 255 }); + + float blink = 0.5f + 0.5f * std::sin(animTime * 10.0f); + Color ledCol = (blink > 0.4f) ? RED : Color{ 120, 20, 30, 255 }; + DrawSphere({ mCenter.x, 0.12f, mCenter.z }, 0.10f, ledCol); + DrawCircle3D({ mCenter.x, 0.09f, mCenter.z }, 0.28f, { 1.0f, 0.0f, 0.0f }, 90.0f, Color{ 255, 120, 0, static_cast(100 + blink * 100) }); + } + + // Render Cyber Shrines + for (const auto& s : dungeon.getShrines()) { + if (std::abs(s.x - pX) > renderDist || std::abs(s.y - pY) > renderDist) continue; + Vector3 sCenter = { s.x * tileSize, 0.0f, s.y * tileSize }; + + DrawCylinder({ sCenter.x, 0.10f, sCenter.z }, 0.9f, 1.0f, 0.20f, 8, Color{ 20, 14, 30, 255 }); + DrawCylinderWires({ sCenter.x, 0.10f, sCenter.z }, 0.92f, 1.02f, 0.20f, 8, CyberColors::NeonPurple); + + DrawCube({ sCenter.x, 0.95f, sCenter.z }, 0.55f, 1.50f, 0.55f, Color{ 14, 10, 22, 255 }); + DrawCubeWires({ sCenter.x, 0.95f, sCenter.z }, 0.57f, 1.52f, 0.57f, s.used ? GRAY : CyberColors::NeonPurple); + + float pulse = 0.5f + 0.5f * std::sin(animTime * 3.5f); + Vector3 runePos = { sCenter.x, 1.95f + pulse * 0.10f, sCenter.z }; + Color runeCol = s.used ? GRAY : Color{ 255, 40, 160, static_cast(180 + pulse * 75) }; + DrawSphere(runePos, 0.18f + pulse * 0.04f, runeCol); + DrawCircle3D(runePos, 0.40f, { 1.0f, 0.0f, 0.0f }, 90.0f, runeCol); + } + // Render Corridor Patrol Enemies as 3D Cyber-Drones auto& enemies = const_cast(dungeon).getEnemies(); for (const auto& me : enemies) { @@ -1505,7 +1580,7 @@ void CyberRenderer::render3D(const Dungeon& dungeon, const Player& player, float if (std::abs(me.x - pX) > renderDist || std::abs(me.y - pY) > renderDist) continue; Vector3 ePos = { me.x * tileSize, 0.9f, me.y * tileSize }; - renderCyberDrone(ePos, me.enemy.getColor(), 1.0f, animTime); + renderCyberDrone(ePos, me.enemy.getColor(), 1.0f, animTime, false, false, me.enemy.isElite()); } EndMode3D(); @@ -1586,7 +1661,7 @@ void CyberRenderer::renderCombat3D(const Dungeon& dungeon, const Player& player, bool isHit = (combat.getPhase() == CombatPhase::PLAYER_ACTION_ANIM); bool isAttacking = (combat.getPhase() == CombatPhase::ENEMY_ACTION_ANIM); - renderCyberDrone(combatDronePos, enemy->getColor(), 1.5f, animTime, isHit, isAttacking); + renderCyberDrone(combatDronePos, enemy->getColor(), 1.5f, animTime, isHit, isAttacking, enemy->isElite()); } EndMode3D(); diff --git a/src/render/CyberRenderer.hpp b/src/render/CyberRenderer.hpp index 398f6bd..c06bb06 100644 --- a/src/render/CyberRenderer.hpp +++ b/src/render/CyberRenderer.hpp @@ -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; diff --git a/src/render/UIRenderer.cpp b/src/render/UIRenderer.cpp index eb7eb9d..02a8955 100644 --- a/src/render/UIRenderer.cpp +++ b/src/render/UIRenderer.cpp @@ -23,9 +23,13 @@ void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int s DrawRectangleLines(startX - 4, startY - 4, size + 8, size + 8, CyberColors::NeonCyan); DrawText("NET_MAP // 2.5D", startX, startY - 18, 11, CyberColors::NeonCyan); + bool hasPrecog = player.hasImplant(ImplantType::PRECOGNITION_OPTICS); + for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { - if (!dungeon.isExplored(x, y)) continue; + int dist = std::abs(x - player.getGridX()) + std::abs(y - player.getGridY()); + bool visible = dungeon.isExplored(x, y) || (hasPrecog && dist <= 5); + if (!visible) continue; int px = startX + static_cast(x * cellSize); int py = startY + static_cast(y * cellSize); @@ -57,6 +61,56 @@ void UIRenderer::drawMinimap(const Dungeon& dungeon, const Player& player, int s } } + // Lasers on minimap + for (const auto& laser : dungeon.getLasers()) { + int dist = std::abs(laser.x - player.getGridX()) + std::abs(laser.y - player.getGridY()); + if (dungeon.isExplored(laser.x, laser.y) || (hasPrecog && dist <= 5)) { + int lx = startX + static_cast(laser.x * cellSize); + int ly = startY + static_cast(laser.y * cellSize); + int cs = static_cast(cellSize) + 1; + Color lCol = laser.disarmed ? DARKGRAY : (laser.active ? RED : CyberColors::NeonCyan); + DrawRectangle(lx + 1, ly + 1, std::max(2, cs - 2), std::max(2, cs - 2), lCol); + } + } + + // Mines on minimap + for (const auto& mine : dungeon.getMines()) { + int dist = std::abs(mine.x - player.getGridX()) + std::abs(mine.y - player.getGridY()); + if (dungeon.isExplored(mine.x, mine.y) || (hasPrecog && dist <= 5)) { + int mx = startX + static_cast((mine.x + 0.5f) * cellSize); + int my = startY + static_cast((mine.y + 0.5f) * cellSize); + Color mCol = mine.defused ? DARKGRAY : (mine.isArmed() ? CyberColors::NeonOrange : RED); + DrawCircle(mx, my, std::max(2.0f, cellSize * 0.35f), mCol); + } + } + + // Cyber Shrines on minimap + for (const auto& shrine : dungeon.getShrines()) { + int dist = std::abs(shrine.x - player.getGridX()) + std::abs(shrine.y - player.getGridY()); + if (dungeon.isExplored(shrine.x, shrine.y) || (hasPrecog && dist <= 5)) { + int sx = startX + static_cast((shrine.x + 0.5f) * cellSize); + int sy = startY + static_cast((shrine.y + 0.5f) * cellSize); + Color sCol = shrine.used ? DARKGRAY : CyberColors::NeonPink; + DrawCircle(sx, sy, std::max(2.5f, cellSize * 0.45f), sCol); + } + } + + // Enemies on minimap + for (const auto& me : dungeon.getEnemies()) { + if (!me.alive) continue; + int dist = std::abs(me.x - player.getGridX()) + std::abs(me.y - player.getGridY()); + if (dungeon.isExplored(me.x, me.y) || (hasPrecog && dist <= 5)) { + int ex = startX + static_cast((me.x + 0.5f) * cellSize); + int ey = startY + static_cast((me.y + 0.5f) * cellSize); + if (me.enemy.isElite()) { + DrawCircle(ex, ey, std::max(3.0f, cellSize * 0.5f), CyberColors::NeonYellow); + DrawCircleLines(ex, ey, std::max(4.0f, cellSize * 0.65f), CyberColors::NeonPink); + } else { + DrawCircle(ex, ey, std::max(2.0f, cellSize * 0.35f), RED); + } + } + } + // Player icon and direction on minimap int plX = startX + static_cast((player.getGridX() + 0.5f) * cellSize); int plY = startY + static_cast((player.getGridY() + 0.5f) * cellSize); @@ -129,6 +183,63 @@ void UIRenderer::drawExplorationHUD(const Dungeon& dungeon, const Player& player DrawText(statStr.c_str(), hudX + 160, hudY + 102, 14, RED); } + // Neural Implants HUD Bar + int impBarX = hudX; + int impBarY = hudY + 152; + int impBarW = 330; + int impBarH = 34; + DrawRectangle(impBarX, impBarY, impBarW, impBarH, Color{ 6, 12, 22, 220 }); + DrawRectangleLines(impBarX, impBarY, impBarW, impBarH, CyberColors::NeonCyan); + + const auto& implants = player.getImplants(); + Vector2 mousePos = GetMousePosition(); + int hoveredImpIdx = -1; + + for (int s = 0; s < 4; ++s) { + int bX = impBarX + 7 + s * 80; + int bY = impBarY + 5; + Rectangle bRect = { static_cast(bX), static_cast(bY), 74.0f, 24.0f }; + bool isHover = CheckCollisionPointRec(mousePos, bRect); + + if (s < static_cast(implants.size())) { + ImplantType imp = implants[s]; + const auto& data = getImplantData(imp); + Color archCol = data.color; + + if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + archCol = RED; + } + + DrawRectangleRec(bRect, Color{ 12, 20, 32, 240 }); + DrawRectangleLinesEx(bRect, isHover ? 2.0f : 1.0f, isHover ? WHITE : archCol); + + std::string code = data.code; + if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + code = "USED"; + } + int textW = MeasureText(code.c_str(), 11); + DrawText(code.c_str(), bX + (74 - textW) / 2, bY + 6, 11, archCol); + + if (isHover) hoveredImpIdx = s; + } else { + DrawRectangleRec(bRect, Color{ 8, 14, 22, 180 }); + DrawRectangleLinesEx(bRect, 1.0f, Color{ 40, 60, 85, 180 }); + DrawText("[--]", bX + 27, bY + 6, 11, DARKGRAY); + } + } + + if (hoveredImpIdx >= 0 && hoveredImpIdx < static_cast(implants.size())) { + const auto& tipData = getImplantData(implants[hoveredImpIdx]); + int tipX = impBarX; + int tipY = impBarY + impBarH + 6; + int tipW = 330; + int tipH = 50; + DrawRectangle(tipX, tipY, tipW, tipH, Color{ 10, 16, 26, 250 }); + DrawRectangleLines(tipX, tipY, tipW, tipH, CyberColors::NeonYellow); + DrawText(tipData.name.c_str(), tipX + 10, tipY + 8, 12, CyberColors::NeonYellow); + DrawText(tipData.description.c_str(), tipX + 10, tipY + 26, 10, RAYWHITE); + } + // Top-Right: Holographic Minimap drawMinimap(dungeon, player, GetScreenWidth() - 200, 20, 180); @@ -692,6 +803,59 @@ void UIRenderer::drawCombatUI(const CombatSystem& combat, const Player& player, DrawRectangleLinesEx(btnCodex, hoverCodex ? 2.0f : 1.0f, hoverCodex ? CyberColors::NeonYellow : CyberColors::NeonCyan); DrawText("[H] CODEX", logX + logW - 87, pvY + 38, 12, hoverCodex ? CyberColors::NeonYellow : RAYWHITE); + // Neural Rig Active Perks Bar in Combat + int rigY = pvY + pvH + 6; + int rigH = 34; + DrawRectangle(logX, rigY, logW, rigH, Color{ 8, 14, 26, 245 }); + DrawRectangleLines(logX, rigY, logW, rigH, CyberColors::NeonCyan); + + const auto& cImplants = player.getImplants(); + int hoveredCombatImp = -1; + for (int s = 0; s < 4; ++s) { + int bX = logX + 8 + s * 112; + int bY = rigY + 5; + Rectangle bRect = { static_cast(bX), static_cast(bY), 104.0f, 24.0f }; + bool isHover = CheckCollisionPointRec(mousePos, bRect); + + if (s < static_cast(cImplants.size())) { + ImplantType imp = cImplants[s]; + const auto& data = getImplantData(imp); + Color archCol = data.color; + + if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + archCol = RED; + } + + DrawRectangleRec(bRect, Color{ 12, 20, 32, 240 }); + DrawRectangleLinesEx(bRect, isHover ? 2.0f : 1.0f, isHover ? WHITE : archCol); + + std::string code = data.code; + if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + code += " [USED]"; + } + int tw = MeasureText(code.c_str(), 11); + DrawText(code.c_str(), bX + (104 - tw) / 2, bY + 6, 11, archCol); + + if (isHover) hoveredCombatImp = s; + } else { + DrawRectangleRec(bRect, Color{ 8, 14, 22, 180 }); + DrawRectangleLinesEx(bRect, 1.0f, Color{ 40, 60, 85, 180 }); + DrawText("[--]", bX + 42, bY + 6, 11, DARKGRAY); + } + } + + if (hoveredCombatImp >= 0 && hoveredCombatImp < static_cast(cImplants.size())) { + const auto& tipData = getImplantData(cImplants[hoveredCombatImp]); + int tipX = logX; + int tipY = rigY - 54; + int tipW = logW; + int tipH = 50; + DrawRectangle(tipX, tipY, tipW, tipH, Color{ 10, 16, 26, 250 }); + DrawRectangleLines(tipX, tipY, tipW, tipH, CyberColors::NeonYellow); + DrawText(tipData.name.c_str(), tipX + 10, tipY + 8, 12, CyberColors::NeonYellow); + DrawText(tipData.description.c_str(), tipX + 10, tipY + 26, 10, RAYWHITE); + } + // 4. Center: Tactical AI Advisor Hologram Ticker int advX = 380; int advY = 485; @@ -1065,6 +1229,327 @@ void UIRenderer::drawWorkbenchModal(const Player& player) { DrawText("[ESC] CLOSE", static_cast(btnClose.x + 18), static_cast(btnClose.y + 9), 13, isHoverClose ? RED : RAYWHITE); } +Rectangle UIRenderer::getDraftCardButtonRect(int cardIndex, int screenW, int screenH) { + int modalW = 960; + int modalH = 550; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + int cardW = 280; + int cardH = 240; + int gapX = 25; + int startX = modalX + (modalW - (3 * cardW + 2 * gapX)) / 2; + int cardX = startX + cardIndex * (cardW + gapX); + int cardY = modalY + 80; + return Rectangle{ static_cast(cardX + 15), static_cast(cardY + cardH - 42), static_cast(cardW - 30), 32.0f }; +} + +Rectangle UIRenderer::getDraftScrapButtonRect(int screenW, int screenH) { + int modalW = 960; + int modalH = 550; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + return Rectangle{ static_cast(modalX + (modalW - 400) / 2), static_cast(modalY + modalH - 46), 400.0f, 32.0f }; +} + +void UIRenderer::drawImplantDraftModal(const std::vector& choices, const Player& player, int selectedReplaceSlot) { + int screenW = GetScreenWidth(); + int screenH = GetScreenHeight(); + + DrawRectangle(0, 0, screenW, screenH, Color{ 4, 8, 16, 235 }); + + int modalW = 960; + int modalH = 550; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + + DrawRectangle(modalX, modalY, modalW, modalH, Color{ 10, 16, 28, 255 }); + DrawRectangleLines(modalX, modalY, modalW, modalH, CyberColors::NeonCyan); + + // Decorative corners + DrawRectangle(modalX - 2, modalY - 2, 8, 8, CyberColors::NeonCyan); + DrawRectangle(modalX + modalW - 6, modalY - 2, 8, 8, CyberColors::NeonCyan); + DrawRectangle(modalX - 2, modalY + modalH - 6, 8, 8, CyberColors::NeonCyan); + DrawRectangle(modalX + modalW - 6, modalY + modalH - 6, 8, 8, CyberColors::NeonCyan); + + // Header Banner + DrawRectangle(modalX, modalY, modalW, 48, Color{ 16, 26, 44, 255 }); + DrawRectangleLines(modalX, modalY, modalW, 48, CyberColors::NeonCyan); + DrawText("// NEURAL MATRIX SYNAPSE // CYBERNETIC IMPLANT DRAFT", modalX + 25, modalY + 15, 17, CyberColors::NeonCyan); + + std::string scrapBadge = "[ CORE CHIPS: " + std::to_string(player.getTechScraps()) + " ]"; + int badgeW = MeasureText(scrapBadge.c_str(), 15); + DrawText(scrapBadge.c_str(), modalX + modalW - badgeW - 25, modalY + 16, 15, CyberColors::NeonYellow); + + // Subtitle / instructions + std::string subtitle; + Color subCol = RAYWHITE; + if (player.getImplants().size() < 4) { + subtitle = "SELECT 1 OF 3 NEURAL IMPLANTS TO INSTALL (MOUNTED: " + std::to_string(player.getImplants().size()) + "/4) OR SCRAP FOR TECH CHIPS"; + } else { + if (selectedReplaceSlot == -1) { + subtitle = "IMPLANT CAPACITY FULL (4/4)! CLICK A MOUNTED IMPLANT BELOW TO MARK FOR REPLACEMENT, OR SCRAP DRAFT"; + subCol = CyberColors::NeonYellow; + } else { + ImplantType curEq = player.getImplants()[selectedReplaceSlot]; + subtitle = "REPLACING SLOT #" + std::to_string(selectedReplaceSlot + 1) + " (" + getImplantData(curEq).name + "). SELECT NEW IMPLANT ABOVE:"; + subCol = CyberColors::NeonPink; + } + } + DrawText(subtitle.c_str(), modalX + 25, modalY + 56, 12, subCol); + + Vector2 mouse = GetMousePosition(); + int cardW = 280; + int cardH = 240; + int gapX = 25; + int startX = modalX + (modalW - (3 * cardW + 2 * gapX)) / 2; + int cardY = modalY + 80; + + for (size_t i = 0; i < choices.size() && i < 3; ++i) { + ImplantType imp = choices[i]; + const auto& data = getImplantData(imp); + int cardX = startX + static_cast(i) * (cardW + gapX); + + Rectangle btnRect = getDraftCardButtonRect(static_cast(i), screenW, screenH); + bool isHoverCard = CheckCollisionPointRec(mouse, Rectangle{ static_cast(cardX), static_cast(cardY), static_cast(cardW), static_cast(cardH) }); + bool isHoverBtn = CheckCollisionPointRec(mouse, btnRect); + + Color archCol = data.color; + std::string archName = data.archetype + " PROTOCOL"; + + DrawRectangle(cardX, cardY, cardW, cardH, Color{ 14, 22, 36, 255 }); + DrawRectangleLines(cardX, cardY, cardW, cardH, (isHoverCard || isHoverBtn) ? CyberColors::NeonYellow : archCol); + + // Archetype tag + DrawRectangle(cardX + 12, cardY + 12, cardW - 24, 22, Color{ 20, 30, 48, 255 }); + DrawText(archName.c_str(), cardX + 18, cardY + 17, 11, archCol); + + // Implant Name + DrawText(data.name.c_str(), cardX + 18, cardY + 44, 15, WHITE); + + DrawLine(cardX + 12, cardY + 68, cardX + cardW - 12, cardY + 68, Color{ 35, 55, 80, 255 }); + + // Description text wrapped into lines + std::string desc = data.description; + size_t breakPos = desc.find(". "); + if (breakPos != std::string::npos && breakPos < 35) { + std::string line1 = desc.substr(0, breakPos + 1); + std::string line2 = desc.substr(breakPos + 2); + DrawText(line1.c_str(), cardX + 18, cardY + 78, 12, archCol); + DrawText(line2.c_str(), cardX + 18, cardY + 98, 11, RAYWHITE); + } else { + DrawText(desc.c_str(), cardX + 18, cardY + 78, 11, RAYWHITE); + } + + // Install / Replace button + Color btnBg = isHoverBtn ? Color{ 20, 55, 75, 255 } : Color{ 14, 35, 50, 255 }; + DrawRectangleRec(btnRect, btnBg); + DrawRectangleLinesEx(btnRect, isHoverBtn ? 2.0f : 1.0f, isHoverBtn ? CyberColors::NeonYellow : archCol); + + std::string btnLabel = "[" + std::to_string(i + 1) + "] INSTALL MATRIX"; + if (player.getImplants().size() >= 4) { + btnLabel = (selectedReplaceSlot != -1) ? ("[" + std::to_string(i + 1) + "] OVERWRITE SLOT " + std::to_string(selectedReplaceSlot + 1)) : ("[" + std::to_string(i + 1) + "] SELECT TO REPLACE"); + } + int lblW = MeasureText(btnLabel.c_str(), 12); + DrawText(btnLabel.c_str(), static_cast(btnRect.x + (btnRect.width - lblW) / 2), static_cast(btnRect.y + 9), 12, isHoverBtn ? CyberColors::NeonYellow : WHITE); + } + + // Currently Mounted Implants Bar + int mountY = modalY + 332; + DrawText("// MOUNTED NEURAL RIG (MAX 4):", modalX + 30, mountY, 12, CyberColors::NeonCyan); + + const auto& playerImps = player.getImplants(); + 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(slX), static_cast(slY), static_cast(slotW), static_cast(slotH) }; + bool isHoverSlot = CheckCollisionPointRec(mouse, slotRect); + bool isSelectedForReplace = (s == selectedReplaceSlot); + + if (s < static_cast(playerImps.size())) { + ImplantType eqImp = playerImps[s]; + const auto& eqData = getImplantData(eqImp); + Color eqArchCol = eqData.color; + + Color boxBg = isSelectedForReplace ? Color{ 45, 25, 30, 255 } : (isHoverSlot && playerImps.size() == 4 ? Color{ 25, 35, 50, 255 } : Color{ 12, 18, 30, 255 }); + Color borderCol = isSelectedForReplace ? RED : (isHoverSlot && playerImps.size() == 4 ? CyberColors::NeonYellow : eqArchCol); + + DrawRectangleRec(slotRect, boxBg); + DrawRectangleLinesEx(slotRect, (isSelectedForReplace || isHoverSlot) ? 2.0f : 1.0f, borderCol); + + std::string slotHdr = "SLOT " + std::to_string(s + 1); + if (isSelectedForReplace) slotHdr += " [TARGET REPLACEMENT]"; + DrawText(slotHdr.c_str(), slX + 10, slY + 8, 10, borderCol); + DrawText(eqData.name.c_str(), slX + 10, slY + 24, 12, WHITE); + + if (eqImp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) { + DrawText("[STATUS: CHARGE DEPLETED]", slX + 10, slY + 44, 10, RED); + } else { + DrawText(eqData.code.c_str(), slX + 10, slY + 44, 10, eqArchCol); + } + } else { + DrawRectangleRec(slotRect, Color{ 8, 14, 22, 200 }); + DrawRectangleLinesEx(slotRect, 1.0f, Color{ 35, 50, 75, 200 }); + std::string emptyTxt = "SLOT " + std::to_string(s + 1) + " [AVAILABLE]"; + DrawText(emptyTxt.c_str(), slX + 10, slY + 12, 11, GRAY); + DrawText("Empty Socket", slX + 10, slY + 34, 11, DARKGRAY); + } + } + + // Scrap Button + Rectangle btnScrap = getDraftScrapButtonRect(screenW, screenH); + bool isHoverScrap = CheckCollisionPointRec(mouse, btnScrap); + DrawRectangleRec(btnScrap, isHoverScrap ? Color{ 50, 35, 15, 255 } : Color{ 25, 18, 12, 255 }); + DrawRectangleLinesEx(btnScrap, isHoverScrap ? 2.0f : 1.0f, isHoverScrap ? CyberColors::NeonYellow : Color{ 180, 140, 50, 255 }); + DrawText("[4 / ESC] SCRAP MATRIX PROTOCOL (+35 CORE CHIPS)", static_cast(btnScrap.x + 24), static_cast(btnScrap.y + 9), 13, isHoverScrap ? CyberColors::NeonYellow : RAYWHITE); +} + +Rectangle UIRenderer::getShrineOptionButtonRect(int optionIndex, int screenW, int screenH) { + int modalW = 820; + int modalH = 460; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + int cardW = 230; + int cardH = 260; + int gapX = 25; + int startX = modalX + (modalW - (3 * cardW + 2 * gapX)) / 2; + int cardX = startX + optionIndex * (cardW + gapX); + int cardY = modalY + 85; + return Rectangle{ static_cast(cardX + 15), static_cast(cardY + cardH - 45), static_cast(cardW - 30), 34.0f }; +} + +Rectangle UIRenderer::getShrineCloseButtonRect(int screenW, int screenH) { + int modalW = 820; + int modalH = 460; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + return Rectangle{ static_cast(modalX + (modalW - 240) / 2), static_cast(modalY + modalH - 48), 240.0f, 34.0f }; +} + +void UIRenderer::drawShrineModal(const Player& player) { + int screenW = GetScreenWidth(); + int screenH = GetScreenHeight(); + + DrawRectangle(0, 0, screenW, screenH, Color{ 4, 8, 16, 235 }); + + int modalW = 820; + int modalH = 460; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + + DrawRectangle(modalX, modalY, modalW, modalH, Color{ 14, 10, 24, 255 }); + DrawRectangleLines(modalX, modalY, modalW, modalH, CyberColors::NeonPink); + + // Decorative corners + DrawRectangle(modalX - 2, modalY - 2, 8, 8, CyberColors::NeonPink); + DrawRectangle(modalX + modalW - 6, modalY - 2, 8, 8, CyberColors::NeonPink); + DrawRectangle(modalX - 2, modalY + modalH - 6, 8, 8, CyberColors::NeonPink); + DrawRectangle(modalX + modalW - 6, modalY + modalH - 6, 8, 8, CyberColors::NeonPink); + + // Header + DrawRectangle(modalX, modalY, modalW, 50, Color{ 26, 14, 38, 255 }); + DrawRectangleLines(modalX, modalY, modalW, 50, CyberColors::NeonPink); + DrawText("// DARKNET CYBER SHRINE // FORBIDDEN DATA CONTRACTS", modalX + 25, modalY + 16, 17, CyberColors::NeonPink); + DrawText("ANCIENT MAINFRAME DEMANDS RUN TRIBUTES FOR REWRITING SUBNET SYSTEM CODE", modalX + 25, modalY + 60, 12, GRAY); + + Vector2 mouse = GetMousePosition(); + int cardW = 230; + int cardH = 260; + int gapX = 25; + int startX = modalX + (modalW - (3 * cardW + 2 * gapX)) / 2; + int cardY = modalY + 85; + + // Option 0: BLOOD + { + int cardX = startX; + Rectangle btnRect = getShrineOptionButtonRect(0, screenW, screenH); + bool isHover = CheckCollisionPointRec(mouse, btnRect); + bool canAfford = player.getHp() > 25; + + DrawRectangle(cardX, cardY, cardW, cardH, Color{ 22, 12, 20, 255 }); + DrawRectangleLines(cardX, cardY, cardW, cardH, isHover && canAfford ? CyberColors::NeonYellow : Color{ 180, 50, 80, 255 }); + + DrawText("CONTRACT I", cardX + 16, cardY + 16, 12, CyberColors::NeonPink); + DrawText("BLOOD PACT", cardX + 16, cardY + 34, 16, WHITE); + DrawLine(cardX + 16, cardY + 58, cardX + cardW - 16, cardY + 58, Color{ 80, 30, 45, 255 }); + + DrawText("SACRIFICE:", cardX + 16, cardY + 70, 11, GRAY); + DrawText("-25 CORE HULL HP", cardX + 16, cardY + 86, 13, RED); + + DrawText("ACQUIRE:", cardX + 16, cardY + 115, 11, GRAY); + DrawText("+60 Core Chips", cardX + 16, cardY + 132, 13, CyberColors::NeonYellow); + DrawText("+1 Kinetic Round", cardX + 16, cardY + 150, 12, CyberColors::NeonGreen); + DrawText("+1 Glitch Round", cardX + 16, cardY + 168, 12, CyberColors::NeonPurple); + + DrawRectangleRec(btnRect, canAfford ? (isHover ? Color{ 60, 20, 30, 255 } : Color{ 35, 14, 20, 255 }) : Color{ 20, 14, 16, 255 }); + DrawRectangleLinesEx(btnRect, isHover && canAfford ? 2.0f : 1.0f, canAfford ? RED : DARKGRAY); + DrawText(canAfford ? "[ 1 ] EXECUTE" : "HP TOO LOW", static_cast(btnRect.x + 28), static_cast(btnRect.y + 10), 12, canAfford ? WHITE : GRAY); + } + + // Option 1: OVERCHARGE + { + int cardX = startX + (cardW + gapX); + Rectangle btnRect = getShrineOptionButtonRect(1, screenW, screenH); + bool isHover = CheckCollisionPointRec(mouse, btnRect); + bool canAfford = player.getCyberPicks() >= 1; + + DrawRectangle(cardX, cardY, cardW, cardH, Color{ 12, 20, 28, 255 }); + DrawRectangleLines(cardX, cardY, cardW, cardH, isHover && canAfford ? CyberColors::NeonYellow : CyberColors::NeonCyan); + + DrawText("CONTRACT II", cardX + 16, cardY + 16, 12, CyberColors::NeonCyan); + DrawText("SHIELD OVERCLOCK", cardX + 16, cardY + 34, 15, WHITE); + DrawLine(cardX + 16, cardY + 58, cardX + cardW - 16, cardY + 58, Color{ 30, 50, 75, 255 }); + + DrawText("SACRIFICE:", cardX + 16, cardY + 70, 11, GRAY); + DrawText("-1 ICE Cyber Pick", cardX + 16, cardY + 86, 13, CyberColors::NeonYellow); + + DrawText("ACQUIRE:", cardX + 16, cardY + 115, 11, GRAY); + DrawText("+15 Maximum Shield", cardX + 16, cardY + 132, 13, CyberColors::NeonCyan); + DrawText("100% Shield Recharge", cardX + 16, cardY + 150, 12, CyberColors::NeonGreen); + + DrawRectangleRec(btnRect, canAfford ? (isHover ? Color{ 20, 45, 65, 255 } : Color{ 12, 28, 42, 255 }) : Color{ 16, 20, 24, 255 }); + DrawRectangleLinesEx(btnRect, isHover && canAfford ? 2.0f : 1.0f, canAfford ? CyberColors::NeonCyan : DARKGRAY); + DrawText(canAfford ? "[ 2 ] EXECUTE" : "NO PICKS", static_cast(btnRect.x + 28), static_cast(btnRect.y + 10), 12, canAfford ? WHITE : GRAY); + } + + // Option 2: NANITE ALCHEMY + { + int cardX = startX + 2 * (cardW + gapX); + Rectangle btnRect = getShrineOptionButtonRect(2, screenW, screenH); + bool isHover = CheckCollisionPointRec(mouse, btnRect); + bool canAfford = player.getAmmoCount(AmmoType::KINETIC) >= 2; + + DrawRectangle(cardX, cardY, cardW, cardH, Color{ 14, 22, 20, 255 }); + DrawRectangleLines(cardX, cardY, cardW, cardH, isHover && canAfford ? CyberColors::NeonYellow : CyberColors::NeonGreen); + + DrawText("CONTRACT III", cardX + 16, cardY + 16, 12, CyberColors::NeonGreen); + DrawText("NANITE ALCHEMY", cardX + 16, cardY + 34, 15, WHITE); + DrawLine(cardX + 16, cardY + 58, cardX + cardW - 16, cardY + 58, Color{ 25, 55, 40, 255 }); + + DrawText("SACRIFICE:", cardX + 16, cardY + 70, 11, GRAY); + DrawText("-2 Kinetic Rounds", cardX + 16, cardY + 86, 13, RAYWHITE); + + DrawText("ACQUIRE:", cardX + 16, cardY + 115, 11, GRAY); + DrawText("+1 Nanite Round", cardX + 16, cardY + 132, 13, CyberColors::NeonGreen); + DrawText("+1 Glitch Round", cardX + 16, cardY + 150, 12, CyberColors::NeonPurple); + + DrawRectangleRec(btnRect, canAfford ? (isHover ? Color{ 20, 50, 35, 255 } : Color{ 14, 32, 24, 255 }) : Color{ 16, 20, 18, 255 }); + DrawRectangleLinesEx(btnRect, isHover && canAfford ? 2.0f : 1.0f, canAfford ? CyberColors::NeonGreen : DARKGRAY); + DrawText(canAfford ? "[ 3 ] EXECUTE" : "LACK KINETIC", static_cast(btnRect.x + 24), static_cast(btnRect.y + 10), 12, canAfford ? WHITE : GRAY); + } + + // Decline button + Rectangle btnClose = getShrineCloseButtonRect(screenW, screenH); + bool isHoverClose = CheckCollisionPointRec(mouse, btnClose); + DrawRectangleRec(btnClose, isHoverClose ? Color{ 40, 20, 30, 255 } : Color{ 20, 12, 18, 255 }); + DrawRectangleLinesEx(btnClose, isHoverClose ? 2.0f : 1.0f, isHoverClose ? RED : Color{ 160, 60, 80, 255 }); + DrawText("[ESC / 4] DECLINE PROTOCOL", static_cast(btnClose.x + 18), static_cast(btnClose.y + 10), 12, isHoverClose ? RED : RAYWHITE); +} + void UIRenderer::drawPauseMenu(bool hasSavedMessage) { DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 225 }); diff --git a/src/render/UIRenderer.hpp b/src/render/UIRenderer.hpp index 129631b..6daa700 100644 --- a/src/render/UIRenderer.hpp +++ b/src/render/UIRenderer.hpp @@ -18,6 +18,15 @@ public: void drawWorkbenchModal(const Player& player); static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH); static Rectangle getWorkbenchCloseButtonRect(int screenW, int screenH); + + void drawImplantDraftModal(const std::vector& 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();