feat(debug): Add autonomous testing toolkit with CLI args, bot agent, state dump, and screenshots
This commit is contained in:
parent
2730ff6720
commit
2d8e702bf3
7 changed files with 864 additions and 3 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
741
src/debug/DebugSystem.cpp
Normal 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
91
src/debug/DebugSystem.hpp
Normal 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
|
||||
17
src/main.cpp
17
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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue