feat: Roguelike Neural Implants system, Elite Drones & Corridor Hazards
This commit is contained in:
parent
7b82325729
commit
1e3b05d5c8
23 changed files with 1515 additions and 74 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue