DoorNroll/src/core/Game.cpp
Игорь Федосеев 4cad719290 feat: Cyber-noir hub room lighting, pixel wave transitions, tactical UI & ammo overhaul
- Replace procedural city with Room_window.jpg panoramic backdrop
- Add volumetric violet godrays, floor ambient pools, and cyan monitor glow
- Fix bed occlusion bug: reorder renderHub pipeline with rlDrawRenderBatchActive()
- Darken hub materials (smoked walnut parquet, slate plaster, midnight quilt)
- Enable cyber-noir postprocessing shader with vignette in hub room
- Implement diagonal matrix pixel wave screen transitions (ScreenTransition.hpp)
- Add tactical ammo palette (KIN, EMP, THM, ACD, GLT, NAN) & 3D combat laser tracers
- Center [ 12:00 FIRING PIN ] indicator and add drawWrappedText word-wrapping in UI
- Maintain strict zero-rounding constraint across the entire codebase
2026-10-06 21:15:02 +03:00

1189 lines
44 KiB
C++

#include "Game.hpp"
#include "dungeon/DungeonGen.hpp"
#include "core/SoundManager.hpp"
#include <cstdlib>
#include <algorithm>
#include <cmath>
namespace dnr {
Game::Game() = default;
Game::~Game() {
m_renderer.cleanup();
SoundManager::get().cleanup();
}
void Game::init() {
SoundManager::get().init();
m_renderer.init();
m_state = GameState::MAIN_MENU;
SoundManager::get().playTrack(MusicTrack::MENU_HUB);
}
void Game::startNewRun() {
m_dungeon.setFloor(1);
m_player.reset(3, 4, Direction::NORTH);
m_player.resetImplants();
m_showImplantDraft = false;
m_showShrineUI = false;
m_activeShrine = nullptr;
m_isDefusingMine = false;
m_activeMine = nullptr;
m_hubRoom.init(m_player);
m_state = GameState::HUB;
SoundManager::get().playTrack(MusicTrack::MENU_HUB);
SoundManager::get().playPickup();
}
void Game::enterCyberspace() {
int floor = m_dungeon.getFloor();
auto res = DungeonGen::generate(m_dungeon, floor);
m_player.setGridPosition(res.startX, res.startY);
m_player.setFacing(Direction::NORTH);
m_dungeon.reveal(res.startX, res.startY, 4);
m_state = GameState::EXPLORATION;
SoundManager::get().playTrack(MusicTrack::EXPLORATION);
SoundManager::get().playDoorOpen();
}
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();
}
void Game::nextFloor() {
int nextF = m_dungeon.getFloor() + 1;
m_dungeon.setFloor(nextF);
enterCyberspace();
}
void Game::saveCurrentProgress() {
bool inHub = (m_state == GameState::HUB || m_previousState == GameState::HUB);
SaveSystem::saveGame("savegame.dat", m_dungeon.getFloor(), inHub, m_player);
m_saveFeedbackTimer = 2.0f;
SoundManager::get().playSkillSuccess();
}
bool Game::loadSavedProgress() {
SaveData data;
if (!SaveSystem::loadGame("savegame.dat", data)) return false;
m_dungeon.setFloor(data.floor);
m_player.setStats(data.hp, data.maxHp, data.shield, data.maxShield, data.picks);
// Restore cylinder chambers
auto& rev = m_player.getRevolver();
for (int i = 0; i < 6; ++i) {
rev.setChamber(i, static_cast<AmmoType>(data.chambers[i]));
}
// Restore stacked ammo counts
for (size_t i = 0; i < data.ammoCounts.size(); ++i) {
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]);
}
// Restore Neural Implants & Defibrillator state
std::vector<ImplantType> imps;
for (int val : data.implants) {
imps.push_back(static_cast<ImplantType>(val));
}
m_player.setImplants(imps);
m_player.setDefibrillatorUsed(data.defibrillatorUsed);
m_showImplantDraft = false;
m_showShrineUI = false;
m_activeShrine = nullptr;
m_isDefusingMine = false;
m_activeMine = nullptr;
if (data.inHub) {
m_hubRoom.init(m_player);
m_state = GameState::HUB;
SoundManager::get().playTrack(MusicTrack::MENU_HUB);
} else {
auto res = DungeonGen::generate(m_dungeon, data.floor);
m_player.reset(res.startX, res.startY, Direction::NORTH);
m_dungeon.reveal(res.startX, res.startY, 4);
m_state = GameState::EXPLORATION;
SoundManager::get().playTrack(MusicTrack::EXPLORATION);
}
SoundManager::get().playPickup();
return true;
}
void Game::update(float dt) {
m_animTime += dt;
if (m_saveFeedbackTimer > 0.0f) {
m_saveFeedbackTimer -= dt;
}
// Update screen transition wave
m_transition.update(dt);
if (m_transition.isBusy()) {
return;
}
// Update dynamic music streaming engine every frame
SoundManager::get().updateMusic(dt);
switch (m_state) {
case GameState::MAIN_MENU:
updateMainMenu(dt);
break;
case GameState::HUB:
updateHub(dt);
break;
case GameState::REST_ROOM:
updateRestRoom(dt);
break;
case GameState::EXPLORATION:
updateExploration(dt);
break;
case GameState::HACKING:
updateHacking(dt);
break;
case GameState::COMBAT:
updateCombat(dt);
break;
case GameState::INVENTORY:
updateInventory(dt);
break;
case GameState::PAUSE_MENU:
updatePauseMenu(dt);
break;
case GameState::SETTINGS:
updateSettings(dt);
break;
case GameState::VICTORY_LEVEL:
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
m_transition.start([this]() { enterRestRoom(); });
}
break;
case GameState::GAME_OVER:
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
m_transition.start([this]() { startNewRun(); });
}
break;
}
}
void Game::updateMainMenu(float dt) {
int cx = GetScreenWidth() / 2;
int cy = GetScreenHeight() / 2;
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
Rectangle btnContinue = { static_cast<float>(cx - 190), static_cast<float>(cy - 50), 380.0f, 48.0f };
Rectangle btnNewRun = { static_cast<float>(cx - 190), static_cast<float>(cy + 10), 380.0f, 48.0f };
Rectangle btnSettings = { static_cast<float>(cx - 190), static_cast<float>(cy + 70), 380.0f, 48.0f };
Rectangle btnExit = { static_cast<float>(cx - 190), static_cast<float>(cy + 130), 380.0f, 48.0f };
bool hasSave = SaveSystem::hasSaveFile();
if ((IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btnContinue))) && hasSave) {
m_transition.start([this]() { loadSavedProgress(); });
} else if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btnNewRun))) {
m_transition.start([this]() { startNewRun(); });
} else if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btnSettings))) {
m_previousState = GameState::MAIN_MENU;
m_state = GameState::SETTINGS;
SoundManager::get().playTumblerClick();
} else if (IsKeyPressed(KEY_FOUR) || (mouseClicked && CheckCollisionPointRec(mouse, btnExit))) {
m_shouldQuit = true;
}
}
void Game::updateHub(float dt) {
m_hubRoom.update(dt, m_player);
Vector2 dirVec = getDirectionOffset(m_player.getFacing());
// Check interaction context
m_contextPrompt = "";
if (m_hubRoom.isFacingPortal(m_player)) {
m_contextPrompt = "[SPACE / E] JACK IN // ENTER CYBERSPACE GATEWAY";
} else if (m_hubRoom.isFacingTerminal(m_player)) {
m_contextPrompt = "[SPACE / E] DESK TERMINAL // SAVE RUN PROGRESS";
}
if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) {
tryMoveHub(static_cast<int>(dirVec.x), static_cast<int>(dirVec.y));
}
if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) {
tryMoveHub(-static_cast<int>(dirVec.x), -static_cast<int>(dirVec.y));
}
if (IsKeyPressed(KEY_A) || IsKeyPressed(KEY_LEFT)) {
m_player.turnLeft();
}
if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) {
m_player.turnRight();
}
if (IsKeyPressed(KEY_Q)) {
Vector2 strafe = { dirVec.y, -dirVec.x };
tryMoveHub(static_cast<int>(strafe.x), static_cast<int>(strafe.y));
}
if (IsKeyPressed(KEY_E)) {
if (!m_contextPrompt.empty()) {
tryInteractHub();
} else {
Vector2 strafe = { -dirVec.y, dirVec.x };
tryMoveHub(static_cast<int>(strafe.x), static_cast<int>(strafe.y));
}
}
if (IsKeyPressed(KEY_SPACE) && !m_contextPrompt.empty()) {
tryInteractHub();
}
if (IsKeyPressed(KEY_TAB)) {
m_previousState = GameState::HUB;
m_state = GameState::INVENTORY;
SoundManager::get().playTumblerClick();
}
if (IsKeyPressed(KEY_ESCAPE)) {
m_previousState = GameState::HUB;
m_state = GameState::PAUSE_MENU;
SoundManager::get().playTumblerClick();
}
}
void Game::tryMoveHub(int dx, int dy) {
m_hubRoom.tryMove(dx, dy, m_player);
}
void Game::tryInteractHub() {
if (m_hubRoom.isFacingPortal(m_player)) {
m_transition.start([this]() { enterCyberspace(); });
} else if (m_hubRoom.isFacingTerminal(m_player)) {
saveCurrentProgress();
}
}
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());
// Check interaction context
m_contextPrompt = "";
if (m_restRoom.isFacingGateway(m_player)) {
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] NECTAR FOUNTAIN // DRINK (HP & SHIELD RESTORED)";
} else {
m_contextPrompt = "NECTAR FOUNTAIN EMPTIED // VITALITY OPTIMAL";
}
} else if (m_restRoom.isFacingTerminal(m_player)) {
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)) {
tryMoveRestRoom(static_cast<int>(dirVec.x), static_cast<int>(dirVec.y));
}
if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) {
tryMoveRestRoom(-static_cast<int>(dirVec.x), -static_cast<int>(dirVec.y));
}
if (IsKeyPressed(KEY_A) || IsKeyPressed(KEY_LEFT)) {
m_player.turnLeft();
}
if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) {
m_player.turnRight();
}
if (IsKeyPressed(KEY_Q)) {
Vector2 strafe = { dirVec.y, -dirVec.x };
tryMoveRestRoom(static_cast<int>(strafe.x), static_cast<int>(strafe.y));
}
if (IsKeyPressed(KEY_E)) {
if (!m_contextPrompt.empty()) {
tryInteractRestRoom();
} else {
Vector2 strafe = { -dirVec.y, dirVec.x };
tryMoveRestRoom(static_cast<int>(strafe.x), static_cast<int>(strafe.y));
}
}
if (IsKeyPressed(KEY_SPACE) && !m_contextPrompt.empty()) {
tryInteractRestRoom();
}
if (IsKeyPressed(KEY_TAB)) {
m_previousState = GameState::REST_ROOM;
m_state = GameState::INVENTORY;
SoundManager::get().playTumblerClick();
}
if (IsKeyPressed(KEY_ESCAPE)) {
m_previousState = GameState::REST_ROOM;
m_state = GameState::PAUSE_MENU;
SoundManager::get().playTumblerClick();
}
}
void Game::tryMoveRestRoom(int dx, int dy) {
m_restRoom.tryMove(dx, dy, m_player);
}
void Game::tryInteractRestRoom() {
if (m_restRoom.isFacingGateway(m_player)) {
m_transition.start([this]() { nextFloor(); });
} else if (m_restRoom.isFacingRechargeMatrix(m_player)) {
if (!m_restRoom.isRecharged()) {
m_restRoom.triggerRecharge(m_player);
SoundManager::get().playPickup();
}
} else if (m_restRoom.isFacingTerminal(m_player)) {
saveCurrentProgress();
} else if (m_restRoom.isFacingWorkbench(m_player)) {
m_showWorkbenchUI = true;
SoundManager::get().playTumblerClick();
}
}
void Game::triggerImplantDraft() {
std::vector<ImplantType> available;
for (int i = 0; i < 12; ++i) {
ImplantType t = static_cast<ImplantType>(i);
if (!m_player.hasImplant(t)) {
available.push_back(t);
}
}
if (available.empty()) {
m_player.addTechScraps(50);
return;
}
for (size_t i = available.size() - 1; i > 0; --i) {
size_t j = std::rand() % (i + 1);
std::swap(available[i], available[j]);
}
m_draftChoices.clear();
for (size_t i = 0; i < available.size() && i < 3; ++i) {
m_draftChoices.push_back(available[i]);
}
m_showImplantDraft = true;
m_draftReplaceSlot = -1;
SoundManager::get().playSkillGreat();
}
void Game::handleImplantDraftInput() {
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
if (m_player.getImplants().size() >= 4) {
int modalW = 960;
int modalH = 550;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
int mountY = modalY + 332;
int slotW = 212;
int slotH = 68;
int slotGap = 16;
int startSlotX = modalX + 30;
for (int s = 0; s < 4; ++s) {
int slX = startSlotX + s * (slotW + slotGap);
int slY = mountY + 20;
Rectangle slotRect = { static_cast<float>(slX), static_cast<float>(slY), static_cast<float>(slotW), static_cast<float>(slotH) };
if (mouseClicked && CheckCollisionPointRec(mouse, slotRect)) {
m_draftReplaceSlot = s;
SoundManager::get().playTumblerClick();
return;
}
}
}
for (size_t i = 0; i < m_draftChoices.size() && i < 3; ++i) {
Rectangle btn = UIRenderer::getDraftCardButtonRect(static_cast<int>(i), screenW, screenH);
bool keyPressed = (i == 0 && IsKeyPressed(KEY_ONE)) ||
(i == 1 && IsKeyPressed(KEY_TWO)) ||
(i == 2 && IsKeyPressed(KEY_THREE));
if (keyPressed || (mouseClicked && CheckCollisionPointRec(mouse, btn))) {
ImplantType chosen = m_draftChoices[i];
if (m_player.getImplants().size() < 4) {
m_player.addImplant(chosen);
m_showImplantDraft = false;
SoundManager::get().playPickup();
return;
} else {
if (m_draftReplaceSlot >= 0 && m_draftReplaceSlot < 4) {
m_player.replaceImplant(m_draftReplaceSlot, chosen);
m_showImplantDraft = false;
m_draftReplaceSlot = -1;
SoundManager::get().playPickup();
return;
} else {
SoundManager::get().playDamage();
}
}
}
}
Rectangle scrapBtn = UIRenderer::getDraftScrapButtonRect(screenW, screenH);
if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, scrapBtn))) {
m_player.addTechScraps(35);
m_showImplantDraft = false;
m_draftReplaceSlot = -1;
SoundManager::get().playTumblerClick();
}
}
void Game::handleShrineInput() {
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
Rectangle btn0 = UIRenderer::getShrineOptionButtonRect(0, screenW, screenH);
if (IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btn0))) {
if (m_player.getHp() > 25) {
m_player.takeDamage(25);
m_player.addTechScraps(60);
m_player.addAmmo(AmmoType::KINETIC, 1);
m_player.addAmmo(AmmoType::GLITCH, 1);
if (m_activeShrine) m_activeShrine->used = true;
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playSkillGreat();
return;
} else {
SoundManager::get().playDamage();
}
}
Rectangle btn1 = UIRenderer::getShrineOptionButtonRect(1, screenW, screenH);
if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btn1))) {
if (m_player.getCyberPicks() >= 1) {
m_player.usePick();
m_player.increaseMaxShield(15);
m_player.rechargeShield(m_player.getMaxShield());
if (m_activeShrine) m_activeShrine->used = true;
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playSkillGreat();
return;
} else {
SoundManager::get().playDamage();
}
}
Rectangle btn2 = UIRenderer::getShrineOptionButtonRect(2, screenW, screenH);
if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btn2))) {
if (m_player.getAmmoCount(AmmoType::KINETIC) >= 2) {
m_player.consumeAmmo(AmmoType::KINETIC);
m_player.consumeAmmo(AmmoType::KINETIC);
m_player.addAmmo(AmmoType::NANITE, 1);
m_player.addAmmo(AmmoType::GLITCH, 1);
if (m_activeShrine) m_activeShrine->used = true;
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playSkillGreat();
return;
} else {
SoundManager::get().playDamage();
}
}
Rectangle btnClose = UIRenderer::getShrineCloseButtonRect(screenW, screenH);
if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, btnClose))) {
m_showShrineUI = false;
m_activeShrine = nullptr;
SoundManager::get().playTumblerClick();
}
}
void Game::updateExploration(float dt) {
if (m_showImplantDraft) {
handleImplantDraftInput();
return;
}
if (m_showShrineUI) {
handleShrineInput();
return;
}
m_player.update(dt);
m_dungeon.reveal(m_player.getGridX(), m_player.getGridY(), 3);
// Update laser barrier timers
for (auto& laser : m_dungeon.getLasers()) {
if (laser.disarmed) continue;
laser.timer += dt;
if (laser.timer >= laser.period) {
laser.timer -= laser.period;
}
laser.active = (laser.timer < 2.0f);
}
Vector2 dirVec = getDirectionOffset(m_player.getFacing());
int frontX = m_player.getGridX() + static_cast<int>(dirVec.x);
int frontY = m_player.getGridY() + static_cast<int>(dirVec.y);
m_contextPrompt = "";
if (m_dungeon.isDoorLocked(frontX, frontY)) {
m_contextPrompt = "[SPACE / E] Bypass Door ICE [Picks: " + std::to_string(m_player.getCyberPicks()) + "]";
} else {
LaserBarrier* frontLaser = m_dungeon.getLaserAt(frontX, frontY);
ProximityMine* frontMine = m_dungeon.getMineAt(frontX, frontY);
CyberShrine* frontShrine = m_dungeon.getShrineAt(frontX, frontY);
if (!frontShrine) frontShrine = m_dungeon.getShrineAt(m_player.getGridX(), m_player.getGridY());
if (frontLaser && !frontLaser->disarmed) {
m_contextPrompt = "[SPACE / E] Disarm Laser Barrier (Cost: 1 ICE Pick)";
} else if (frontMine && frontMine->isArmed()) {
m_contextPrompt = "[SPACE / E] Defuse Proximity Mine (Quick DBD Timing Check)";
} else if (frontShrine && !frontShrine->used) {
m_contextPrompt = "[SPACE / E] Access Darknet Cyber Shrine";
} else if (m_dungeon.getTile(frontX, frontY) == TileType::LOOT_CACHE ||
m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::LOOT_CACHE) {
m_contextPrompt = "[SPACE / E] Extract Data Cache";
} else if (m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::EXIT_PORTAL ||
m_dungeon.getTile(frontX, frontY) == TileType::EXIT_PORTAL) {
m_contextPrompt = "[SPACE / E] Jack Out To Safe Haven Rest Node";
}
}
if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) {
tryMoveDungeon(static_cast<int>(dirVec.x), static_cast<int>(dirVec.y));
}
if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) {
tryMoveDungeon(-static_cast<int>(dirVec.x), -static_cast<int>(dirVec.y));
}
if (IsKeyPressed(KEY_A) || IsKeyPressed(KEY_LEFT)) {
m_player.turnLeft();
}
if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) {
m_player.turnRight();
}
if (IsKeyPressed(KEY_Q)) {
Vector2 strafe = { dirVec.y, -dirVec.x };
tryMoveDungeon(static_cast<int>(strafe.x), static_cast<int>(strafe.y));
}
if (IsKeyPressed(KEY_E)) {
if (!m_contextPrompt.empty()) {
tryInteractDungeon();
} else {
Vector2 strafe = { -dirVec.y, dirVec.x };
tryMoveDungeon(static_cast<int>(strafe.x), static_cast<int>(strafe.y));
}
}
if (IsKeyPressed(KEY_SPACE) && !m_contextPrompt.empty()) {
tryInteractDungeon();
}
if (IsKeyPressed(KEY_TAB)) {
m_previousState = GameState::EXPLORATION;
m_state = GameState::INVENTORY;
SoundManager::get().setInventoryDucked(true);
SoundManager::get().playTumblerClick();
}
if (IsKeyPressed(KEY_ESCAPE)) {
m_previousState = GameState::EXPLORATION;
m_state = GameState::PAUSE_MENU;
SoundManager::get().playTumblerClick();
}
// Step on enemy
MapEnemy* me = m_dungeon.getEnemyAt(m_player.getGridX(), m_player.getGridY());
if (me && me->alive) {
m_state = GameState::COMBAT;
m_showCombatCodex = false;
m_lastPlayerHp = m_player.getHp();
SoundManager::get().playTrack(MusicTrack::COMBAT);
auto enemyPtr = std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor());
if (me->enemy.isElite()) enemyPtr->setElite(true);
m_combat.startCombat(m_player, std::move(enemyPtr));
m_dungeon.removeEnemyAt(me->x, me->y);
}
}
void Game::tryMoveDungeon(int dx, int dy) {
int nx = m_player.getGridX() + dx;
int ny = m_player.getGridY() + dy;
if (m_dungeon.isWalkable(nx, ny)) {
m_player.setGridPosition(nx, ny);
MapEnemy* me = m_dungeon.getEnemyAt(nx, ny);
if (me && me->alive) {
m_state = GameState::COMBAT;
m_showCombatCodex = false;
m_lastPlayerHp = m_player.getHp();
SoundManager::get().playTrack(MusicTrack::COMBAT);
auto enemyPtr = std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor());
if (me->enemy.isElite()) enemyPtr->setElite(true);
m_combat.startCombat(m_player, std::move(enemyPtr));
m_dungeon.removeEnemyAt(nx, ny);
return;
}
// Check if stepped on laser barrier
LaserBarrier* lb = m_dungeon.getLaserAt(nx, ny);
if (lb && !lb->disarmed && lb->active) {
m_player.takeDamage(15, AmmoType::EMPTY);
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
SoundManager::get().playDamage();
if (m_player.isDead()) {
m_state = GameState::GAME_OVER;
return;
}
}
// Check if stepped on proximity mine
ProximityMine* pm = m_dungeon.getMineAt(nx, ny);
if (pm && pm->isArmed()) {
pm->triggered = true;
m_player.takeDamage(25, AmmoType::EMPTY);
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
SoundManager::get().playDamage();
if (m_player.isDead()) {
m_state = GameState::GAME_OVER;
return;
}
}
} else if (m_dungeon.isDoorLocked(nx, ny)) {
m_targetDoorX = nx;
m_targetDoorY = ny;
}
}
void Game::tryInteractDungeon() {
Vector2 dirVec = getDirectionOffset(m_player.getFacing());
int fx = m_player.getGridX() + static_cast<int>(dirVec.x);
int fy = m_player.getGridY() + static_cast<int>(dirVec.y);
int px = m_player.getGridX();
int py = m_player.getGridY();
if (m_dungeon.isDoorLocked(fx, fy)) {
if (m_player.getCyberPicks() > 0) {
m_targetDoorX = fx;
m_targetDoorY = fy;
DoorData* dd = m_dungeon.getDoorAt(fx, fy);
int sec = dd ? dd->securityLevel : 1;
bool expanded = m_player.hasImplant(ImplantType::LOGIC_OVERCLOCK);
m_hacking.start(sec, expanded);
m_state = GameState::HACKING;
SoundManager::get().playTumblerClick();
} else {
SoundManager::get().playDamage();
}
return;
}
LaserBarrier* lb = m_dungeon.getLaserAt(fx, fy);
if (lb && !lb->disarmed) {
if (m_player.getCyberPicks() > 0) {
m_player.usePick();
lb->disarmed = true;
SoundManager::get().playDoorOpen();
} else {
SoundManager::get().playDamage();
}
return;
}
ProximityMine* pm = m_dungeon.getMineAt(fx, fy);
if (pm && pm->isArmed()) {
m_activeMine = pm;
m_isDefusingMine = true;
bool expanded = m_player.hasImplant(ImplantType::LOGIC_OVERCLOCK);
m_hacking.startSkillCheckOnly(1, expanded);
m_state = GameState::HACKING;
SoundManager::get().playTumblerClick();
return;
}
CyberShrine* cs = m_dungeon.getShrineAt(fx, fy);
if (!cs) cs = m_dungeon.getShrineAt(px, py);
if (cs && !cs->used) {
m_activeShrine = cs;
m_showShrineUI = true;
SoundManager::get().playSkillGreat();
return;
}
LootData* ld = m_dungeon.getLootAt(fx, fy);
if (!ld) ld = m_dungeon.getLootAt(px, py);
if (ld) {
ld->looted = true;
m_dungeon.setTile(ld->x, ld->y, TileType::FLOOR);
for (auto a : ld->ammoRewards) {
m_player.addAmmo(a, 1);
}
m_player.addPicks(ld->picksReward);
m_player.addTechScraps(35 + (std::rand() % 25));
SoundManager::get().playPickup();
return;
}
if (m_dungeon.getTile(px, py) == TileType::EXIT_PORTAL ||
m_dungeon.getTile(fx, fy) == TileType::EXIT_PORTAL) {
m_transition.start([this]() { enterRestRoom(); });
return;
}
}
void Game::updateHacking(float dt) {
if (IsKeyPressed(KEY_ESCAPE)) {
m_previousState = GameState::HACKING;
m_state = GameState::PAUSE_MENU;
SoundManager::get().playTumblerClick();
return;
}
m_hacking.update(dt);
m_hacking.handleInput();
if (m_hacking.isFinished() && m_hacking.getResultTimer() > 1.2f) {
if (m_isDefusingMine) {
m_isDefusingMine = false;
if (m_hacking.isSuccess()) {
if (m_activeMine) {
m_activeMine->defused = true;
}
m_player.addTechScraps(20);
m_player.addAmmo(AmmoType::KINETIC, 1);
if (m_hacking.isCritical()) {
m_player.addTechScraps(15);
m_player.addAmmo(AmmoType::EMP, 1);
}
SoundManager::get().playPickup();
} else {
if (m_activeMine) {
m_activeMine->triggered = true;
}
m_player.takeDamage(25, AmmoType::EMPTY);
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
SoundManager::get().playDamage();
if (m_player.isDead()) {
m_activeMine = nullptr;
m_state = GameState::GAME_OVER;
return;
}
}
m_activeMine = nullptr;
m_state = GameState::EXPLORATION;
return;
}
if (m_hacking.isSuccess()) {
m_dungeon.unlockDoorAt(m_targetDoorX, m_targetDoorY);
SoundManager::get().playDoorOpen();
if (m_hacking.isCritical()) {
m_player.addAmmo(AmmoType::NANITE, 1);
m_player.addPicks(1);
m_player.addTechScraps(40);
if (m_hacking.getSecurityLevel() >= 3) {
triggerImplantDraft();
}
} else {
m_player.addTechScraps(15);
}
m_state = GameState::EXPLORATION;
} else if (m_hacking.isAlarmTriggered()) {
m_player.usePick();
m_state = GameState::COMBAT;
m_showCombatCodex = false;
m_lastPlayerHp = m_player.getHp();
SoundManager::get().playTrack(MusicTrack::COMBAT);
m_combat.startCombat(m_player, std::make_unique<Enemy>(EnemyType::ICE_SENTINEL, m_dungeon.getFloor()));
}
}
}
void Game::updateCombat(float dt) {
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
int screenW = GetScreenWidth();
int screenH = GetScreenHeight();
int modalW = 980;
int modalH = 580;
int modalX = (screenW - modalW) / 2;
int modalY = (screenH - modalH) / 2;
Rectangle btnCloseCodex = { static_cast<float>(modalX + modalW / 2 - 120), static_cast<float>(modalY + modalH - 52), 240.0f, 38.0f };
int logX = 770;
int pvY = 180 + 210 + 12;
Rectangle btnCodexToggle = { static_cast<float>(logX + 460 - 95), static_cast<float>(pvY + 25), 85.0f, 40.0f };
// Toggle Codex Guide Modal
if (m_showCombatCodex) {
if (IsKeyPressed(KEY_H) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, btnCloseCodex))) {
m_showCombatCodex = false;
SoundManager::get().playTumblerClick();
}
return;
}
if (IsKeyPressed(KEY_H) || (mouseClicked && CheckCollisionPointRec(mouse, btnCodexToggle))) {
m_showCombatCodex = true;
SoundManager::get().playTumblerClick();
return;
}
if (IsKeyPressed(KEY_ESCAPE) && m_combat.getPhase() == CombatPhase::PLAYER_TURN) {
m_previousState = GameState::COMBAT;
m_state = GameState::PAUSE_MENU;
SoundManager::get().playTumblerClick();
return;
}
m_combat.update(dt, m_player);
// Trigger glitch post-processing effect whenever player takes damage
int curHp = m_player.getHp();
if (curHp < m_lastPlayerHp) {
m_renderer.triggerGlitch(0.7f);
}
m_lastPlayerHp = curHp;
int barX = 50;
int barY = 548;
Rectangle btnShoot = { static_cast<float>(barX + 15), static_cast<float>(barY + 12), 370.0f, 96.0f };
Rectangle btnShield = { static_cast<float>(barX + 405), static_cast<float>(barY + 12), 370.0f, 96.0f };
Rectangle btnTriple = { static_cast<float>(barX + 795), static_cast<float>(barY + 12), 370.0f, 96.0f };
if (m_combat.getPhase() == CombatPhase::PLAYER_TURN) {
bool doShoot = IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1) ||
IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) ||
(mouseClicked && CheckCollisionPointRec(mouse, btnShoot));
bool doShield = IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2) ||
(mouseClicked && CheckCollisionPointRec(mouse, btnShield));
bool doTriple = IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3) ||
(mouseClicked && CheckCollisionPointRec(mouse, btnTriple));
if (doShoot) {
m_combat.actionSpinAndShoot(m_player);
} else if (doShield) {
m_combat.actionShield(m_player);
} else if (doTriple && m_combat.getTripleCooldown() == 0) {
m_combat.actionTripleShot(m_player);
}
} else if (m_combat.getPhase() == CombatPhase::VICTORY) {
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) || mouseClicked) {
bool wasElite = m_combat.wasElite();
m_player.heal(25);
m_player.addAmmo(AmmoType::KINETIC, 1);
m_player.addAmmo(AmmoType::EMP, 1);
m_state = GameState::EXPLORATION;
SoundManager::get().playTrack(MusicTrack::EXPLORATION, true);
if (wasElite) {
triggerImplantDraft();
}
}
} else if (m_combat.getPhase() == CombatPhase::DEFEAT) {
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) || mouseClicked) {
m_state = GameState::GAME_OVER;
}
}
}
void Game::updateInventory(float dt) {
m_player.getRevolver().update(dt);
m_inventoryUI.update(m_player);
if (IsKeyPressed(KEY_TAB) || IsKeyPressed(KEY_ESCAPE)) {
SoundManager::get().setInventoryDucked(false);
m_state = m_previousState;
SoundManager::get().playTumblerClick();
}
}
void Game::updatePauseMenu(float dt) {
int menuW = 480;
int menuH = 430;
int menuX = (GetScreenWidth() - menuW) / 2;
int menuY = (GetScreenHeight() - menuH) / 2;
int startY = menuY + 90;
Vector2 mouse = GetMousePosition();
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
Rectangle btnResume = { static_cast<float>(menuX + 30), static_cast<float>(startY), static_cast<float>(menuW - 60), 48.0f };
Rectangle btnSettings = { static_cast<float>(menuX + 30), static_cast<float>(startY + 58), static_cast<float>(menuW - 60), 48.0f };
Rectangle btnSave = { static_cast<float>(menuX + 30), static_cast<float>(startY + 58 * 2), static_cast<float>(menuW - 60), 48.0f };
Rectangle btnHub = { static_cast<float>(menuX + 30), static_cast<float>(startY + 58 * 3), static_cast<float>(menuW - 60), 48.0f };
Rectangle btnQuit = { static_cast<float>(menuX + 30), static_cast<float>(startY + 58 * 4), static_cast<float>(menuW - 60), 48.0f };
if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, btnResume))) {
m_state = m_previousState;
SoundManager::get().playTumblerClick();
} else if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btnSettings))) {
m_previousState = GameState::PAUSE_MENU;
m_state = GameState::SETTINGS;
SoundManager::get().playTumblerClick();
} else if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btnSave))) {
saveCurrentProgress();
} else if (IsKeyPressed(KEY_FOUR) || (mouseClicked && CheckCollisionPointRec(mouse, btnHub))) {
m_hubRoom.init(m_player);
m_state = GameState::HUB;
SoundManager::get().playTrack(MusicTrack::MENU_HUB);
SoundManager::get().playDoorOpen();
} else if (IsKeyPressed(KEY_FIVE) || (mouseClicked && CheckCollisionPointRec(mouse, btnQuit))) {
m_shouldQuit = true;
}
}
void Game::updateSettings(float dt) {
auto& sm = SoundManager::get();
float mVol = sm.getMasterVolume();
float muVol = sm.getMusicVolume();
float sVol = sm.getSfxVolume();
// Keyboard selection & adjustment
if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) {
m_selectedSlider = (m_selectedSlider + 2) % 3;
sm.playTumblerClick();
}
if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) {
m_selectedSlider = (m_selectedSlider + 1) % 3;
sm.playTumblerClick();
}
if (IsKeyPressed(KEY_A) || IsKeyPressed(KEY_LEFT)) {
if (m_selectedSlider == 0) sm.setMasterVolume(mVol - 0.05f);
else if (m_selectedSlider == 1) sm.setMusicVolume(muVol - 0.05f);
else if (m_selectedSlider == 2) sm.setSfxVolume(sVol - 0.05f);
sm.playTumblerClick();
}
if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) {
if (m_selectedSlider == 0) sm.setMasterVolume(mVol + 0.05f);
else if (m_selectedSlider == 1) sm.setMusicVolume(muVol + 0.05f);
else if (m_selectedSlider == 2) sm.setSfxVolume(sVol + 0.05f);
sm.playTumblerClick();
}
// Mouse Interaction
Vector2 mouse = GetMousePosition();
int menuW = 580;
int menuH = 430;
int menuX = (GetScreenWidth() - menuW) / 2;
int menuY = (GetScreenHeight() - menuH) / 2;
int startY = menuY + 85;
int sliderW = 340;
int sliderH = 26;
int sliderX = menuX + 180;
Rectangle btnBack = { static_cast<float>(menuX + (menuW - 240) / 2), static_cast<float>(menuY + menuH - 65), 240.0f, 44.0f };
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (CheckCollisionPointRec(mouse, btnBack)) {
m_state = m_previousState;
sm.playTumblerClick();
return;
}
for (int i = 0; i < 3; ++i) {
int itemY = startY + i * 78;
Rectangle sBar = { static_cast<float>(sliderX - 10), static_cast<float>(itemY - 6), static_cast<float>(sliderW + 20), static_cast<float>(sliderH + 12) };
if (CheckCollisionPointRec(mouse, sBar)) {
m_selectedSlider = i;
m_isDraggingSlider = true;
float ratio = std::clamp((mouse.x - sliderX) / static_cast<float>(sliderW), 0.0f, 1.0f);
if (i == 0) sm.setMasterVolume(ratio);
else if (i == 1) sm.setMusicVolume(ratio);
else if (i == 2) sm.setSfxVolume(ratio);
sm.playTumblerClick();
break;
}
}
}
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && m_isDraggingSlider) {
float ratio = std::clamp((mouse.x - sliderX) / static_cast<float>(sliderW), 0.0f, 1.0f);
if (m_selectedSlider == 0) sm.setMasterVolume(ratio);
else if (m_selectedSlider == 1) sm.setMusicVolume(ratio);
else if (m_selectedSlider == 2) sm.setSfxVolume(ratio);
}
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
m_isDraggingSlider = false;
}
if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_ENTER)) {
m_state = m_previousState;
sm.playTumblerClick();
}
}
void Game::draw() {
BeginDrawing();
ClearBackground(Color{ 4, 6, 12, 255 });
switch (m_state) {
case GameState::MAIN_MENU:
m_uiRenderer.drawMainMenu(SaveSystem::hasSaveFile());
break;
case GameState::HUB:
m_renderer.renderHub(m_hubRoom, m_player, m_animTime);
m_uiRenderer.drawHubHUD(m_player, m_contextPrompt);
break;
case GameState::REST_ROOM:
m_renderer.renderRestRoom(m_restRoom, m_player, m_animTime);
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;
case GameState::EXPLORATION:
m_renderer.render3D(m_dungeon, m_player, m_animTime);
m_uiRenderer.drawExplorationHUD(m_dungeon, m_player, m_contextPrompt);
if (m_showImplantDraft) {
m_uiRenderer.drawImplantDraftModal(m_draftChoices, m_player, m_draftReplaceSlot);
} else if (m_showShrineUI) {
m_uiRenderer.drawShrineModal(m_player);
}
m_uiRenderer.drawMatrixOverlay(m_animTime);
break;
case GameState::HACKING:
m_renderer.render3D(m_dungeon, m_player, m_animTime);
m_uiRenderer.drawHackingUI(m_hacking);
m_uiRenderer.drawMatrixOverlay(m_animTime);
break;
case GameState::COMBAT:
m_renderer.renderCombat3D(m_dungeon, m_player, m_combat, m_animTime);
m_uiRenderer.drawCombatUI(m_combat, m_player, m_animTime, m_showCombatCodex);
m_uiRenderer.drawMatrixOverlay(m_animTime);
break;
case GameState::INVENTORY:
if (m_previousState == GameState::HUB) {
m_renderer.renderHub(m_hubRoom, m_player, m_animTime);
} else if (m_previousState == GameState::REST_ROOM) {
m_renderer.renderRestRoom(m_restRoom, m_player, m_animTime);
} else {
m_renderer.render3D(m_dungeon, m_player, m_animTime);
}
m_inventoryUI.draw(m_player);
break;
case GameState::PAUSE_MENU:
if (m_previousState == GameState::HUB) {
m_renderer.renderHub(m_hubRoom, m_player, m_animTime);
m_uiRenderer.drawHubHUD(m_player, m_contextPrompt);
} else if (m_previousState == GameState::REST_ROOM) {
m_renderer.renderRestRoom(m_restRoom, m_player, m_animTime);
m_uiRenderer.drawRestRoomHUD(m_restRoom.getNextFloor(), m_player, m_contextPrompt);
} else {
m_renderer.render3D(m_dungeon, m_player, m_animTime);
m_uiRenderer.drawExplorationHUD(m_dungeon, m_player, m_contextPrompt);
}
m_uiRenderer.drawPauseMenu(m_saveFeedbackTimer > 0.0f);
break;
case GameState::SETTINGS:
if (m_previousState == GameState::MAIN_MENU) {
m_uiRenderer.drawMainMenu(SaveSystem::hasSaveFile());
} else if (m_previousState == GameState::HUB) {
m_renderer.renderHub(m_hubRoom, m_player, m_animTime);
m_uiRenderer.drawHubHUD(m_player, m_contextPrompt);
} else if (m_previousState == GameState::REST_ROOM) {
m_renderer.renderRestRoom(m_restRoom, m_player, m_animTime);
m_uiRenderer.drawRestRoomHUD(m_restRoom.getNextFloor(), m_player, m_contextPrompt);
} else {
m_renderer.render3D(m_dungeon, m_player, m_animTime);
m_uiRenderer.drawExplorationHUD(m_dungeon, m_player, m_contextPrompt);
}
m_uiRenderer.drawSettingsMenu(SoundManager::get().getMasterVolume(),
SoundManager::get().getMusicVolume(),
SoundManager::get().getSfxVolume(),
m_selectedSlider);
break;
case GameState::VICTORY_LEVEL:
m_uiRenderer.drawLevelClearScreen(m_dungeon.getFloor());
break;
case GameState::GAME_OVER:
m_uiRenderer.drawGameOverScreen();
break;
}
// Render screen transition wave on top of all 2D/3D elements
m_transition.draw();
EndDrawing();
}
} // namespace dnr