feat: Roguelike Neural Implants system, Elite Drones & Corridor Hazards

This commit is contained in:
Игорь Федосеев 2026-10-05 15:46:19 +03:00
parent 7b82325729
commit 1e3b05d5c8
23 changed files with 1515 additions and 74 deletions

View file

@ -11,11 +11,16 @@ 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_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<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 +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<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 +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<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!");
@ -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<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 +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);

View file

@ -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<std::string> m_combatLog;
std::vector<FloatingText> m_floatingTexts;
};

View file

@ -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

@ -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 };

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;
@ -381,10 +400,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 +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<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 +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<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 +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;

View file

@ -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<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

@ -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<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();
@ -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<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 });
@ -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<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 = 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

View file

@ -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<unsigned char>(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<unsigned char>(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<unsigned char>(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<unsigned char>(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&>(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();

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;

View file

@ -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<int>(x * cellSize);
int py = startY + static_cast<int>(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<int>(laser.x * cellSize);
int ly = startY + static_cast<int>(laser.y * cellSize);
int cs = static_cast<int>(cellSize) + 1;
Color lCol = laser.disarmed ? DARKGRAY : (laser.active ? RED : CyberColors::NeonCyan);
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<int>((mine.x + 0.5f) * cellSize);
int my = startY + static_cast<int>((mine.y + 0.5f) * cellSize);
Color mCol = mine.defused ? DARKGRAY : (mine.isArmed() ? CyberColors::NeonOrange : RED);
DrawCircle(mx, my, std::max(2.0f, cellSize * 0.35f), mCol);
}
}
// 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<int>((shrine.x + 0.5f) * cellSize);
int sy = startY + static_cast<int>((shrine.y + 0.5f) * cellSize);
Color sCol = shrine.used ? DARKGRAY : CyberColors::NeonPink;
DrawCircle(sx, sy, std::max(2.5f, cellSize * 0.45f), sCol);
}
}
// 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<int>((me.x + 0.5f) * cellSize);
int ey = startY + static_cast<int>((me.y + 0.5f) * cellSize);
if (me.enemy.isElite()) {
DrawCircle(ex, ey, std::max(3.0f, cellSize * 0.5f), CyberColors::NeonYellow);
DrawCircleLines(ex, ey, std::max(4.0f, cellSize * 0.65f), CyberColors::NeonPink);
} else {
DrawCircle(ex, ey, std::max(2.0f, cellSize * 0.35f), RED);
}
}
}
// Player icon and direction on minimap
int plX = startX + static_cast<int>((player.getGridX() + 0.5f) * cellSize);
int plY = startY + static_cast<int>((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<float>(bX), static_cast<float>(bY), 74.0f, 24.0f };
bool isHover = CheckCollisionPointRec(mousePos, bRect);
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 = 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<int>(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<float>(bX), static_cast<float>(bY), 104.0f, 24.0f };
bool isHover = CheckCollisionPointRec(mousePos, bRect);
if (s < static_cast<int>(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<int>(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<int>(btnClose.x + 18), static_cast<int>(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<float>(cardX + 15), static_cast<float>(cardY + cardH - 42), static_cast<float>(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<float>(modalX + (modalW - 400) / 2), static_cast<float>(modalY + modalH - 46), 400.0f, 32.0f };
}
void UIRenderer::drawImplantDraftModal(const std::vector<ImplantType>& 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<int>(i) * (cardW + gapX);
Rectangle btnRect = getDraftCardButtonRect(static_cast<int>(i), screenW, screenH);
bool isHoverCard = CheckCollisionPointRec(mouse, Rectangle{ static_cast<float>(cardX), static_cast<float>(cardY), static_cast<float>(cardW), static_cast<float>(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<int>(btnRect.x + (btnRect.width - lblW) / 2), static_cast<int>(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<float>(slX), static_cast<float>(slY), static_cast<float>(slotW), static_cast<float>(slotH) };
bool isHoverSlot = CheckCollisionPointRec(mouse, slotRect);
bool isSelectedForReplace = (s == selectedReplaceSlot);
if (s < static_cast<int>(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<int>(btnScrap.x + 24), static_cast<int>(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<float>(cardX + 15), static_cast<float>(cardY + cardH - 45), static_cast<float>(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<float>(modalX + (modalW - 240) / 2), static_cast<float>(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<int>(btnRect.x + 28), static_cast<int>(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<int>(btnRect.x + 28), static_cast<int>(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<int>(btnRect.x + 24), static_cast<int>(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<int>(btnClose.x + 18), static_cast<int>(btnClose.y + 10), 12, isHoverClose ? RED : RAYWHITE);
}
void UIRenderer::drawPauseMenu(bool hasSavedMessage) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 225 });

View file

@ -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<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();