diff --git a/src/combat/CombatSystem.cpp b/src/combat/CombatSystem.cpp index fe70dbd..911bce8 100644 --- a/src/combat/CombatSystem.cpp +++ b/src/combat/CombatSystem.cpp @@ -56,7 +56,11 @@ void CombatSystem::update(float dt, Player& player) { } 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(); } else { m_phase = CombatPhase::ENEMY_TURN; @@ -68,7 +72,11 @@ void CombatSystem::update(float dt, Player& player) { 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(); } else { m_phase = CombatPhase::ENEMY_TURN; @@ -120,33 +128,51 @@ void CombatSystem::actionTripleShot(Player& player) { void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& player) { const auto& info = getAmmoInfo(chamberAmmo); + int tier = player.getAmmoTier(chamberAmmo); + switch (chamberAmmo) { - case AmmoType::EMP: - if ((std::rand() % 100) < static_cast(info.effectChance * 100)) { - m_enemy->applyStatus(StatusEffect::SHOCKED, 1, 0); + case AmmoType::EMP: { + float chance = (tier >= 3) ? 1.0f : ((tier == 2) ? 0.85f : info.effectChance); + if ((std::rand() % 100) < static_cast(chance * 100)) { + m_enemy->applyStatus(StatusEffect::SHOCKED, (tier >= 3 ? 2 : 1), 0); addLog(m_enemy->getName() + " is SHOCKED! Reboot delayed."); - addFloatingText("STUNNED!", { 640, 180 }, CyberColors::NeonCyan); + addFloatingText((tier >= 3) ? "EMP OVERLOAD!" : "STUNNED!", { 640, 180 }, CyberColors::NeonCyan); + } + if (tier >= 3 && m_enemy->getShield() > 0) { + m_enemy->takeDamage(m_enemy->getShield(), AmmoType::EMP); + addLog("EMP TIER III: Hostile barrier shattered!"); } break; - case AmmoType::OVERHEAT: - m_enemy->applyStatus(StatusEffect::BURNING, 3, 10); - addLog(m_enemy->getName() + " ignited with THERMAL BURN!"); + } + case AmmoType::OVERHEAT: { + int duration = (tier >= 3) ? 5 : ((tier == 2) ? 4 : 3); + int tickDmg = (tier >= 3) ? 22 : ((tier == 2) ? 15 : 10); + m_enemy->applyStatus(StatusEffect::BURNING, duration, tickDmg); + addLog(m_enemy->getName() + " ignited with THERMAL BURN (" + std::to_string(duration) + " turns)!"); addFloatingText("BURNING!", { 640, 180 }, CyberColors::NeonOrange); break; - case AmmoType::CORROSIVE: - m_enemy->applyStatus(StatusEffect::CORRODED, 3, 0); - addLog("Armor corroded! Target takes +50% damage."); + } + case AmmoType::CORROSIVE: { + int duration = (tier >= 3) ? 5 : ((tier == 2) ? 4 : 3); + int power = (tier >= 3) ? 2 : ((tier == 2) ? 1 : 0); + m_enemy->applyStatus(StatusEffect::CORRODED, duration, power); + std::string pct = (tier >= 3) ? "+100%" : ((tier == 2) ? "+75%" : "+50%"); + addLog("Armor corroded! Target takes " + pct + " damage (" + std::to_string(duration) + " turns)."); addFloatingText("CORROSION", { 640, 180 }, CyberColors::NeonGreen); break; - case AmmoType::GLITCH: - m_enemy->applyStatus(StatusEffect::GLITCHED, 3, 0); - addLog("Glitch injected! Enemy targeting scrambled."); + } + case AmmoType::GLITCH: { + int duration = (tier >= 3) ? 5 : ((tier == 2) ? 4 : 3); + m_enemy->applyStatus(StatusEffect::GLITCHED, duration, 0); + addLog("Glitch injected! Enemy targeting scrambled (" + std::to_string(duration) + " turns)."); addFloatingText("GLITCHED", { 640, 180 }, CyberColors::NeonPurple); break; + } case AmmoType::NANITE: { - int siphon = dealt / 2; + float ratio = (tier >= 3) ? 0.85f : ((tier == 2) ? 0.70f : 0.50f); + int siphon = std::max(1, static_cast(dealt * ratio)); player.heal(siphon); - player.restoreShield(siphon / 2); + player.restoreShield(tier >= 3 ? siphon : siphon / 2); addLog("Nanites siphoned +" + std::to_string(siphon) + " HP!"); addFloatingText("+" + std::to_string(siphon) + " HP", { 300, 480 }, CyberColors::NeonGreen); break; @@ -167,7 +193,10 @@ void CombatSystem::resolveBurstShot(Player& player, int shotNum) { addLog("[SHOT " + std::to_string(shotNum) + "] *CLICK* Empty chamber misfire!"); addFloatingText("MISFIRE", { 640.0f + offsetX, 240.0f }, GRAY); } else { - int dmg = info.baseDamage; + int dmg = player.getEffectiveDamage(chamberAmmo); + int tier = player.getAmmoTier(chamberAmmo); + auto tierData = getAmmoTierData(chamberAmmo, tier); + bool isWeakness = (chamberAmmo == m_enemy->getWeakness()); if (isWeakness) { addFloatingText("WEAKNESS!", { 640.0f + offsetX, 200.0f }, CyberColors::NeonYellow); @@ -176,7 +205,7 @@ void CombatSystem::resolveBurstShot(Player& player, int shotNum) { int dealt = m_enemy->takeDamage(dmg, chamberAmmo); SoundManager::get().playEnemyHit(); - addLog("[SHOT " + std::to_string(shotNum) + "] Hit with [" + info.name + "] for " + std::to_string(dealt) + " damage!"); + 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); applyAmmoStatus(chamberAmmo, dealt, player); @@ -194,7 +223,10 @@ void CombatSystem::resolveShotImpact(Player& player) { } else { SoundManager::get().playGunshot(); - int dmg = info.baseDamage; + int dmg = player.getEffectiveDamage(chamberAmmo); + int tier = player.getAmmoTier(chamberAmmo); + auto tierData = getAmmoTierData(chamberAmmo, tier); + bool isWeakness = (chamberAmmo == m_enemy->getWeakness()); if (isWeakness) { addLog("WEAKNESS EXPLOITED! Critical damage surge!"); @@ -204,7 +236,7 @@ void CombatSystem::resolveShotImpact(Player& player) { int dealt = m_enemy->takeDamage(dmg, chamberAmmo); SoundManager::get().playEnemyHit(); - addLog("Hit with [" + info.name + "] for " + std::to_string(dealt) + " damage!"); + addLog("Hit with [" + info.name + " " + tierData.tierBadge + "] for " + std::to_string(dealt) + " damage!"); addFloatingText("-" + std::to_string(dealt), { 640, 240 }, info.color); applyAmmoStatus(chamberAmmo, dealt, player); diff --git a/src/core/Common.hpp b/src/core/Common.hpp index c93ef45..c800769 100644 --- a/src/core/Common.hpp +++ b/src/core/Common.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "raylib.h" namespace dnr { @@ -62,6 +63,101 @@ inline const AmmoInfo& getAmmoInfo(AmmoType type) { } } +struct AmmoTierData { + int tier; // 1, 2, 3 + std::string tierTitle; // "TIER I: STANDARD", "TIER II: REINFORCED", "TIER III: OVERCLOCK" + std::string tierBadge; // "[★☆☆]", "[★★☆]", "[★★★]" + int damage; + int upgradeCost; // 0 for tier 1, 40 for tier 2, 80 for tier 3 + std::string perkDesc; +}; + +inline AmmoTierData getAmmoTierData(AmmoType type, int tier) { + int t = std::clamp(tier, 1, 3); + const auto& base = getAmmoInfo(type); + + AmmoTierData data; + data.tier = t; + + if (t == 1) { + data.tierTitle = "TIER I: BASE SPEC"; + data.tierBadge = "[* ]"; + data.damage = base.baseDamage; + data.upgradeCost = 0; + data.perkDesc = "Standard factory configuration."; + } else if (t == 2) { + data.tierTitle = "TIER II: REINFORCED"; + data.tierBadge = "[** ]"; + data.upgradeCost = 40; + switch (type) { + case AmmoType::KINETIC: + data.damage = 50; + data.perkDesc = "+43% Sabot DMG & High velocity impact."; + break; + case AmmoType::EMP: + data.damage = 28; + data.perkDesc = "+40% DMG, 85% Stun chance & +50% Shield drain."; + break; + case AmmoType::OVERHEAT: + data.damage = 26; + data.perkDesc = "+44% DMG & Burn extended to 4 turns (+50% DoT)."; + break; + case AmmoType::CORROSIVE: + data.damage = 22; + data.perkDesc = "+46% DMG & Corrodes armor (+75% damage taken)."; + break; + case AmmoType::GLITCH: + data.damage = 32; + data.perkDesc = "+45% DMG & Scrambles accuracy for 4 turns."; + break; + case AmmoType::NANITE: + data.damage = 24; + data.perkDesc = "+50% DMG & Siphons 70% of impact to Player HP."; + break; + default: + data.damage = 0; + data.perkDesc = ""; + break; + } + } else { // t == 3 + data.tierTitle = "TIER III: OVERCLOCK MASTER"; + data.tierBadge = "[***]"; + data.upgradeCost = 80; + switch (type) { + case AmmoType::KINETIC: + data.damage = 70; + data.perkDesc = "+100% Hyper-Kinetic Sabot & Armor Penetration."; + break; + case AmmoType::EMP: + data.damage = 40; + data.perkDesc = "+100% DMG, 100% Stun on shield break & EMP Surge."; + break; + case AmmoType::OVERHEAT: + data.damage = 38; + data.perkDesc = "+111% DMG, 5-Turn Inferno & Thermal Detonation."; + break; + case AmmoType::CORROSIVE: + data.damage = 32; + data.perkDesc = "+113% DMG & Total armor strip (+100% damage taken)."; + break; + case AmmoType::GLITCH: + data.damage = 46; + data.perkDesc = "+109% DMG & Subsystem lock (High enemy misfire rate)."; + break; + case AmmoType::NANITE: + data.damage = 36; + data.perkDesc = "+125% DMG & Siphons 85% to HP + Recharges Shield."; + break; + default: + data.damage = 0; + data.perkDesc = ""; + break; + } + } + + return data; +} + enum class Direction { NORTH = 0, EAST = 1, diff --git a/src/core/Game.cpp b/src/core/Game.cpp index 2a4fbb7..3e2b313 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -43,6 +43,7 @@ void Game::enterCyberspace() { void Game::enterRestRoom() { int completedFloor = m_dungeon.getFloor(); m_restRoom.init(m_player, completedFloor); + m_showWorkbenchUI = false; m_state = GameState::REST_ROOM; SoundManager::get().playTrack(MusicTrack::REST_ROOM); SoundManager::get().playSkillGreat(); @@ -79,6 +80,12 @@ bool Game::loadSavedProgress() { m_player.setAmmoCount(static_cast(i), data.ammoCounts[i]); } + // Restore Core Chips & Cartridge upgrade tiers + m_player.setTechScraps(data.techScraps); + for (size_t i = 0; i < data.ammoTiers.size(); ++i) { + m_player.setAmmoTier(static_cast(i), data.ammoTiers[i]); + } + if (data.inHub) { m_hubRoom.init(m_player); m_state = GameState::HUB; @@ -238,6 +245,61 @@ void Game::tryInteractHub() { } void Game::updateRestRoom(float dt) { + if (m_showWorkbenchUI) { + int screenW = GetScreenWidth(); + int screenH = GetScreenHeight(); + Vector2 mouse = GetMousePosition(); + bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); + + Rectangle btnClose = UIRenderer::getWorkbenchCloseButtonRect(screenW, screenH); + if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_E) || (mouseClicked && CheckCollisionPointRec(mouse, btnClose))) { + m_showWorkbenchUI = false; + SoundManager::get().playTumblerClick(); + return; + } + + // Check key shortcuts 1..6 or clicking card buttons + AmmoType upgradeList[6] = { + AmmoType::KINETIC, + AmmoType::EMP, + AmmoType::OVERHEAT, + AmmoType::CORROSIVE, + AmmoType::GLITCH, + AmmoType::NANITE + }; + + int triggeredIndex = -1; + if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1)) triggeredIndex = 0; + else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2)) triggeredIndex = 1; + else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3)) triggeredIndex = 2; + else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4)) triggeredIndex = 3; + else if (IsKeyPressed(KEY_FIVE) || IsKeyPressed(KEY_KP_5)) triggeredIndex = 4; + else if (IsKeyPressed(KEY_SIX) || IsKeyPressed(KEY_KP_6)) triggeredIndex = 5; + + if (mouseClicked) { + for (int i = 0; i < 6; ++i) { + Rectangle btnRect = UIRenderer::getWorkbenchUpgradeButtonRect(i, screenW, screenH); + if (CheckCollisionPointRec(mouse, btnRect)) { + triggeredIndex = i; + break; + } + } + } + + if (triggeredIndex >= 0 && triggeredIndex < 6) { + AmmoType ammo = upgradeList[triggeredIndex]; + if (m_player.canUpgradeAmmo(ammo)) { + if (m_player.upgradeAmmoTier(ammo)) { + SoundManager::get().playSkillSuccess(); + } + } else { + SoundManager::get().playDamage(); + } + } + + return; + } + m_restRoom.update(dt, m_player); Vector2 dirVec = getDirectionOffset(m_player.getFacing()); @@ -245,15 +307,17 @@ void Game::updateRestRoom(float dt) { // Check interaction context m_contextPrompt = ""; if (m_restRoom.isFacingGateway(m_player)) { - m_contextPrompt = "[SPACE / E] DIVE INTO LAYER 0" + std::to_string(m_restRoom.getNextFloor()) + " // CYBER GATEWAY"; + m_contextPrompt = "[SPACE / E] DESCEND TO LAYER 0" + std::to_string(m_restRoom.getNextFloor()) + " // PALACE ARCHWAY"; } else if (m_restRoom.isFacingRechargeMatrix(m_player)) { if (!m_restRoom.isRecharged()) { - m_contextPrompt = "[SPACE / E] NANITE MATRIX // FULL RESTORATION (HP & SHIELD)"; + m_contextPrompt = "[SPACE / E] NECTAR FOUNTAIN // DRINK (HP & SHIELD RESTORED)"; } else { - m_contextPrompt = "NANITE MATRIX RECHARGED // SYSTEMS OPTIMAL"; + m_contextPrompt = "NECTAR FOUNTAIN EMPTIED // VITALITY OPTIMAL"; } } else if (m_restRoom.isFacingTerminal(m_player)) { - m_contextPrompt = "[SPACE / E] ARCHIVE TERMINAL // SAVE RUN PROGRESS"; + m_contextPrompt = "[SPACE / E] SCRIBE'S STELE // ENGRAVE PROGRESS (SAVE RUN)"; + } else if (m_restRoom.isFacingWorkbench(m_player)) { + m_contextPrompt = "[SPACE / E] CRAFTSMAN WORKBENCH // UPGRADE AMMO PROTOCOLS"; } if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) { @@ -311,6 +375,9 @@ void Game::tryInteractRestRoom() { } } else if (m_restRoom.isFacingTerminal(m_player)) { saveCurrentProgress(); + } else if (m_restRoom.isFacingWorkbench(m_player)) { + m_showWorkbenchUI = true; + SoundManager::get().playTumblerClick(); } } @@ -441,6 +508,7 @@ void Game::tryInteractDungeon() { m_player.addAmmo(a, 1); } m_player.addPicks(ld->picksReward); + m_player.addTechScraps(35 + (std::rand() % 25)); SoundManager::get().playPickup(); return; } @@ -471,6 +539,9 @@ void Game::updateHacking(float dt) { if (m_hacking.isCritical()) { m_player.addAmmo(AmmoType::NANITE, 1); m_player.addPicks(1); + m_player.addTechScraps(40); + } else { + m_player.addTechScraps(15); } m_state = GameState::EXPLORATION; } else if (m_hacking.isAlarmTriggered()) { @@ -714,7 +785,10 @@ void Game::draw() { case GameState::REST_ROOM: m_renderer.renderRestRoom(m_restRoom, m_player, m_animTime); - m_uiRenderer.drawRestRoomHUD(m_restRoom.getNextFloor(), m_player, m_contextPrompt); + m_uiRenderer.drawRestRoomHUD(m_restRoom.getNextFloor(), m_player, m_showWorkbenchUI ? "" : m_contextPrompt); + if (m_showWorkbenchUI) { + m_uiRenderer.drawWorkbenchModal(m_player); + } m_uiRenderer.drawMatrixOverlay(m_animTime); break; diff --git a/src/core/Game.hpp b/src/core/Game.hpp index bc4a56b..1d7e025 100644 --- a/src/core/Game.hpp +++ b/src/core/Game.hpp @@ -76,6 +76,7 @@ private: bool m_isDraggingSlider = false; bool m_showCombatCodex = false; + bool m_showWorkbenchUI = false; int m_lastPlayerHp = 100; bool m_shouldQuit = false; diff --git a/src/core/SaveSystem.cpp b/src/core/SaveSystem.cpp index 6c87d70..aee03f4 100644 --- a/src/core/SaveSystem.cpp +++ b/src/core/SaveSystem.cpp @@ -15,6 +15,7 @@ bool SaveSystem::saveGame(const std::string& path, int floor, bool inHub, const out << "hp " << player.getHp() << " " << player.getMaxHp() << "\n"; out << "shield " << player.getShield() << " " << player.getMaxShield() << "\n"; out << "picks " << player.getCyberPicks() << "\n"; + out << "scraps " << player.getTechScraps() << "\n"; out << "chambers"; const auto& rev = player.getRevolver(); @@ -30,6 +31,13 @@ bool SaveSystem::saveGame(const std::string& path, int floor, bool inHub, const } out << "\n"; + out << "tiers"; + const auto& tiers = player.getAmmoTiers(); + for (size_t i = 0; i < tiers.size(); ++i) { + out << " " << tiers[i]; + } + out << "\n"; + return true; } @@ -55,6 +63,8 @@ bool SaveSystem::loadGame(const std::string& path, SaveData& outData) { in >> outData.shield >> outData.maxShield; } else if (key == "picks") { in >> outData.picks; + } else if (key == "scraps") { + in >> outData.techScraps; } else if (key == "chambers") { for (int i = 0; i < 6; ++i) { in >> outData.chambers[i]; @@ -63,6 +73,10 @@ bool SaveSystem::loadGame(const std::string& path, SaveData& outData) { for (size_t i = 0; i < outData.ammoCounts.size(); ++i) { in >> outData.ammoCounts[i]; } + } else if (key == "tiers") { + for (size_t i = 0; i < outData.ammoTiers.size(); ++i) { + in >> outData.ammoTiers[i]; + } } } diff --git a/src/core/SaveSystem.hpp b/src/core/SaveSystem.hpp index a9d3c7f..bd18a79 100644 --- a/src/core/SaveSystem.hpp +++ b/src/core/SaveSystem.hpp @@ -16,8 +16,10 @@ struct SaveData { int shield = 40; int maxShield = 40; int picks = 5; + int techScraps = 0; std::array chambers{}; std::array ammoCounts{}; + std::array ammoTiers{ 1, 1, 1, 1, 1, 1, 1 }; }; class SaveSystem { diff --git a/src/dungeon/Player.cpp b/src/dungeon/Player.cpp index 03cc6b8..c858615 100644 --- a/src/dungeon/Player.cpp +++ b/src/dungeon/Player.cpp @@ -22,6 +22,8 @@ void Player::reset(int startX, int startY, Direction startDir) { m_shield = 40; m_maxShield = 40; m_cyberPicks = 5; + m_techScraps = 0; + m_ammoTiers.fill(1); m_revolver.reset(); m_statuses.clear(); @@ -192,4 +194,43 @@ void Player::setAmmoCount(AmmoType type, int count) { } } +int Player::getAmmoTier(AmmoType type) const { + int idx = static_cast(type); + if (idx >= 0 && idx < static_cast(m_ammoTiers.size())) { + return m_ammoTiers[idx]; + } + return 1; +} + +void Player::setAmmoTier(AmmoType type, int tier) { + int idx = static_cast(type); + if (idx >= 0 && idx < static_cast(m_ammoTiers.size())) { + m_ammoTiers[idx] = std::clamp(tier, 1, 3); + } +} + +bool Player::canUpgradeAmmo(AmmoType type) const { + if (type == AmmoType::EMPTY) return false; + int curTier = getAmmoTier(type); + if (curTier >= 3) return false; + int nextCost = getAmmoTierData(type, curTier + 1).upgradeCost; + return m_techScraps >= nextCost; +} + +bool Player::upgradeAmmoTier(AmmoType type) { + if (!canUpgradeAmmo(type)) return false; + int curTier = getAmmoTier(type); + int nextCost = getAmmoTierData(type, curTier + 1).upgradeCost; + if (spendTechScraps(nextCost)) { + setAmmoTier(type, curTier + 1); + return true; + } + return false; +} + +int Player::getEffectiveDamage(AmmoType type) const { + int t = getAmmoTier(type); + return getAmmoTierData(type, t).damage; +} + } // namespace dnr diff --git a/src/dungeon/Player.hpp b/src/dungeon/Player.hpp index eae8cee..279d29c 100644 --- a/src/dungeon/Player.hpp +++ b/src/dungeon/Player.hpp @@ -61,6 +61,25 @@ public: void addAmmoToPouch(AmmoType type) { addAmmo(type, 1); } bool removeAmmoFromPouch(AmmoType type) { return consumeAmmo(type); } + // Currency & Upgrades + int getTechScraps() const { return m_techScraps; } + void addTechScraps(int amount) { m_techScraps += amount; } + bool spendTechScraps(int amount) { + if (m_techScraps >= amount) { + m_techScraps -= amount; + return true; + } + return false; + } + void setTechScraps(int amount) { m_techScraps = amount; } + + int getAmmoTier(AmmoType type) const; + void setAmmoTier(AmmoType type, int tier); + bool canUpgradeAmmo(AmmoType type) const; + bool upgradeAmmoTier(AmmoType type); + int getEffectiveDamage(AmmoType type) const; + const std::array& getAmmoTiers() const { return m_ammoTiers; } + const std::vector& getStatuses() const { return m_statuses; } private: @@ -77,8 +96,10 @@ private: int m_shield = 40; int m_maxShield = 40; int m_cyberPicks = 5; + int m_techScraps = 0; std::array m_ammoCounts{ 0 }; + std::array m_ammoTiers{ 1, 1, 1, 1, 1, 1, 1 }; Revolver m_revolver; std::vector m_statuses; }; diff --git a/src/hub/RestRoom.cpp b/src/hub/RestRoom.cpp index 5544577..2a08938 100644 --- a/src/hub/RestRoom.cpp +++ b/src/hub/RestRoom.cpp @@ -23,12 +23,18 @@ bool RestRoom::tryMove(int dx, int dy, Player& player) { // Outer boundaries if (nx < 1 || nx > 5 || ny < 1 || ny > 5) return false; - // Obstacle: Central nanite recharge pod at (3, 3) + // Obstacle: Central Marble Nectar Fountain at (3, 3) if (nx == 3 && ny == 3) return false; - // Obstacle: Terminal at (1, 3) + // Obstacle: Scribe's Stele at West wall (1, 3) if (nx == 1 && ny == 3) return false; + // Obstacle: Craftsman's Workbench at East wall (5, 3) + if (nx == 5 && ny == 3) return false; + + // Obstacle: Corner amphorae and crate piles at (1, 1) and (5, 1) + if ((nx == 1 && ny == 1) || (nx == 5 && ny == 1)) return false; + player.setGridPosition(nx, ny); return true; } @@ -43,7 +49,7 @@ bool RestRoom::isFacingGateway(const Player& player) const { } bool RestRoom::isFacingRechargeMatrix(const Player& player) const { - // Central recharge station at (3, 3) + // Central Nectar Fountain at (3, 3) int px = player.getGridX(); int py = player.getGridY(); Direction f = player.getFacing(); @@ -57,7 +63,7 @@ bool RestRoom::isFacingRechargeMatrix(const Player& player) const { } bool RestRoom::isFacingTerminal(const Player& player) const { - // Save terminal at (1, 3) + // Scribe's Stele at West wall (1, 3) int px = player.getGridX(); int py = player.getGridY(); Direction f = player.getFacing(); @@ -65,6 +71,19 @@ bool RestRoom::isFacingTerminal(const Player& player) const { return (px == 2 && py == 3 && f == Direction::WEST); } +bool RestRoom::isFacingWorkbench(const Player& player) const { + // Craftsman's Workbench at East wall (5, 3) + int px = player.getGridX(); + int py = player.getGridY(); + Direction f = player.getFacing(); + + if (px == 4 && py == 3 && f == Direction::EAST) return true; + if (px == 5 && py == 4 && f == Direction::NORTH) return true; + if (px == 5 && py == 2 && f == Direction::SOUTH) return true; + + return false; +} + void RestRoom::triggerRecharge(Player& player) { player.heal(player.getMaxHp()); player.restoreShield(player.getMaxShield()); diff --git a/src/hub/RestRoom.hpp b/src/hub/RestRoom.hpp index 6df2124..638a165 100644 --- a/src/hub/RestRoom.hpp +++ b/src/hub/RestRoom.hpp @@ -15,7 +15,10 @@ public: bool tryMove(int dx, int dy, Player& player); bool isFacingGateway(const Player& player) const; bool isFacingRechargeMatrix(const Player& player) const; + bool isFacingNectarFountain(const Player& player) const { return isFacingRechargeMatrix(player); } bool isFacingTerminal(const Player& player) const; + bool isFacingStele(const Player& player) const { return isFacingTerminal(player); } + bool isFacingWorkbench(const Player& player) const; int getNextFloor() const { return m_completedFloor + 1; } int getCompletedFloor() const { return m_completedFloor; } @@ -29,6 +32,7 @@ public: Vector3 getGatewayPosition() const { return m_gatewayPos; } Vector3 getRechargePosition() const { return m_rechargePos; } Vector3 getTerminalPosition() const { return m_terminalPos; } + Vector3 getWorkbenchPosition() const { return m_workbenchPos; } private: int m_width = 7; @@ -38,7 +42,8 @@ private: Vector3 m_gatewayPos{ 6.0f, 1.2f, 2.0f }; Vector3 m_rechargePos{ 6.0f, 0.8f, 6.0f }; - Vector3 m_terminalPos{ 2.0f, 0.8f, 6.0f }; + Vector3 m_terminalPos{ 1.2f, 0.8f, 6.0f }; + Vector3 m_workbenchPos{ 10.8f, 0.8f, 6.0f }; }; } // namespace dnr diff --git a/src/render/CyberRenderer.cpp b/src/render/CyberRenderer.cpp index ac41718..cea5717 100644 --- a/src/render/CyberRenderer.cpp +++ b/src/render/CyberRenderer.cpp @@ -864,6 +864,276 @@ Texture2D createDoorFrameTexture() { return tex; } +// Procedural generator: Ancient Palace Ashlar Sandstone Wall with Greek Key Meander (256x256) +Texture2D createAncientStoneWallTexture() { + const int w = 256; + const int h = 256; + Image img = GenImageColor(w, h, Color{ 185, 170, 142, 255 }); + Color* pixels = (Color*)img.data; + + const int rowH = 64; + + for (int y = 0; y < h; ++y) { + int row = y / rowH; + int rowY = y % rowH; + bool isHorizMortar = (rowY <= 2 || rowY >= rowH - 2); + + for (int x = 0; x < w; ++x) { + int shift = (row % 2 == 0) ? 0 : 128; + int blockCol = (x + shift) / 128; + int blockX = (x + shift) % 128; + bool isVertMortar = (blockX <= 2 || blockX >= 126); + + if (isHorizMortar || isVertMortar) { + pixels[y * w + x] = Color{ 88, 76, 62, 255 }; + continue; + } + + int blockSeed = (row * 23 + blockCol * 41); + int blockTone = (blockSeed % 25) - 12; + int chiselNoise = ((x * 17 + y * 31 + (x ^ y) * 7) % 17) - 8; + + int r = std::clamp(188 + blockTone + chiselNoise, 0, 255); + int g = std::clamp(172 + blockTone + chiselNoise, 0, 255); + int b = std::clamp(144 + blockTone + chiselNoise, 0, 255); + + if (row == 0 && rowY >= 14 && rowY <= 48) { + int fretX = x % 32; + bool isFret = (rowY >= 18 && rowY <= 22) || (rowY >= 40 && rowY <= 44) || + (fretX >= 14 && fretX <= 18 && rowY >= 22 && rowY <= 40); + if (isFret) { + r = std::clamp(r - 45, 0, 255); + g = std::clamp(g - 50, 0, 255); + b = std::clamp(b - 55, 0, 255); + } + } + + pixels[y * w + x] = Color{ (unsigned char)r, (unsigned char)g, (unsigned char)b, 255 }; + } + } + + Texture2D tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + UnloadImage(img); + return tex; +} + +// Procedural generator: Ancient Roman Opus Sectile Floor (Terracotta & Marble Flagstones) (256x256) +Texture2D createAncientFloorTexture() { + const int w = 256; + const int h = 256; + Image img = GenImageColor(w, h, Color{ 175, 100, 65, 255 }); + Color* pixels = (Color*)img.data; + + const int tileSize = 32; + + for (int y = 0; y < h; ++y) { + int tY = y / tileSize; + int py = y % tileSize; + bool isHorizGrout = (py <= 1 || py >= tileSize - 1); + + for (int x = 0; x < w; ++x) { + int tX = x / tileSize; + int px = x % tileSize; + bool isVertGrout = (px <= 1 || px >= tileSize - 1); + + if (isHorizGrout || isVertGrout) { + pixels[y * w + x] = Color{ 58, 48, 40, 255 }; + continue; + } + + bool isMarble = ((tX + tY) % 2 == 0); + int stoneNoise = ((x * 19 + y * 23) % 11) - 5; + + if (isMarble) { + int vein = ((x + y * 2) % 31 == 0 || (x * 2 - y + 500) % 37 == 0) ? -28 : 0; + int r = std::clamp(224 + stoneNoise + vein, 0, 255); + int g = std::clamp(218 + stoneNoise + vein, 0, 255); + int b = std::clamp(205 + stoneNoise + vein, 0, 255); + pixels[y * w + x] = Color{ (unsigned char)r, (unsigned char)g, (unsigned char)b, 255 }; + } else { + int r = std::clamp(178 + stoneNoise, 0, 255); + int g = std::clamp(96 + stoneNoise / 2, 0, 255); + int b = std::clamp(62 + stoneNoise / 3, 0, 255); + pixels[y * w + x] = Color{ (unsigned char)r, (unsigned char)g, (unsigned char)b, 255 }; + } + } + } + + Texture2D tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + UnloadImage(img); + return tex; +} + +// Procedural generator: Ancient Timber Ceiling Beams (256x256) +Texture2D createAncientCeilTexture() { + const int w = 256; + const int h = 256; + Image img = GenImageColor(w, h, Color{ 70, 42, 22, 255 }); + Color* pixels = (Color*)img.data; + + const int beamSpan = 64; + + for (int y = 0; y < h; ++y) { + int bY = y % beamSpan; + bool isBeam = (bY >= 14 && bY <= 50); + + for (int x = 0; x < w; ++x) { + if (isBeam) { + int grain = static_cast(std::sin((x * 0.08f + std::sin(y * 0.3f)) * 4.0f) * 12.0f); + int fiber = ((x * 13 + y * 29) % 7) - 3; + int r = std::clamp(76 + grain + fiber, 0, 255); + int g = std::clamp(46 + grain / 2 + fiber, 0, 255); + int b = std::clamp(26 + grain / 3 + fiber, 0, 255); + + if ((x % 48 >= 22 && x % 48 <= 26) && (bY >= 30 && bY <= 34)) { + r = 35; g = 35; b = 40; + } + pixels[y * w + x] = Color{ (unsigned char)r, (unsigned char)g, (unsigned char)b, 255 }; + } else { + int stipple = ((x * 19 + y * 23) % 15) - 7; + int r = std::clamp(142 + stipple, 0, 255); + int g = std::clamp(132 + stipple, 0, 255); + int b = std::clamp(116 + stipple, 0, 255); + pixels[y * w + x] = Color{ (unsigned char)r, (unsigned char)g, (unsigned char)b, 255 }; + } + } + } + + Texture2D tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + UnloadImage(img); + return tex; +} + +// Procedural generator: Weathered Ancient Storage Wood & Chest Planks (256x256) +Texture2D createAncientWoodTexture() { + const int w = 256; + const int h = 256; + Image img = GenImageColor(w, h, Color{ 105, 68, 38, 255 }); + Color* pixels = (Color*)img.data; + + const int plankW = 42; + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + int px = x % plankW; + bool isSeam = (px == 0 || px == plankW - 1); + if (isSeam) { + pixels[y * w + x] = Color{ 42, 26, 14, 255 }; + continue; + } + + int grain = static_cast(std::sin((y * 0.12f + std::sin(x * 0.05f)) * 5.0f) * 14.0f); + int noise = ((x * 17 + y * 29) % 9) - 4; + + int r = std::clamp(115 + grain + noise, 0, 255); + int g = std::clamp(75 + (grain * 2) / 3 + noise, 0, 255); + int b = std::clamp(45 + grain / 2 + noise, 0, 255); + + pixels[y * w + x] = Color{ (unsigned char)r, (unsigned char)g, (unsigned char)b, 255 }; + } + } + + Texture2D tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + UnloadImage(img); + return tex; +} + +// 3D Ancient Prop Helpers +void drawAncientAmphora(Vector3 pos, float height, Color clayColor) { + DrawCylinder({ pos.x, pos.y + height * 0.05f, pos.z }, height * 0.14f, height * 0.18f, height * 0.10f, 12, clayColor); + DrawCylinder({ pos.x, pos.y + height * 0.40f, pos.z }, height * 0.18f, height * 0.38f, height * 0.60f, 12, clayColor); + DrawCylinder({ pos.x, pos.y + height * 0.72f, pos.z }, height * 0.38f, height * 0.16f, height * 0.20f, 12, clayColor); + DrawCylinder({ pos.x, pos.y + height * 0.88f, pos.z }, height * 0.16f, height * 0.18f, height * 0.20f, 12, clayColor); + DrawCircle3D({ pos.x, pos.y + height, pos.z }, height * 0.20f, { 1.0f, 0.0f, 0.0f }, 90.0f, clayColor); + + // Twin loop handles + DrawCube({ pos.x - height * 0.26f, pos.y + height * 0.62f, pos.z }, height * 0.06f, height * 0.28f, height * 0.06f, clayColor); + DrawCube({ pos.x + height * 0.26f, pos.y + height * 0.62f, pos.z }, height * 0.06f, height * 0.28f, height * 0.06f, clayColor); +} + +void drawAncientCrate(Texture2D woodTex, Vector3 pos, Vector3 size) { + drawTexturedBox(woodTex, pos, size, 1.0f, 1.0f); + DrawCubeWires(pos, size.x + 0.015f, size.y + 0.015f, size.z + 0.015f, Color{ 145, 105, 55, 255 }); +} + +void drawMarbleColumn(Vector3 pos, float height, float radius) { + DrawCube({ pos.x, pos.y + 0.12f, pos.z }, radius * 2.4f, 0.24f, radius * 2.4f, Color{ 215, 210, 200, 255 }); + DrawCylinder({ pos.x, pos.y + height * 0.5f, pos.z }, radius, radius * 0.95f, height - 0.48f, 16, Color{ 232, 226, 216, 255 }); + DrawCylinderWires({ pos.x, pos.y + height * 0.5f, pos.z }, radius + 0.01f, radius * 0.95f + 0.01f, height - 0.48f, 16, Color{ 185, 180, 170, 255 }); + DrawCube({ pos.x, pos.y + height - 0.12f, pos.z }, radius * 2.4f, 0.24f, radius * 2.4f, Color{ 215, 210, 200, 255 }); +} + +void drawMarbleNectarFountain(Vector3 pos, bool recharged, float animTime) { + // Stepped octagonal marble dais + DrawCylinder({ pos.x, pos.y + 0.08f, pos.z }, 1.6f, 1.7f, 0.16f, 16, Color{ 210, 205, 195, 255 }); + DrawCylinder({ pos.x, pos.y + 0.24f, pos.z }, 1.3f, 1.4f, 0.16f, 16, Color{ 225, 220, 210, 255 }); + + // Basin pedestal and upper marble bowl + DrawCylinder({ pos.x, pos.y + 0.65f, pos.z }, 1.0f, 0.45f, 0.65f, 16, Color{ 240, 235, 225, 255 }); + DrawCylinderWires({ pos.x, pos.y + 0.65f, pos.z }, 1.02f, 0.47f, 0.66f, 16, Color{ 185, 155, 95, 255 }); + + // Shimmering restorative nectar pool + float pulse = 0.5f + 0.5f * std::sin(animTime * 3.5f); + Color nectarCol = recharged ? Color{ 80, 230, 160, 235 } : + Color{ 255, 195, static_cast(60 + pulse * 45), 235 }; + DrawCylinder({ pos.x, pos.y + 0.88f, pos.z }, 0.88f, 0.88f, 0.12f, 16, nectarCol); + + // Spouting urn in center + DrawCylinder({ pos.x, pos.y + 1.25f, pos.z }, 0.22f, 0.14f, 0.55f, 12, Color{ 185, 110, 70, 255 }); + DrawCircle3D({ pos.x, pos.y + 1.05f + pulse * 0.12f, pos.z }, 0.55f, { 1.0f, 0.0f, 0.0f }, 90.0f, nectarCol); + DrawSphere({ pos.x, pos.y + 1.55f, pos.z }, 0.12f + pulse * 0.04f, WHITE); +} + +void drawScribeStele(Vector3 pos) { + // Stele base plinth + DrawCube({ pos.x, pos.y + 0.15f, pos.z }, 0.90f, 0.30f, 0.70f, Color{ 195, 185, 170, 255 }); + // Inscribed marble monolith + DrawCube({ pos.x, pos.y + 1.15f, pos.z }, 0.60f, 1.70f, 0.30f, Color{ 228, 222, 210, 255 }); + DrawCubeWires({ pos.x, pos.y + 1.15f, pos.z }, 0.62f, 1.72f, 0.32f, Color{ 170, 135, 75, 255 }); + + // Engraved glowing inscription lines on East face (facing room center) + for (float ly = 0.55f; ly <= 1.75f; ly += 0.22f) { + DrawLine3D({ pos.x + 0.31f, pos.y + ly, pos.z - 0.22f }, + { pos.x + 0.31f, pos.y + ly, pos.z + 0.22f }, + Color{ 240, 195, 60, 220 }); + } + + // Bronze reading lectern + DrawCube({ pos.x + 0.26f, pos.y + 0.88f, pos.z }, 0.26f, 0.06f, 0.55f, Color{ 145, 105, 45, 255 }); + // Wax tablet ledger + DrawCube({ pos.x + 0.28f, pos.y + 0.93f, pos.z }, 0.18f, 0.02f, 0.38f, Color{ 210, 165, 95, 255 }); +} + +void drawCraftsmanWorkbench(Texture2D woodTex, Vector3 pos) { + // Heavy timber workbench top + drawTexturedBox(woodTex, { pos.x, pos.y + 0.75f, pos.z }, { 1.0f, 0.14f, 2.6f }); + // 4 Heavy timber legs + DrawCube({ pos.x - 0.35f, 0.37f, pos.z - 1.05f }, 0.14f, 0.74f, 0.14f, Color{ 65, 42, 22, 255 }); + DrawCube({ pos.x + 0.35f, 0.37f, pos.z - 1.05f }, 0.14f, 0.74f, 0.14f, Color{ 65, 42, 22, 255 }); + DrawCube({ pos.x - 0.35f, 0.37f, pos.z + 1.05f }, 0.14f, 0.74f, 0.14f, Color{ 65, 42, 22, 255 }); + DrawCube({ pos.x + 0.35f, 0.37f, pos.z + 1.05f }, 0.14f, 0.74f, 0.14f, Color{ 65, 42, 22, 255 }); + + // Bronze gunsmith anvil + DrawCube({ pos.x - 0.15f, 0.92f, pos.z - 0.60f }, 0.32f, 0.20f, 0.48f, Color{ 135, 95, 45, 255 }); + DrawCylinder({ pos.x - 0.15f, 0.96f, pos.z - 0.90f }, 0.02f, 0.12f, 0.24f, 8, Color{ 165, 120, 60, 255 }); + + // Cartridge vise and bullet mold + DrawCube({ pos.x - 0.12f, 0.88f, pos.z + 0.20f }, 0.22f, 0.12f, 0.36f, Color{ 65, 70, 80, 255 }); + DrawCube({ pos.x - 0.14f, 0.96f, pos.z + 0.20f }, 0.04f, 0.08f, 0.20f, Color{ 180, 140, 60, 255 }); + + // Terracotta crucible with glowing bronze / embers + DrawCylinder({ pos.x - 0.08f, 0.88f, pos.z + 0.75f }, 0.16f, 0.12f, 0.16f, 10, Color{ 180, 85, 45, 255 }); + DrawSphere({ pos.x - 0.08f, 0.94f, pos.z + 0.75f }, 0.11f, Color{ 255, 160, 45, 255 }); + + // Ancient clay oil lamp with flame + DrawCylinder({ pos.x - 0.10f, 0.86f, pos.z - 0.05f }, 0.12f, 0.08f, 0.08f, 8, Color{ 190, 100, 55, 255 }); + DrawSphere({ pos.x - 0.10f, 0.93f, pos.z - 0.05f }, 0.05f, Color{ 255, 225, 110, 255 }); +} + } // anonymous namespace CyberRenderer::CyberRenderer() = default; @@ -933,6 +1203,11 @@ void CyberRenderer::generateTextures() { m_texCityNight = createCityNightTexture(); m_texCyberGate = createCyberGateTexture(); + m_texAncientWall = createAncientStoneWallTexture(); + m_texAncientFloor = createAncientFloorTexture(); + m_texAncientCeil = createAncientCeilTexture(); + m_texAncientWood = createAncientWoodTexture(); + m_texturesLoaded = true; } @@ -956,9 +1231,15 @@ void CyberRenderer::unloadTextures() { UnloadTexture(m_texCityNight); UnloadTexture(m_texCyberGate); + UnloadTexture(m_texAncientWall); + UnloadTexture(m_texAncientFloor); + UnloadTexture(m_texAncientCeil); + UnloadTexture(m_texAncientWood); + m_texturesLoaded = false; } + void CyberRenderer::beginPostProcess() { if (m_postProcessLoaded) { BeginTextureMode(m_renderTarget); @@ -1575,103 +1856,110 @@ void CyberRenderer::renderRestRoom(const RestRoom& restRoom, const Player& playe const float minCoord = 0.6f; const float maxCoord = 11.4f; - // 1. High-Tech Floor & Ceiling Textures - drawTexturedQuad(m_texDungeonFloor, + // 1. Ancient Terracotta & Marble Floor + drawTexturedQuad(m_texAncientFloor, { minCoord, -0.01f, minCoord }, { minCoord, -0.01f, maxCoord }, { maxCoord, -0.01f, maxCoord }, { maxCoord, -0.01f, minCoord }, 5.0f, 5.0f); - drawTexturedQuad(m_texDungeonCeil, + // 2. Ancient Cedar Timber Ceiling Beams + drawTexturedQuad(m_texAncientCeil, { maxCoord, roomHeight, minCoord }, { maxCoord, roomHeight, maxCoord }, { minCoord, roomHeight, maxCoord }, { minCoord, roomHeight, minCoord }, 5.0f, 5.0f); - // Neon floor grid rings around central sanctuary - for (float r = 1.0f; r <= 4.0f; r += 1.0f) { - DrawCircle3D({ 6.0f, 0.01f, 6.0f }, r, { 1.0f, 0.0f, 0.0f }, 90.0f, Color{ 0, 180, 240, 60 }); - } - - // 2. High-Tech Cyber Walls + // 3. Ancient Ashlar Stone Walls // South Wall (Z = maxCoord) - drawTexturedBox(m_texDungeonWall, { 6.0f, roomHeight * 0.5f, maxCoord }, { 11.0f, roomHeight, 0.3f }, 5.5f, 1.0f); + drawTexturedBox(m_texAncientWall, { 6.0f, roomHeight * 0.5f, maxCoord }, { 11.0f, roomHeight, 0.3f }, 5.5f, 1.0f); // East Wall (X = maxCoord) - drawTexturedBox(m_texDungeonWall, { maxCoord, roomHeight * 0.5f, 6.0f }, { 0.3f, roomHeight, 11.0f }, 5.5f, 1.0f); + drawTexturedBox(m_texAncientWall, { maxCoord, roomHeight * 0.5f, 6.0f }, { 0.3f, roomHeight, 11.0f }, 5.5f, 1.0f); // West Wall (X = minCoord) - drawTexturedBox(m_texDungeonWall, { minCoord, roomHeight * 0.5f, 6.0f }, { 0.3f, roomHeight, 11.0f }, 5.5f, 1.0f); + drawTexturedBox(m_texAncientWall, { minCoord, roomHeight * 0.5f, 6.0f }, { 0.3f, roomHeight, 11.0f }, 5.5f, 1.0f); - // North Wall: Split around Gateway (X = 6.0f, width 2.6f) - drawTexturedBox(m_texDungeonWall, { 2.7f, roomHeight * 0.5f, minCoord }, { 4.2f, roomHeight, 0.3f }, 2.0f, 1.0f); - drawTexturedBox(m_texDungeonWall, { 9.3f, roomHeight * 0.5f, minCoord }, { 4.2f, roomHeight, 0.3f }, 2.0f, 1.0f); - drawTexturedBox(m_texDungeonWall, { 6.0f, roomHeight - 0.3f, minCoord }, { 2.6f, 0.6f, 0.3f }, 1.3f, 0.3f); + // North Wall: Split around Monumental Palace Portal (X = 6.0f, width 2.6f) + drawTexturedBox(m_texAncientWall, { 2.7f, roomHeight * 0.5f, minCoord }, { 4.2f, roomHeight, 0.3f }, 2.0f, 1.0f); + drawTexturedBox(m_texAncientWall, { 9.3f, roomHeight * 0.5f, minCoord }, { 4.2f, roomHeight, 0.3f }, 2.0f, 1.0f); + drawTexturedBox(m_texAncientWall, { 6.0f, roomHeight - 0.3f, minCoord }, { 2.6f, 0.6f, 0.3f }, 1.3f, 0.3f); - // Corner Energy Pillars - Vector3 pillars[4] = { - { minCoord + 0.5f, roomHeight * 0.5f, minCoord + 0.5f }, - { maxCoord - 0.5f, roomHeight * 0.5f, minCoord + 0.5f }, - { minCoord + 0.5f, roomHeight * 0.5f, maxCoord - 0.5f }, - { maxCoord - 0.5f, roomHeight * 0.5f, maxCoord - 0.5f } + // 4. Classical Fluted Marble Columns in 4 corners + drawMarbleColumn({ minCoord + 0.5f, 0.0f, minCoord + 0.5f }, roomHeight, 0.24f); + drawMarbleColumn({ maxCoord - 0.5f, 0.0f, minCoord + 0.5f }, roomHeight, 0.24f); + drawMarbleColumn({ minCoord + 0.5f, 0.0f, maxCoord - 0.5f }, roomHeight, 0.24f); + drawMarbleColumn({ maxCoord - 0.5f, 0.0f, maxCoord - 0.5f }, roomHeight, 0.24f); + + // 5. Wall Torches / Sconces with flickering warm flame + auto drawTorchSconce = [&](Vector3 wallPos, Vector3 outDir) { + // Bronze mount bracket + DrawCube(wallPos, 0.08f, 0.28f, 0.08f, Color{ 145, 105, 45, 255 }); + Vector3 torchPos = { wallPos.x + outDir.x * 0.22f, wallPos.y + 0.10f, wallPos.z + outDir.z * 0.22f }; + DrawCylinder(torchPos, 0.04f, 0.05f, 0.28f, 8, Color{ 80, 50, 25, 255 }); + + float flick1 = std::sin(animTime * 14.0f + wallPos.x * 3.5f + wallPos.z * 4.2f) * 0.025f; + float flick2 = std::cos(animTime * 18.0f + wallPos.z * 2.8f) * 0.02f; + Vector3 flamePos = { torchPos.x, torchPos.y + 0.16f + flick1, torchPos.z }; + DrawSphere(flamePos, 0.11f + flick2, Color{ 255, 160, 45, 240 }); + DrawSphere({ flamePos.x, flamePos.y + 0.04f, flamePos.z }, 0.06f, Color{ 255, 230, 110, 255 }); }; - for (int i = 0; i < 4; ++i) { - drawTexturedBox(m_texDungeonWall, pillars[i], { 0.7f, roomHeight, 0.7f }, 1.0f, 1.0f); - DrawCubeWires(pillars[i], 0.72f, roomHeight, 0.72f, CyberColors::NeonCyan); - } - // 3. Central Nanite Recharge Matrix / Pod at (6.0f, 0.0f, 6.0f) - Vector3 podCenter = { 6.0f, 0.12f, 6.0f }; - DrawCylinder(podCenter, 1.3f, 1.4f, 0.24f, 8, Color{ 22, 32, 50, 255 }); - DrawCylinderWires(podCenter, 1.32f, 1.42f, 0.24f, 8, CyberColors::NeonCyan); + // Torch sconces on walls + drawTorchSconce({ 3.2f, 1.7f, minCoord + 0.16f }, { 0.0f, 0.0f, 1.0f }); + drawTorchSconce({ 8.8f, 1.7f, minCoord + 0.16f }, { 0.0f, 0.0f, 1.0f }); + drawTorchSconce({ 3.5f, 1.7f, maxCoord - 0.16f }, { 0.0f, 0.0f, -1.0f }); + drawTorchSconce({ 8.5f, 1.7f, maxCoord - 0.16f }, { 0.0f, 0.0f, -1.0f }); + drawTorchSconce({ minCoord + 0.16f, 1.7f, 4.0f }, { 1.0f, 0.0f, 0.0f }); + drawTorchSconce({ minCoord + 0.16f, 1.7f, 8.0f }, { 1.0f, 0.0f, 0.0f }); + drawTorchSconce({ maxCoord - 0.16f, 1.7f, 4.0f }, { -1.0f, 0.0f, 0.0f }); + drawTorchSconce({ maxCoord - 0.16f, 1.7f, 8.0f }, { -1.0f, 0.0f, 0.0f }); - float pulse = 0.5f + 0.5f * std::sin(animTime * 3.5f); - Color auraColor = restRoom.isRecharged() ? Color{ 0, 255, 140, 140 } : - Color{ 0, static_cast(180 + pulse * 75), 255, static_cast(140 + pulse * 90) }; + // 6. Monumental Palace Portal to Next Floor (North: X = 6.0f, Z = minCoord) + drawMarbleColumn({ 4.7f, 0.0f, minCoord + 0.16f }, roomHeight - 0.2f, 0.22f); + drawMarbleColumn({ 7.3f, 0.0f, minCoord + 0.16f }, roomHeight - 0.2f, 0.22f); + // Classical marble lintel archway + DrawCube({ 6.0f, roomHeight - 0.40f, minCoord + 0.16f }, 3.0f, 0.32f, 0.38f, Color{ 220, 215, 205, 255 }); + DrawCubeWires({ 6.0f, roomHeight - 0.40f, minCoord + 0.16f }, 3.02f, 0.34f, 0.40f, Color{ 175, 140, 80, 255 }); - // Fluid Nanite Column - Vector3 fluidCol = { 6.0f, 0.85f, 6.0f }; - DrawCylinder(fluidCol, 0.55f, 0.55f, 1.2f, 16, auraColor); - DrawCylinderWires(fluidCol, 0.58f, 0.58f, 1.22f, 8, CyberColors::NeonGreen); + // Dark descending stairway passage into catacombs + DrawCube({ 6.0f, (roomHeight - 0.6f) * 0.5f, minCoord + 0.06f }, 2.0f, roomHeight - 0.6f, 0.10f, Color{ 18, 14, 22, 255 }); + float portalPulse = 0.5f + 0.5f * std::sin(animTime * 3.2f); + DrawCube({ 6.0f, (roomHeight - 0.6f) * 0.5f, minCoord + 0.08f }, 1.5f, roomHeight - 0.8f, 0.04f, + Color{ 230, 190, 80, static_cast(110 + portalPulse * 70) }); - // Rising Levitation Rings - DrawCircle3D({ 6.0f, 0.55f + pulse * 0.10f, 6.0f }, 0.85f, { 1.0f, 0.0f, 0.0f }, 90.0f, auraColor); - DrawCircle3D({ 6.0f, 1.05f + pulse * 0.15f, 6.0f }, 0.65f, { 1.0f, 0.0f, 0.0f }, 90.0f, CyberColors::NeonCyan); - DrawSphere({ 6.0f, 0.85f + pulse * 0.10f, 6.0f }, 0.25f, restRoom.isRecharged() ? CyberColors::NeonGreen : WHITE); + // 7. Corner Storeroom Clusters (Terracotta Amphorae & Weathered Crates) + // Northwest Cluster at tile (1, 1) -> (2.0f, 0.0f, 2.0f) + drawAncientCrate(m_texAncientWood, { 2.1f, 0.45f, 2.1f }, { 0.90f, 0.90f, 0.90f }); + drawAncientCrate(m_texAncientWood, { 2.1f, 1.15f, 2.1f }, { 0.65f, 0.50f, 0.65f }); + drawAncientAmphora({ 1.5f, 0.0f, 1.6f }, 1.15f, Color{ 188, 102, 58, 255 }); + drawAncientAmphora({ 2.5f, 0.0f, 1.5f }, 0.95f, Color{ 168, 88, 48, 255 }); + drawAncientAmphora({ 1.6f, 0.0f, 2.4f }, 1.05f, Color{ 198, 115, 68, 255 }); - // Diagnostic console on South face of nanite pod (facing player at ny = 4) - Vector3 diagPos = { 6.0f, 0.45f, 7.35f }; - DrawCube(diagPos, 0.6f, 0.5f, 0.2f, Color{ 16, 24, 38, 255 }); - DrawCubeWires(diagPos, 0.62f, 0.52f, 0.22f, CyberColors::NeonCyan); - DrawCube({ diagPos.x, diagPos.y + 0.1f, diagPos.z + 0.08f }, 0.45f, 0.25f, 0.04f, restRoom.isRecharged() ? Color{ 0, 255, 120, 200 } : Color{ 0, 200, 255, 200 }); + // Northeast Cluster at tile (5, 1) -> (10.0f, 0.0f, 2.0f) + drawAncientCrate(m_texAncientWood, { 9.9f, 0.50f, 1.8f }, { 1.00f, 1.00f, 1.00f }); + drawAncientCrate(m_texAncientWood, { 10.4f, 0.40f, 2.4f }, { 0.80f, 0.80f, 0.80f }); + drawAncientAmphora({ 9.4f, 0.0f, 2.3f }, 1.20f, Color{ 180, 95, 52, 255 }); + drawAncientAmphora({ 10.4f, 0.0f, 1.4f }, 0.90f, Color{ 205, 120, 72, 255 }); - // 4. Save Archive Terminal at West Wall (nx = 1, ny = 3 => X = 1.0f, Z = 6.0f) - Vector3 termPos = { 1.05f, 0.65f, 6.0f }; - DrawCube(termPos, 0.8f, 1.3f, 1.4f, Color{ 20, 28, 44, 255 }); - DrawCubeWires(termPos, 0.82f, 1.32f, 1.42f, CyberColors::NeonCyan); + // Additional storeroom props along south walls + drawAncientAmphora({ 1.4f, 0.0f, 10.2f }, 1.10f, Color{ 175, 92, 50, 255 }); + drawAncientAmphora({ 2.2f, 0.0f, 10.5f }, 0.95f, Color{ 192, 108, 62, 255 }); + drawAncientCrate(m_texAncientWood, { 10.1f, 0.40f, 10.2f }, { 0.80f, 0.80f, 0.80f }); + drawAncientAmphora({ 9.4f, 0.0f, 10.4f }, 1.15f, Color{ 185, 100, 56, 255 }); - // Angled telemetry monitor - Vector3 monPos = { 1.15f, 1.15f, 6.0f }; - drawTexturedQuad(m_texMonitorRadar, - { monPos.x + 0.30f, monPos.y + 0.35f, monPos.z + 0.45f }, - { monPos.x + 0.30f, monPos.y - 0.25f, monPos.z + 0.45f }, - { monPos.x + 0.30f, monPos.y - 0.25f, monPos.z - 0.45f }, - { monPos.x + 0.30f, monPos.y + 0.35f, monPos.z - 0.45f }, - 1.0f, 1.0f); + // 8. Stations: + // West: Scribe's Inscribed Stele / Archive at tile (1, 3) -> (2.0f, 0.0f, 6.0f) + drawScribeStele({ 2.0f, 0.0f, 6.0f }); - // 5. Gateway Portal to Next Layer (North: X = 6.0f, Z = minCoord) - Vector3 gateFrame = { 6.0f, roomHeight * 0.5f, minCoord + 0.08f }; - drawTexturedBox(m_texCyberGate, gateFrame, { 2.6f, roomHeight, 0.30f }, 1.0f, 1.0f); + // Center: Marble Nectar Fountain at tile (3, 3) -> (6.0f, 0.0f, 6.0f) + drawMarbleNectarFountain({ 6.0f, 0.0f, 6.0f }, restRoom.isRecharged(), animTime); - float gatePulse = 0.5f + 0.5f * std::sin(animTime * 4.0f); - Vector3 gateField = { 6.0f, (roomHeight - 0.4f) * 0.5f, minCoord + 0.10f }; - DrawCube(gateField, 1.8f, roomHeight - 0.4f, 0.08f, Color{ 0, 240, 255, 150 }); - DrawCubeWires(gateField, 1.82f, roomHeight - 0.38f, 0.10f, - Color{ 0, 255, 255, static_cast(180 + gatePulse * 75) }); - - Vector3 gateSign = { 6.0f, roomHeight - 0.20f, minCoord + 0.26f }; - DrawCube(gateSign, 1.8f, 0.22f, 0.06f, Color{ 10, 16, 26, 255 }); - DrawCubeWires(gateSign, 1.8f, 0.22f, 0.06f, CyberColors::NeonYellow); + // East: Craftsman's Workbench at tile (5, 3) -> (10.0f, 0.0f, 6.0f) + drawCraftsmanWorkbench(m_texAncientWood, { 10.0f, 0.0f, 6.0f }); + // Water quenching urn & parts basket beside workbench + drawAncientAmphora({ 10.0f, 0.0f, 7.6f }, 0.85f, Color{ 140, 110, 85, 255 }); + drawAncientCrate(m_texAncientWood, { 10.0f, 0.30f, 4.4f }, { 0.65f, 0.60f, 0.65f }); EndMode3D(); diff --git a/src/render/CyberRenderer.hpp b/src/render/CyberRenderer.hpp index 5f2d05e..398f6bd 100644 --- a/src/render/CyberRenderer.hpp +++ b/src/render/CyberRenderer.hpp @@ -64,6 +64,12 @@ private: Texture2D m_texBedQuilt{}; Texture2D m_texCityNight{}; Texture2D m_texCyberGate{}; + + // Ancient Palace Storeroom Procedural Textures + Texture2D m_texAncientWall{}; + Texture2D m_texAncientFloor{}; + Texture2D m_texAncientCeil{}; + Texture2D m_texAncientWood{}; }; } // namespace dnr diff --git a/src/render/UIRenderer.cpp b/src/render/UIRenderer.cpp index 1e68fa1..eb7eb9d 100644 --- a/src/render/UIRenderer.cpp +++ b/src/render/UIRenderer.cpp @@ -850,41 +850,222 @@ void UIRenderer::drawHubHUD(const Player& player, const std::string& prompt) { void UIRenderer::drawRestRoomHUD(int nextFloor, const Player& player, const std::string& prompt) { int hudX = 20; int hudY = 20; - DrawRectangle(hudX, hudY, 340, 120, Color{ 8, 16, 30, 220 }); - DrawRectangleLines(hudX, hudY, 340, 120, CyberColors::NeonCyan); + DrawRectangle(hudX, hudY, 350, 142, Color{ 14, 18, 28, 230 }); + DrawRectangleLines(hudX, hudY, 350, 142, Color{ 210, 170, 70, 255 }); - DrawText("CYBERSANCTUARY // REST NODE", hudX + 15, hudY + 10, 16, CyberColors::NeonCyan); - DrawText(("NEXT TARGET: CYBERSPACE LAYER 0" + std::to_string(nextFloor)).c_str(), hudX + 15, hudY + 30, 13, CyberColors::NeonYellow); + DrawText("PALACE STOREROOM // SAFE REFUGE", hudX + 15, hudY + 10, 15, Color{ 240, 210, 140, 255 }); + DrawText(("NEXT DEPTH: LAYER 0" + std::to_string(nextFloor)).c_str(), hudX + 15, hudY + 30, 13, CyberColors::NeonYellow); // HP Bar float hpRatio = static_cast(player.getHp()) / player.getMaxHp(); - drawProgressBar(hudX + 55, hudY + 52, 260, 16, hpRatio, CyberColors::NeonGreen, Color{ 40, 15, 15, 255 }); + drawProgressBar(hudX + 55, hudY + 52, 270, 16, hpRatio, CyberColors::NeonGreen, Color{ 40, 15, 15, 255 }); DrawText("HP", hudX + 15, hudY + 52, 13, CyberColors::NeonGreen); - DrawText((std::to_string(player.getHp()) + "/" + std::to_string(player.getMaxHp())).c_str(), hudX + 150, hudY + 53, 12, RAYWHITE); + DrawText((std::to_string(player.getHp()) + "/" + std::to_string(player.getMaxHp())).c_str(), hudX + 155, hudY + 53, 12, RAYWHITE); // Shield Bar float shRatio = static_cast(player.getShield()) / player.getMaxShield(); - drawProgressBar(hudX + 55, hudY + 74, 260, 16, shRatio, CyberColors::NeonCyan, Color{ 10, 25, 45, 255 }); + drawProgressBar(hudX + 55, hudY + 74, 270, 16, shRatio, CyberColors::NeonCyan, Color{ 10, 25, 45, 255 }); DrawText("SHLD", hudX + 15, hudY + 74, 13, CyberColors::NeonCyan); - DrawText((std::to_string(player.getShield()) + "/" + std::to_string(player.getMaxShield())).c_str(), hudX + 150, hudY + 75, 12, RAYWHITE); + DrawText((std::to_string(player.getShield()) + "/" + std::to_string(player.getMaxShield())).c_str(), hudX + 155, hudY + 75, 12, RAYWHITE); DrawText(("PICKS: " + std::to_string(player.getCyberPicks())).c_str(), hudX + 15, hudY + 98, 13, CyberColors::NeonYellow); - DrawText("STATUS: SAFE HAVEN", hudX + 150, hudY + 98, 13, CyberColors::NeonGreen); + DrawText(("CORE CHIPS: " + std::to_string(player.getTechScraps())).c_str(), hudX + 120, hudY + 98, 13, CyberColors::NeonCyan); + DrawText("STATUS: PALACE SANCTUARY", hudX + 15, hudY + 118, 12, CyberColors::NeonGreen); // Context Action Prompt if (!prompt.empty()) { int promptW = MeasureText(prompt.c_str(), 18) + 40; int promptX = (GetScreenWidth() - promptW) / 2; int promptY = GetScreenHeight() - 70; - DrawRectangle(promptX, promptY, promptW, 45, Color{ 10, 20, 36, 240 }); - DrawRectangleLines(promptX, promptY, promptW, 45, CyberColors::NeonGreen); - DrawText(prompt.c_str(), promptX + 20, promptY + 13, 18, CyberColors::NeonGreen); + DrawRectangle(promptX, promptY, promptW, 45, Color{ 14, 20, 32, 240 }); + DrawRectangleLines(promptX, promptY, promptW, 45, Color{ 220, 180, 80, 255 }); + DrawText(prompt.c_str(), promptX + 20, promptY + 13, 18, Color{ 255, 220, 100, 255 }); } DrawText("[W/S] WALK | [A/D] TURN 90 | [SPACE/E] INTERACT | [TAB] ARMORY | [ESC] PAUSE MENU", 20, GetScreenHeight() - 35, 13, Color{ 180, 190, 200, 220 }); } +Rectangle UIRenderer::getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH) { + int modalW = 1020; + int modalH = 600; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + + int col = ammoIndex % 3; + int row = ammoIndex / 3; + int cardW = 310; + int cardH = 220; + int startX = modalX + 30; + int startY = modalY + 68; + int gapX = 15; + int gapY = 16; + int cardX = startX + col * (cardW + gapX); + int cardY = startY + row * (cardH + gapY); + + return Rectangle{ static_cast(cardX + 12), static_cast(cardY + cardH - 42), static_cast(cardW - 24), 32.0f }; +} + +Rectangle UIRenderer::getWorkbenchCloseButtonRect(int screenW, int screenH) { + int modalW = 1020; + int modalH = 600; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + return Rectangle{ static_cast(modalX + modalW - 140), static_cast(modalY + modalH - 42), 115.0f, 32.0f }; +} + +void UIRenderer::drawWorkbenchModal(const Player& player) { + int screenW = GetScreenWidth(); + int screenH = GetScreenHeight(); + + DrawRectangle(0, 0, screenW, screenH, Color{ 4, 8, 16, 235 }); + + int modalW = 1020; + int modalH = 600; + int modalX = (screenW - modalW) / 2; + int modalY = (screenH - modalH) / 2; + + // Outer background and classical bronze border + DrawRectangle(modalX, modalY, modalW, modalH, Color{ 10, 16, 26, 255 }); + DrawRectangleLines(modalX, modalY, modalW, modalH, Color{ 200, 160, 60, 255 }); + + // Decorative corners + DrawRectangle(modalX - 2, modalY - 2, 8, 8, Color{ 220, 180, 80, 255 }); + DrawRectangle(modalX + modalW - 6, modalY - 2, 8, 8, Color{ 220, 180, 80, 255 }); + DrawRectangle(modalX - 2, modalY + modalH - 6, 8, 8, Color{ 220, 180, 80, 255 }); + DrawRectangle(modalX + modalW - 6, modalY + modalH - 6, 8, 8, Color{ 220, 180, 80, 255 }); + + // Header Banner + DrawRectangle(modalX, modalY, modalW, 52, Color{ 18, 24, 38, 255 }); + DrawRectangleLines(modalX, modalY, modalW, 52, Color{ 140, 110, 50, 255 }); + + DrawText("// CRAFTSMAN WORKBENCH // RUN CARTRIDGE FORGE & OVERCLOCK", modalX + 25, modalY + 16, 17, Color{ 255, 215, 80, 255 }); + + // Live Core Chips Balance badge in header + std::string scrapBadge = "[ ❖ CORE CHIPS: " + std::to_string(player.getTechScraps()) + " ]"; + int badgeW = MeasureText(scrapBadge.c_str(), 16); + DrawRectangle(modalX + modalW - badgeW - 40, modalY + 12, badgeW + 20, 28, Color{ 8, 28, 38, 255 }); + DrawRectangleLines(modalX + modalW - badgeW - 40, modalY + 12, badgeW + 20, 28, CyberColors::NeonCyan); + DrawText(scrapBadge.c_str(), modalX + modalW - badgeW - 30, modalY + 18, 16, CyberColors::NeonCyan); + + Vector2 mouse = GetMousePosition(); + + AmmoType ammos[6] = { + AmmoType::KINETIC, + AmmoType::EMP, + AmmoType::OVERHEAT, + AmmoType::CORROSIVE, + AmmoType::GLITCH, + AmmoType::NANITE + }; + + int startX = modalX + 30; + int startY = modalY + 68; + int cardW = 310; + int cardH = 220; + int gapX = 15; + int gapY = 16; + + for (int i = 0; i < 6; ++i) { + AmmoType ammo = ammos[i]; + const auto& info = getAmmoInfo(ammo); + int curTier = player.getAmmoTier(ammo); + const auto& curTierData = getAmmoTierData(ammo, curTier); + + int col = i % 3; + int row = i / 3; + int cardX = startX + col * (cardW + gapX); + int cardY = startY + row * (cardH + gapY); + + Rectangle btnRect = getWorkbenchUpgradeButtonRect(i, screenW, screenH); + bool isHoverBtn = CheckCollisionPointRec(mouse, btnRect); + + // Card container + DrawRectangle(cardX, cardY, cardW, cardH, Color{ 14, 20, 32, 255 }); + DrawRectangleLines(cardX, cardY, cardW, cardH, isHoverBtn ? CyberColors::NeonYellow : info.color); + + // Header inside card: Ammo icon & Name + DrawCircle(cardX + 22, cardY + 24, 10.0f, info.color); + DrawCircleLines(cardX + 22, cardY + 24, 10.0f, WHITE); + + std::string nameTxt = info.name + " [" + info.shortName + "]"; + DrawText(nameTxt.c_str(), cardX + 40, cardY + 15, 13, info.color); + + // Tier badge + DrawText(curTierData.tierBadge.c_str(), cardX + cardW - 75, cardY + 15, 13, Color{ 255, 215, 60, 255 }); + + // Tier Title (e.g. TIER I: BASE SPEC) + DrawText(curTierData.tierTitle.c_str(), cardX + 16, cardY + 44, 12, Color{ 220, 190, 120, 255 }); + + // Damage display + int effDmg = player.getEffectiveDamage(ammo); + std::string dmgStr = "EFFECTIVE DAMAGE: " + std::to_string(effDmg); + if (curTier < 3) { + int nextDmg = getAmmoTierData(ammo, curTier + 1).damage; + dmgStr += " -> " + std::to_string(nextDmg); + } + DrawText(dmgStr.c_str(), cardX + 16, cardY + 64, 12, RAYWHITE); + + // Current Tier Perk & Effects + DrawRectangle(cardX + 12, cardY + 84, cardW - 24, 84, Color{ 10, 14, 24, 255 }); + DrawRectangleLines(cardX + 12, cardY + 84, cardW - 24, 84, Color{ 35, 45, 65, 255 }); + + // Format perk text wrapping + DrawText("PERK:", cardX + 18, cardY + 90, 10, GRAY); + std::string perk = curTierData.perkDesc; + size_t mid1 = perk.find(". "); + if (mid1 != std::string::npos && mid1 < perk.length() - 2) { + std::string line1 = perk.substr(0, mid1 + 1); + std::string line2 = perk.substr(mid1 + 2); + DrawText(line1.c_str(), cardX + 18, cardY + 104, 11, CyberColors::NeonCyan); + DrawText(line2.c_str(), cardX + 18, cardY + 122, 11, RAYWHITE); + } else { + DrawText(perk.c_str(), cardX + 18, cardY + 106, 11, CyberColors::NeonCyan); + } + + // Bottom Action Button / Status + if (curTier >= 3) { + DrawRectangleRec(btnRect, Color{ 40, 34, 14, 255 }); + DrawRectangleLinesEx(btnRect, 1.0f, Color{ 220, 180, 50, 255 }); + DrawText("[ ★ MASTER SPEC REACHED ★ ]", cardX + 32, static_cast(btnRect.y + 9), 12, Color{ 255, 215, 60, 255 }); + } else { + int nextTier = curTier + 1; + const auto& nextData = getAmmoTierData(ammo, nextTier); + int cost = nextData.upgradeCost; + bool canAfford = player.canUpgradeAmmo(ammo); + + Color btnBg = canAfford ? (isHoverBtn ? Color{ 25, 65, 45, 255 } : Color{ 16, 42, 30, 255 }) + : Color{ 30, 18, 22, 255 }; + Color btnLine = canAfford ? (isHoverBtn ? CyberColors::NeonYellow : CyberColors::NeonGreen) + : Color{ 120, 50, 60, 255 }; + Color btnText = canAfford ? (isHoverBtn ? CyberColors::NeonYellow : CyberColors::NeonGreen) + : Color{ 190, 110, 120, 255 }; + + DrawRectangleRec(btnRect, btnBg); + DrawRectangleLinesEx(btnRect, isHoverBtn ? 2.0f : 1.0f, btnLine); + + std::string btnLabel = "[" + std::to_string(i + 1) + "] UPGRADE TO TIER " + (nextTier == 2 ? "II" : "III") + " (" + std::to_string(cost) + " ❖)"; + if (!canAfford) { + btnLabel = "[" + std::to_string(i + 1) + "] NEED " + std::to_string(cost) + " ❖ (LACK " + std::to_string(cost - player.getTechScraps()) + ")"; + } + + int labelW = MeasureText(btnLabel.c_str(), 12); + DrawText(btnLabel.c_str(), cardX + (cardW - labelW) / 2, static_cast(btnRect.y + 9), 12, btnText); + } + } + + // Footer Info & Close Button + DrawText("[1 - 6] OR CLICK BUTTON TO UPGRADE CARTRIDGE | [ESC / E] CLOSE WORKBENCH", + modalX + 30, modalY + modalH - 32, 13, RAYWHITE); + + Rectangle btnClose = getWorkbenchCloseButtonRect(screenW, screenH); + bool isHoverClose = CheckCollisionPointRec(mouse, btnClose); + DrawRectangleRec(btnClose, isHoverClose ? Color{ 45, 25, 30, 255 } : Color{ 24, 16, 20, 255 }); + DrawRectangleLinesEx(btnClose, isHoverClose ? 2.0f : 1.0f, isHoverClose ? RED : Color{ 180, 80, 90, 255 }); + DrawText("[ESC] CLOSE", static_cast(btnClose.x + 18), static_cast(btnClose.y + 9), 13, isHoverClose ? RED : RAYWHITE); +} + + void UIRenderer::drawPauseMenu(bool hasSavedMessage) { DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 225 }); diff --git a/src/render/UIRenderer.hpp b/src/render/UIRenderer.hpp index cbf10b1..129631b 100644 --- a/src/render/UIRenderer.hpp +++ b/src/render/UIRenderer.hpp @@ -15,6 +15,9 @@ public: void drawExplorationHUD(const Dungeon& dungeon, const Player& player, const std::string& prompt); void drawHubHUD(const Player& player, const std::string& prompt); void drawRestRoomHUD(int nextFloor, const Player& player, const std::string& prompt); + void drawWorkbenchModal(const Player& player); + static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH); + static Rectangle getWorkbenchCloseButtonRect(int screenW, int screenH); void drawHackingUI(const HackingGame& hacking); void drawCombatUI(const CombatSystem& combat, const Player& player, float animTime, bool showCodex = false); void drawCodexModal();