Overhaul Rest Room to ancient palace storeroom and add ammo upgrade workbench

This commit is contained in:
Игорь Федосеев 2026-10-05 13:59:49 +03:00
parent 5a76e602f1
commit 7b82325729
14 changed files with 894 additions and 111 deletions

View file

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

View file

@ -4,6 +4,7 @@
#include <vector>
#include <array>
#include <memory>
#include <algorithm>
#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,

View file

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

View file

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

View file

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

View file

@ -16,8 +16,10 @@ struct SaveData {
int shield = 40;
int maxShield = 40;
int picks = 5;
int techScraps = 0;
std::array<int, 6> chambers{};
std::array<int, 7> ammoCounts{};
std::array<int, 7> ammoTiers{ 1, 1, 1, 1, 1, 1, 1 };
};
class SaveSystem {

View file

@ -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<int>(type);
if (idx >= 0 && idx < static_cast<int>(m_ammoTiers.size())) {
return m_ammoTiers[idx];
}
return 1;
}
void Player::setAmmoTier(AmmoType type, int tier) {
int idx = static_cast<int>(type);
if (idx >= 0 && idx < static_cast<int>(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

View file

@ -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<int, 7>& getAmmoTiers() const { return m_ammoTiers; }
const std::vector<ActiveStatus>& 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<int, 7> m_ammoCounts{ 0 };
std::array<int, 7> m_ammoTiers{ 1, 1, 1, 1, 1, 1, 1 };
Revolver m_revolver;
std::vector<ActiveStatus> m_statuses;
};

View file

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

View file

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

View file

@ -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<int>(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<int>(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<unsigned char>(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<unsigned char>(180 + pulse * 75), 255, static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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();

View file

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

View file

@ -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<float>(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<float>(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<float>(cardX + 12), static_cast<float>(cardY + cardH - 42), static_cast<float>(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<float>(modalX + modalW - 140), static_cast<float>(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<int>(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<int>(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<int>(btnClose.x + 18), static_cast<int>(btnClose.y + 9), 13, isHoverClose ? RED : RAYWHITE);
}
void UIRenderer::drawPauseMenu(bool hasSavedMessage) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 225 });

View file

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