feat(debug): Add autonomous testing toolkit with CLI args, bot agent, state dump, and screenshots

This commit is contained in:
Игорь Федосеев 2026-10-07 18:40:43 +03:00
parent 2730ff6720
commit 2d8e702bf3
7 changed files with 864 additions and 3 deletions

5
.gitignore vendored
View file

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

View file

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

View file

@ -1,6 +1,7 @@
#include "Game.hpp"
#include "dungeon/DungeonGen.hpp"
#include "core/SoundManager.hpp"
#include "debug/DebugSystem.hpp"
#include <cstdlib>
#include <algorithm>
#include <cmath>
@ -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

View file

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

741
src/debug/DebugSystem.cpp Normal file
View file

@ -0,0 +1,741 @@
#include "DebugSystem.hpp"
#include "core/Game.hpp"
#include "core/SoundManager.hpp"
#include "dungeon/DungeonGen.hpp"
#include "combat/Enemy.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <queue>
#include <cmath>
#include <algorithm>
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<Enemy*>(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<AmmoType>(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<Enemy>(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<AmmoType>(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<std::vector<int>> dist(height, std::vector<int>(width, -1));
std::vector<std::vector<std::pair<int, int>>> parent(height, std::vector<std::pair<int, int>>(width, { -1, -1 }));
std::queue<std::pair<int, int>> 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<int>(dirVec.x);
int frontY = py + static_cast<int>(dirVec.y);
if (m_game->m_dungeon.isWalkable(frontX, frontY)) {
m_game->tryMoveDungeon(static_cast<int>(dirVec.x), static_cast<int>(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<TumblerLock&>(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<DbdSkillCheck&>(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

91
src/debug/DebugSystem.hpp Normal file
View file

@ -0,0 +1,91 @@
#pragma once
#include "core/Common.hpp"
#include <string>
#include <vector>
#include <set>
#include <memory>
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<std::pair<int, int>> 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

View file

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