From 2d8e702bf3366417cdb8c5b586da8df6282a150a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D0=B5=D0=B5=D0=B2?= Date: Wed, 7 Oct 2026 18:40:43 +0300 Subject: [PATCH] feat(debug): Add autonomous testing toolkit with CLI args, bot agent, state dump, and screenshots --- .gitignore | 5 + CMakeLists.txt | 1 + src/core/Game.cpp | 9 + src/core/Game.hpp | 3 + src/debug/DebugSystem.cpp | 741 ++++++++++++++++++++++++++++++++++++++ src/debug/DebugSystem.hpp | 91 +++++ src/main.cpp | 17 +- 7 files changed, 864 insertions(+), 3 deletions(-) create mode 100644 src/debug/DebugSystem.cpp create mode 100644 src/debug/DebugSystem.hpp diff --git a/.gitignore b/.gitignore index 2f9bead..fae8a85 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,11 @@ err.txt out.txt run_log.txt +# Debugging, Autonomous Testing & Screenshots +screenshots/ +*.png +*.json + # OS / Editor files .DS_Store Thumbs.db diff --git a/CMakeLists.txt b/CMakeLists.txt index 62f4253..3fe8e0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(DoorNroll src/hub/HubRoom.cpp src/hub/RestRoom.cpp src/core/SaveSystem.cpp + src/debug/DebugSystem.cpp ) target_include_directories(DoorNroll PRIVATE src) diff --git a/src/core/Game.cpp b/src/core/Game.cpp index 36b0f79..547695a 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -1,6 +1,7 @@ #include "Game.hpp" #include "dungeon/DungeonGen.hpp" #include "core/SoundManager.hpp" +#include "debug/DebugSystem.hpp" #include #include #include @@ -19,6 +20,7 @@ void Game::init() { m_renderer.setGraphicsQuality(SoundManager::get().getGraphicsQuality()); m_state = GameState::MAIN_MENU; SoundManager::get().playTrack(MusicTrack::MENU_HUB); + DebugSystem::get().init(this); } void Game::startNewRun() { @@ -137,6 +139,8 @@ void Game::update(float dt) { // Update dynamic music streaming engine every frame SoundManager::get().updateMusic(dt); + DebugSystem::get().update(dt); + switch (m_state) { case GameState::MAIN_MENU: updateMainMenu(dt); @@ -1221,7 +1225,12 @@ void Game::draw() { // Render screen transition wave on top of all 2D/3D elements m_transition.draw(); + // Render debug overlay if enabled + DebugSystem::get().drawOverlay(); + EndDrawing(); + + DebugSystem::get().postDraw(); } } // namespace dnr diff --git a/src/core/Game.hpp b/src/core/Game.hpp index 57f7a2b..b89ef37 100644 --- a/src/core/Game.hpp +++ b/src/core/Game.hpp @@ -25,6 +25,9 @@ public: void draw(); bool shouldClose() const { return m_shouldQuit || WindowShouldClose(); } + void requestQuit() { m_shouldQuit = true; } + + friend class DebugSystem; private: void startNewRun(); diff --git a/src/debug/DebugSystem.cpp b/src/debug/DebugSystem.cpp new file mode 100644 index 0000000..045e275 --- /dev/null +++ b/src/debug/DebugSystem.cpp @@ -0,0 +1,741 @@ +#include "DebugSystem.hpp" +#include "core/Game.hpp" +#include "core/SoundManager.hpp" +#include "dungeon/DungeonGen.hpp" +#include "combat/Enemy.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dnr { + +DebugSystem& DebugSystem::get() { + static DebugSystem instance; + return instance; +} + +DebugSystem::DebugSystem() = default; +DebugSystem::~DebugSystem() = default; + +void DebugSystem::parseArgs(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--bot") { + m_config.botEnabled = true; + } else if (arg == "--bot-duration" && i + 1 < argc) { + m_config.botDuration = std::stof(argv[++i]); + m_config.botEnabled = true; + } else if (arg == "--bot-max-steps" && i + 1 < argc) { + m_config.botMaxSteps = std::stoi(argv[++i]); + m_config.botEnabled = true; + } else if (arg == "--screenshot" && i + 1 < argc) { + m_config.singleScreenshotPath = argv[++i]; + } else if (arg == "--screenshot-events") { + m_config.screenshotEvents = true; + } else if (arg == "--screenshot-dir" && i + 1 < argc) { + m_config.screenshotDir = argv[++i]; + } else if (arg == "--state-dump" && i + 1 < argc) { + m_config.stateDumpPath = argv[++i]; + } else if (arg == "--warp-state" && i + 1 < argc) { + m_config.warpState = argv[++i]; + } else if (arg == "--floor" && i + 1 < argc) { + m_config.targetFloor = std::stoi(argv[++i]); + } else if (arg == "--god") { + m_config.godMode = true; + } else if (arg == "--turbo") { + m_config.turboMode = true; + } else if (arg == "--overlay") { + m_config.showOverlay = true; + } + } +} + +void DebugSystem::init(Game* game) { + m_game = game; + + try { + std::filesystem::create_directories(m_config.screenshotDir); + } catch (...) {} + + if (m_config.turboMode) { + SetTargetFPS(0); // Unlocked framerate for ultra-fast simulation + } +} + +void DebugSystem::toggleBot() { + m_config.botEnabled = !m_config.botEnabled; + SoundManager::get().playTumblerClick(); +} + +void DebugSystem::toggleGodMode() { + m_config.godMode = !m_config.godMode; + SoundManager::get().playPickup(); +} + +void DebugSystem::toggleOverlay() { + m_config.showOverlay = !m_config.showOverlay; + SoundManager::get().playTumblerClick(); +} + +void DebugSystem::handleHotkeys() { + if (IsKeyPressed(KEY_F1)) { + toggleOverlay(); + } + if (IsKeyPressed(KEY_F2)) { + toggleGodMode(); + } + if (IsKeyPressed(KEY_F3)) { + // Warp to next floor or instant win combat + if (m_game) { + if (m_game->m_state == GameState::COMBAT) { + m_game->m_combat.actionSpinAndShoot(m_game->m_player); + if (m_game->m_combat.getEnemy()) { + const_cast(m_game->m_combat.getEnemy())->takeDamage(999, AmmoType::KINETIC); + } + } else if (m_game->m_state == GameState::EXPLORATION) { + m_game->nextFloor(); + } + SoundManager::get().playSkillGreat(); + } + } + if (IsKeyPressed(KEY_F4)) { + // Give resources + if (m_game) { + for (int t = 1; t <= 6; ++t) { + m_game->m_player.addAmmo(static_cast(t), 5); + } + m_game->m_player.addTechScraps(100); + m_game->m_player.addPicks(5); + SoundManager::get().playPickup(); + } + } + if (IsKeyPressed(KEY_F10)) { + toggleBot(); + } + if (IsKeyPressed(KEY_F12)) { + captureScreenshot(""); + } +} + +void DebugSystem::applyWarp() { + if (!m_game || m_warpApplied || m_config.warpState.empty()) return; + + m_warpApplied = true; + std::string w = m_config.warpState; + + if (w == "HUB") { + m_game->startNewRun(); + } else if (w == "EXPLORATION") { + m_game->startNewRun(); + if (m_config.targetFloor > 1) { + m_game->m_dungeon.setFloor(m_config.targetFloor); + } + m_game->enterCyberspace(); + } else if (w == "REST_ROOM") { + m_game->startNewRun(); + m_game->m_dungeon.setFloor(m_config.targetFloor); + m_game->enterRestRoom(); + } else if (w == "COMBAT") { + m_game->startNewRun(); + m_game->enterCyberspace(); + auto enemy = std::make_unique(EnemyType::CYBER_DRONE, 1); + m_game->m_combat.startCombat(m_game->m_player, std::move(enemy)); + m_game->m_state = GameState::COMBAT; + } else if (w == "HACKING") { + m_game->startNewRun(); + m_game->enterCyberspace(); + m_game->m_hacking.start(1); + m_game->m_state = GameState::HACKING; + } +} + +void DebugSystem::update(float dt) { + if (!m_game) return; + + m_frameCounter++; + + if (m_frameCounter == 2 && !m_warpApplied) { + applyWarp(); + } + + handleHotkeys(); + + // God Mode replenishment + if (m_config.godMode) { + int maxH = m_game->m_player.getMaxHp(); + int maxS = m_game->m_player.getMaxShield(); + m_game->m_player.setStats(maxH, maxH, maxS, maxS, 99); + for (int t = 1; t <= 6; ++t) { + m_game->m_player.addAmmo(static_cast(t), 2); + } + } + + // State change detection for event screenshots + if (m_game->m_state != m_lastObservedState) { + if (m_config.screenshotEvents) { + std::string stateTag = "state_"; + switch (m_game->m_state) { + case GameState::MAIN_MENU: stateTag += "menu"; break; + case GameState::HUB: stateTag += "hub"; break; + case GameState::EXPLORATION: stateTag += "explore"; break; + case GameState::COMBAT: stateTag += "combat"; break; + case GameState::HACKING: stateTag += "hacking"; break; + case GameState::REST_ROOM: stateTag += "rest"; break; + case GameState::VICTORY_LEVEL: stateTag += "victory"; break; + case GameState::GAME_OVER: stateTag += "gameover"; break; + default: stateTag += "other"; break; + } + captureEventScreenshot(stateTag); + } + m_lastObservedState = m_game->m_state; + } + + // Bot automation + if (m_config.botEnabled) { + m_totalBotRunTime += dt; + updateBot(dt); + + if (m_config.botDuration > 0.0f && m_totalBotRunTime >= m_config.botDuration) { + m_game->m_shouldQuit = true; + } + if (m_config.botMaxSteps > 0 && m_botStepCount >= m_config.botMaxSteps) { + m_game->m_shouldQuit = true; + } + } + + // Continuous state dump + if (!m_config.stateDumpPath.empty() && (m_frameCounter % 15 == 0)) { + dumpStateToJson(m_config.stateDumpPath); + } +} + +void DebugSystem::updateBot(float dt) { + if (!m_game || m_game->m_transition.isBusy()) return; + + m_botActionTimer += dt; + float actionInterval = m_config.turboMode ? 0.02f : 0.15f; + + if (m_botActionTimer < actionInterval) return; + m_botActionTimer = 0.0f; + m_botStepCount++; + + switch (m_game->m_state) { + case GameState::MAIN_MENU: + m_game->startNewRun(); + break; + + case GameState::HUB: + executeBotHubStep(); + break; + + case GameState::EXPLORATION: + executeBotExplorationStep(); + break; + + case GameState::HACKING: + executeBotHackingStep(); + break; + + case GameState::COMBAT: + executeBotCombatStep(); + break; + + case GameState::REST_ROOM: + executeBotRestRoomStep(); + break; + + case GameState::VICTORY_LEVEL: + m_game->enterRestRoom(); + break; + + case GameState::GAME_OVER: + m_game->startNewRun(); + break; + + default: + break; + } +} + +void DebugSystem::executeBotHubStep() { + if (!m_game) return; + + if (m_game->m_hubRoom.isFacingPortal(m_game->m_player)) { + m_game->tryInteractHub(); + return; + } + + int px = m_game->m_player.getGridX(); + int py = m_game->m_player.getGridY(); + Direction facing = m_game->m_player.getFacing(); + + // Portal is at (3, 1) facing NORTH + if (py > 1) { + if (facing != Direction::NORTH) { + m_game->m_player.setFacing(Direction::NORTH); + } else { + m_game->tryMoveHub(0, -1); + } + } else if (px < 3) { + if (facing != Direction::EAST) { + m_game->m_player.setFacing(Direction::EAST); + } else { + m_game->tryMoveHub(1, 0); + } + } else if (px > 3) { + if (facing != Direction::WEST) { + m_game->m_player.setFacing(Direction::WEST); + } else { + m_game->tryMoveHub(-1, 0); + } + } else { + m_game->m_player.setFacing(Direction::NORTH); + } +} + +void DebugSystem::executeBotRestRoomStep() { + if (!m_game) return; + + if (m_game->m_restRoom.isFacingGateway(m_game->m_player)) { + m_game->tryInteractRestRoom(); + return; + } + + if (m_game->m_restRoom.isFacingRechargeMatrix(m_game->m_player) && !m_game->m_restRoom.isRecharged()) { + m_game->tryInteractRestRoom(); + return; + } + + int px = m_game->m_player.getGridX(); + int py = m_game->m_player.getGridY(); + + // Path around fountain: (3, 4) -> (2, 4) -> (2, 1) -> (3, 1) facing NORTH + if (px == 3 && py == 4) { + m_game->tryMoveRestRoom(-1, 0); // Move to (2, 4) + } else if (px == 2 && py > 1) { + m_game->tryMoveRestRoom(0, -1); // Move north to (2, 1) + } else if (px == 2 && py == 1) { + m_game->tryMoveRestRoom(1, 0); // Move east to (3, 1) + } else if (px == 3 && py == 1) { + m_game->m_player.setFacing(Direction::NORTH); + m_game->tryInteractRestRoom(); + } +} + +void DebugSystem::executeBotExplorationStep() { + if (!m_game) return; + + // Reset visited map on floor change + int curFloor = m_game->m_dungeon.getFloor(); + if (m_lastDungeonFloor != curFloor) { + m_visitedDungeonTiles.clear(); + m_lastDungeonFloor = curFloor; + } + + int px = m_game->m_player.getGridX(); + int py = m_game->m_player.getGridY(); + m_visitedDungeonTiles.insert({ px, py }); + + // Handle implant / shrine modals + if (m_game->m_showImplantDraft) { + if (!m_game->m_draftChoices.empty()) { + m_game->m_player.addImplant(m_game->m_draftChoices[0]); + } + m_game->m_showImplantDraft = false; + return; + } + if (m_game->m_showShrineUI) { + m_game->m_showShrineUI = false; + return; + } + + // If context prompt is available (door, mine, chest, exit), interact! + if (!m_game->m_contextPrompt.empty()) { + m_game->tryInteractDungeon(); + return; + } + + // BFS Pathfinding to find next actionable or unvisited target + int width = m_game->m_dungeon.getWidth(); + int height = m_game->m_dungeon.getHeight(); + + std::vector> dist(height, std::vector(width, -1)); + std::vector>> parent(height, std::vector>(width, { -1, -1 })); + std::queue> q; + + q.push({ px, py }); + dist[py][px] = 0; + + int targetX = -1, targetY = -1; + bool foundTarget = false; + + int dirs[4][2] = { {0, -1}, {1, 0}, {0, 1}, {-1, 0} }; + + while (!q.empty()) { + auto [cx, cy] = q.front(); + q.pop(); + + // Priority 1: Unvisited walkable floor or door or exit portal + bool isCurrentStart = (cx == px && cy == py); + TileType tt = m_game->m_dungeon.getTile(cx, cy); + + if (!isCurrentStart) { + if (tt == TileType::EXIT_PORTAL || tt == TileType::LOOT_CACHE || + (tt == TileType::DOOR_LOCKED && m_visitedDungeonTiles.count({cx, cy}) == 0) || + (m_game->m_dungeon.isWalkable(cx, cy) && m_visitedDungeonTiles.count({cx, cy}) == 0)) { + targetX = cx; + targetY = cy; + foundTarget = true; + break; + } + } + + for (int i = 0; i < 4; ++i) { + int nx = cx + dirs[i][0]; + int ny = cy + dirs[i][1]; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + if (dist[ny][nx] == -1) { + bool canTraverse = m_game->m_dungeon.isWalkable(nx, ny) || m_game->m_dungeon.isDoor(nx, ny); + if (canTraverse) { + dist[ny][nx] = dist[cy][cx] + 1; + parent[ny][nx] = { cx, cy }; + q.push({ nx, ny }); + } + } + } + } + } + + if (!foundTarget) { + // Fallback: search for exit portal anywhere + for (int y = 0; y < height && !foundTarget; ++y) { + for (int x = 0; x < width && !foundTarget; ++x) { + if (m_game->m_dungeon.getTile(x, y) == TileType::EXIT_PORTAL) { + targetX = x; + targetY = y; + foundTarget = true; + } + } + } + } + + if (foundTarget && (targetX != px || targetY != py)) { + // Backtrack to get the immediate next step from (px, py) + int currX = targetX; + int currY = targetY; + while (parent[currY][currX].first != -1 && + (parent[currY][currX].first != px || parent[currY][currX].second != py)) { + auto p = parent[currY][currX]; + currX = p.first; + currY = p.second; + } + + int stepX = currX; + int stepY = currY; + + int dx = stepX - px; + int dy = stepY - py; + + Direction desiredDir = Direction::NORTH; + if (dx == 1) desiredDir = Direction::EAST; + else if (dx == -1) desiredDir = Direction::WEST; + else if (dy == 1) desiredDir = Direction::SOUTH; + else if (dy == -1) desiredDir = Direction::NORTH; + + if (m_game->m_player.getFacing() == desiredDir) { + if (m_game->m_dungeon.isDoorLocked(stepX, stepY)) { + m_game->tryInteractDungeon(); + } else { + m_game->tryMoveDungeon(dx, dy); + } + } else { + m_game->m_player.setFacing(desiredDir); + } + } else { + // Random wandering if completely stuck + Vector2 dirVec = getDirectionOffset(m_game->m_player.getFacing()); + int frontX = px + static_cast(dirVec.x); + int frontY = py + static_cast(dirVec.y); + if (m_game->m_dungeon.isWalkable(frontX, frontY)) { + m_game->tryMoveDungeon(static_cast(dirVec.x), static_cast(dirVec.y)); + } else { + m_game->m_player.turnRight(); + } + } +} + +void DebugSystem::executeBotHackingStep() { + if (!m_game) return; + + if (m_game->m_hacking.getPhase() == HackingPhase::PHASE_1_TUMBLER) { + auto& tumbler = const_cast(m_game->m_hacking.getTumbler()); + int pinIdx = tumbler.getSelectedPin(); + const auto& pin = tumbler.getPin(pinIdx); + + if (pin.isInSweetSpot()) { + tumbler.tryLockCurrentPin(); + if (!tumbler.isAllPinsLocked()) { + tumbler.moveRight(); + } + } else if (pin.currentHeight < pin.targetHeight) { + tumbler.adjustHeight(0.08f); + } else { + tumbler.adjustHeight(-0.08f); + } + } else if (m_game->m_hacking.getPhase() == HackingPhase::PHASE_2_SKILLCHECK) { + auto& sc = const_cast(m_game->m_hacking.getSkillCheck()); + float angle = sc.getNeedleAngle(); + float gStart = sc.getGreatStart(); + float gEnd = sc.getGreatEnd(); + + if (angle >= gStart && angle <= gEnd) { + sc.strike(); + } + } +} + +void DebugSystem::executeBotCombatStep() { + if (!m_game) return; + + CombatPhase phase = m_game->m_combat.getPhase(); + + if (phase == CombatPhase::PLAYER_TURN) { + int hp = m_game->m_player.getHp(); + int shield = m_game->m_player.getShield(); + + if (hp < 40 && shield < 10) { + m_game->m_combat.actionShield(m_game->m_player); + } else if (m_game->m_combat.getTripleCooldown() == 0) { + m_game->m_combat.actionTripleShot(m_game->m_player); + } else { + m_game->m_combat.actionSpinAndShoot(m_game->m_player); + } + } else if (phase == CombatPhase::VICTORY) { + bool wasElite = m_game->m_combat.wasElite(); + m_game->m_player.heal(25); + m_game->m_player.addAmmo(AmmoType::KINETIC, 1); + m_game->m_player.addAmmo(AmmoType::EMP, 1); + m_game->m_state = GameState::EXPLORATION; + SoundManager::get().playTrack(MusicTrack::EXPLORATION, true); + if (wasElite) { + m_game->triggerImplantDraft(); + } + } else if (phase == CombatPhase::DEFEAT) { + if (m_config.godMode) { + m_game->m_player.heal(100); + } else { + m_game->m_state = GameState::GAME_OVER; + } + } +} + +void DebugSystem::drawOverlay() { + if (!m_config.showOverlay || !m_game) return; + + // Strict Zero-Rounding tactical industrial diagnostic HUD + int boxX = 16; + int boxY = 16; + int boxW = 390; + int boxH = 220; + + DrawRectangle(boxX, boxY, boxW, boxH, Color{ 6, 10, 16, 235 }); + DrawRectangleLines(boxX, boxY, boxW, boxH, Color{ 70, 150, 255, 255 }); // Cobalt Blue border + DrawRectangle(boxX, boxY, boxW, 24, Color{ 16, 26, 42, 255 }); + DrawRectangleLines(boxX, boxY, boxW, 24, Color{ 70, 150, 255, 255 }); + + DrawText("/// TACTICAL AGENT TELEMETRY ///", boxX + 10, boxY + 6, 12, TacticalColors::PureWhite); + + int textY = boxY + 34; + int lineStep = 18; + + std::string stateStr = "STATE: "; + switch (m_game->m_state) { + case GameState::MAIN_MENU: stateStr += "MAIN_MENU"; break; + case GameState::HUB: stateStr += "HUB_ROOM"; break; + case GameState::EXPLORATION: stateStr += "EXPLORATION"; break; + case GameState::COMBAT: stateStr += "COMBAT"; break; + case GameState::HACKING: stateStr += "HACKING_ICE"; break; + case GameState::REST_ROOM: stateStr += "REST_ROOM"; break; + case GameState::SETTINGS: stateStr += "SETTINGS"; break; + case GameState::PAUSE_MENU: stateStr += "PAUSE"; break; + case GameState::VICTORY_LEVEL: stateStr += "VICTORY"; break; + case GameState::GAME_OVER: stateStr += "GAME_OVER"; break; + default: stateStr += "UNKNOWN"; break; + } + DrawText(stateStr.c_str(), boxX + 12, textY, 13, TacticalColors::PureWhite); + textY += lineStep; + + int fps = GetFPS(); + std::string perfStr = "FPS: " + std::to_string(fps) + " | FRAME: " + std::to_string(m_frameCounter); + DrawText(perfStr.c_str(), boxX + 12, textY, 13, Color{ 180, 200, 220, 255 }); + textY += lineStep; + + const auto& p = m_game->m_player; + std::string playerStr = "HP: " + std::to_string(p.getHp()) + "/" + std::to_string(p.getMaxHp()) + + " | SHIELD: " + std::to_string(p.getShield()) + "/" + std::to_string(p.getMaxShield()) + + " | PICKS: " + std::to_string(p.getCyberPicks()); + DrawText(playerStr.c_str(), boxX + 12, textY, 13, TacticalColors::PureWhite); + textY += lineStep; + + std::string posStr = "GRID: (" + std::to_string(p.getGridX()) + ", " + std::to_string(p.getGridY()) + ") | FLOOR: " + + std::to_string(m_game->m_dungeon.getFloor()); + DrawText(posStr.c_str(), boxX + 12, textY, 13, Color{ 245, 130, 32, 255 }); // Overheat Amber + textY += lineStep; + + std::string botStr = m_config.botEnabled ? "[BOT: AUTO-PILOT ON]" : "[BOT: MANUAL]"; + Color botColor = m_config.botEnabled ? Color{ 95, 185, 75, 255 } : Color{ 140, 150, 160, 255 }; + DrawText(botStr.c_str(), boxX + 12, textY, 13, botColor); + + std::string godStr = m_config.godMode ? "[GOD: ACTIVE]" : "[GOD: OFF]"; + Color godColor = m_config.godMode ? Color{ 225, 25, 55, 255 } : Color{ 140, 150, 160, 255 }; + DrawText(godStr.c_str(), boxX + 210, textY, 13, godColor); + textY += lineStep; + + DrawText("F1: HUD | F2: GOD | F3: SKIP | F4: AMMO | F10: BOT | F12: CAPTURE", + boxX + 12, textY + 6, 10, Color{ 120, 140, 165, 255 }); +} + +void DebugSystem::postDraw() { + // Single screenshot capture on designated frame + if (!m_config.singleScreenshotPath.empty() && !m_singleScreenshotTaken && + m_frameCounter >= m_config.singleScreenshotFrame) { + std::filesystem::path p(m_config.singleScreenshotPath); + if (p.has_parent_path()) { + std::filesystem::create_directories(p.parent_path()); + } + TakeScreenshot(p.make_preferred().string().c_str()); + m_singleScreenshotTaken = true; + if (m_game) { + m_game->m_shouldQuit = true; + } + } + + // Pending event screenshot capture + if (!m_pendingScreenshotPath.empty()) { + std::filesystem::path p(m_pendingScreenshotPath); + if (p.has_parent_path()) { + std::filesystem::create_directories(p.parent_path()); + } + TakeScreenshot(p.make_preferred().string().c_str()); + m_pendingScreenshotPath.clear(); + } +} + +void DebugSystem::captureScreenshot(const std::string& customPath) { + if (!customPath.empty()) { + m_pendingScreenshotPath = customPath; + } else { + auto now = std::chrono::system_clock::now(); + auto timeT = std::chrono::system_clock::to_time_t(now); + std::tm tmNow; +#if defined(_WIN32) + localtime_s(&tmNow, &timeT); +#else + localtime_r(&timeT, &tmNow); +#endif + std::ostringstream ss; + ss << m_config.screenshotDir << "/shot_" + << std::put_time(&tmNow, "%Y%m%d_%H%M%S") << ".png"; + m_pendingScreenshotPath = ss.str(); + } +} + +void DebugSystem::captureEventScreenshot(const std::string& eventTag) { + auto now = std::chrono::system_clock::now(); + auto timeT = std::chrono::system_clock::to_time_t(now); + std::tm tmNow; +#if defined(_WIN32) + localtime_s(&tmNow, &timeT); +#else + localtime_r(&timeT, &tmNow); +#endif + std::ostringstream ss; + ss << m_config.screenshotDir << "/event_" + << std::put_time(&tmNow, "%H%M%S") << "_" << eventTag << ".png"; + m_pendingScreenshotPath = ss.str(); +} + +std::string DebugSystem::serializeStateJson() { + if (!m_game) return "{}"; + + std::ostringstream ss; + ss << "{\n"; + ss << " \"frame\": " << m_frameCounter << ",\n"; + ss << " \"time\": " << m_totalBotRunTime << ",\n"; + + std::string st = "UNKNOWN"; + switch (m_game->m_state) { + case GameState::MAIN_MENU: st = "MAIN_MENU"; break; + case GameState::HUB: st = "HUB"; break; + case GameState::EXPLORATION: st = "EXPLORATION"; break; + case GameState::COMBAT: st = "COMBAT"; break; + case GameState::HACKING: st = "HACKING"; break; + case GameState::REST_ROOM: st = "REST_ROOM"; break; + case GameState::SETTINGS: st = "SETTINGS"; break; + case GameState::PAUSE_MENU: st = "PAUSE_MENU"; break; + case GameState::VICTORY_LEVEL: st = "VICTORY_LEVEL"; break; + case GameState::GAME_OVER: st = "GAME_OVER"; break; + default: break; + } + ss << " \"state\": \"" << st << "\",\n"; + + const auto& p = m_game->m_player; + ss << " \"player\": {\n"; + ss << " \"hp\": " << p.getHp() << ",\n"; + ss << " \"max_hp\": " << p.getMaxHp() << ",\n"; + ss << " \"shield\": " << p.getShield() << ",\n"; + ss << " \"max_shield\": " << p.getMaxShield() << ",\n"; + ss << " \"picks\": " << p.getCyberPicks() << ",\n"; + ss << " \"scraps\": " << p.getTechScraps() << ",\n"; + ss << " \"grid_x\": " << p.getGridX() << ",\n"; + ss << " \"grid_y\": " << p.getGridY() << "\n"; + ss << " },\n"; + + ss << " \"dungeon\": {\n"; + ss << " \"floor\": " << m_game->m_dungeon.getFloor() << ",\n"; + ss << " \"visited_tiles\": " << m_visitedDungeonTiles.size() << "\n"; + ss << " },\n"; + + bool inCombat = (m_game->m_state == GameState::COMBAT); + ss << " \"combat\": {\n"; + ss << " \"active\": " << (inCombat ? "true" : "false"); + if (inCombat && m_game->m_combat.getEnemy()) { + const auto* e = m_game->m_combat.getEnemy(); + ss << ",\n"; + ss << " \"enemy_name\": \"" << e->getName() << "\",\n"; + ss << " \"enemy_hp\": " << e->getHp() << ",\n"; + ss << " \"enemy_max_hp\": " << e->getMaxHp() << ",\n"; + ss << " \"enemy_shield\": " << e->getShield() << "\n"; + } else { + ss << "\n"; + } + ss << " },\n"; + + ss << " \"bot\": {\n"; + ss << " \"enabled\": " << (m_config.botEnabled ? "true" : "false") << ",\n"; + ss << " \"steps\": " << m_botStepCount << ",\n"; + ss << " \"god_mode\": " << (m_config.godMode ? "true" : "false") << "\n"; + ss << " }\n"; + + ss << "}\n"; + return ss.str(); +} + +void DebugSystem::dumpStateToJson(const std::string& path) { + std::string json = serializeStateJson(); + std::ofstream out(path, std::ios::trunc); + if (out.is_open()) { + out << json; + } +} + +} // namespace dnr diff --git a/src/debug/DebugSystem.hpp b/src/debug/DebugSystem.hpp new file mode 100644 index 0000000..2da6d03 --- /dev/null +++ b/src/debug/DebugSystem.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include "core/Common.hpp" +#include +#include +#include +#include + +namespace dnr { + +class Game; + +struct DebugConfig { + bool botEnabled = false; + float botDuration = 0.0f; // Auto-quit after N seconds (0 = infinite) + int botMaxSteps = 0; // Step limit (0 = infinite) + bool godMode = false; + bool turboMode = false; + bool showOverlay = false; + bool screenshotEvents = false; + std::string singleScreenshotPath; + int singleScreenshotFrame = 8; + std::string screenshotDir = "screenshots"; + std::string stateDumpPath; + std::string warpState; // "HUB", "EXPLORATION", "COMBAT", "REST_ROOM", "HACKING" + int targetFloor = 1; +}; + +class DebugSystem { +public: + static DebugSystem& get(); + + void parseArgs(int argc, char* argv[]); + const DebugConfig& getConfig() const { return m_config; } + DebugConfig& getConfig() { return m_config; } + + void init(Game* game); + void update(float dt); + void drawOverlay(); + void postDraw(); + + // Visual captures + void captureScreenshot(const std::string& customPath = ""); + void captureEventScreenshot(const std::string& eventTag); + + // State dump + void dumpStateToJson(const std::string& path); + + // Controls + void toggleBot(); + bool isBotEnabled() const { return m_config.botEnabled; } + void toggleGodMode(); + bool isGodMode() const { return m_config.godMode; } + void toggleOverlay(); + bool isOverlayVisible() const { return m_config.showOverlay; } + + void handleHotkeys(); + void applyWarp(); + +private: + DebugSystem(); + ~DebugSystem(); + + void updateBot(float dt); + void executeBotExplorationStep(); + void executeBotCombatStep(); + void executeBotHackingStep(); + void executeBotHubStep(); + void executeBotRestRoomStep(); + + std::string serializeStateJson(); + + Game* m_game = nullptr; + DebugConfig m_config; + + // Bot telemetry + float m_botActionTimer = 0.0f; + float m_totalBotRunTime = 0.0f; + int m_botStepCount = 0; + GameState m_lastObservedState = GameState::MAIN_MENU; + std::set> m_visitedDungeonTiles; + int m_lastDungeonFloor = -1; + + // Screenshot & state dump tracking + int m_frameCounter = 0; + bool m_singleScreenshotTaken = false; + bool m_warpApplied = false; + std::string m_pendingScreenshotPath; +}; + +} // namespace dnr diff --git a/src/main.cpp b/src/main.cpp index ecdb1d5..5d0376e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,25 @@ #include "raylib.h" #include "core/Game.hpp" +#include "debug/DebugSystem.hpp" + +int main(int argc, char* argv[]) { + dnr::DebugSystem::get().parseArgs(argc, argv); + const auto& debugConfig = dnr::DebugSystem::get().getConfig(); -int main() { const int screenWidth = 1280; const int screenHeight = 720; - SetConfigFlags(FLAG_VSYNC_HINT); + if (!debugConfig.turboMode) { + SetConfigFlags(FLAG_VSYNC_HINT); + } InitWindow(screenWidth, screenHeight, "DoorNroll // Cyber-Revolver Cyberspace Crawler"); SetExitKey(KEY_NULL); // Prevent ESC from closing the game window - SetTargetFPS(60); + + if (debugConfig.turboMode) { + SetTargetFPS(0); + } else { + SetTargetFPS(60); + } dnr::Game game; game.init();