Compare commits

...
Sign in to create a new pull request.

1 commit

4 changed files with 1057 additions and 2 deletions

View file

@ -33,6 +33,7 @@ add_executable(DoorNroll
src/hacking/DbdSkillCheck.cpp
src/inventory/InventoryUI.cpp
src/render/CyberRenderer.cpp
src/render/SoftwareRenderer.cpp
src/render/UIRenderer.cpp
src/hub/HubRoom.cpp
src/hub/RestRoom.cpp

View file

@ -6,7 +6,7 @@
#include "hacking/HackingGame.hpp"
#include "combat/CombatSystem.hpp"
#include "inventory/InventoryUI.hpp"
#include "render/CyberRenderer.hpp"
#include "render/SoftwareRenderer.hpp"
#include "render/UIRenderer.hpp"
#include "render/ScreenTransition.hpp"
#include "hub/HubRoom.hpp"
@ -63,7 +63,7 @@ private:
CombatSystem m_combat;
InventoryUI m_inventoryUI;
CyberRenderer m_renderer;
SoftwareRenderer m_renderer;
UIRenderer m_uiRenderer;
ScreenTransition m_transition;

View file

@ -0,0 +1,941 @@
#include "SoftwareRenderer.hpp"
#include "hub/HubRoom.hpp"
#include "hub/RestRoom.hpp"
#include "combat/CombatSystem.hpp"
#include <cmath>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <iostream>
namespace dnr {
SoftwareRenderer::SoftwareRenderer() = default;
SoftwareRenderer::~SoftwareRenderer() {
cleanup();
}
void SoftwareRenderer::init() {
if (m_initialized) return;
m_framebuffer.resize(FB_WIDTH * FB_HEIGHT, Color{ 6, 8, 12, 255 });
m_depthBuffer.resize(FB_WIDTH, 1000.0f);
m_screenImage.data = m_framebuffer.data();
m_screenImage.width = FB_WIDTH;
m_screenImage.height = FB_HEIGHT;
m_screenImage.mipmaps = 1;
m_screenImage.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
m_screenTexture = LoadTextureFromImage(m_screenImage);
SetTextureFilter(m_screenTexture, TEXTURE_FILTER_POINT);
generateCpuTextures();
m_initialized = true;
}
void SoftwareRenderer::cleanup() {
if (m_initialized) {
UnloadTexture(m_screenTexture);
m_initialized = false;
}
}
void SoftwareRenderer::triggerGlitch(float intensity) {
m_glitchTimer = std::max(m_glitchTimer, intensity);
}
// ----------------------------------------------------------------------------
// Texture Generation & Loading
// ----------------------------------------------------------------------------
void SoftwareRenderer::generateCpuTextures() {
auto makeTex = [](CpuTexture& t, int w = TEX_SIZE, int h = TEX_SIZE) {
t.width = w;
t.height = h;
t.pixels.resize(w * h, Color{ 30, 30, 30, 255 });
};
// 1. Dungeon Wall: Dark tactical gunmetal panel with reinforced seams & rivets
makeTex(m_texDungeonWall);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isBorder = (x < 4 || x > 251 || y < 4 || y > 251 || x == 127 || x == 128 || y == 127 || y == 128);
bool isRivet = ((x == 12 || x == 243 || x == 118 || x == 137) && (y % 32 >= 14 && y % 32 <= 17));
int noise = ((x * 19 + y * 37) % 9) - 4;
int base = isBorder ? 18 : (isRivet ? 95 : 38 + noise);
m_texDungeonWall.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)std::clamp(base - 4, 8, 255),
(unsigned char)std::clamp(base, 10, 255),
(unsigned char)std::clamp(base + 8, 14, 255),
255
};
}
}
// 2. Dungeon Floor: Industrial steel grating
makeTex(m_texDungeonFloor);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isGrate = (x % 16 <= 2 || y % 16 <= 2);
int base = isGrate ? 12 : 32 + ((x ^ y) % 7);
m_texDungeonFloor.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)base,
(unsigned char)(base + 3),
(unsigned char)(base + 7),
255
};
}
}
// 3. Dungeon Ceiling: Metallic beam ribs with dim red emergency light strip
makeTex(m_texDungeonCeil);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isStrip = (x >= 124 && x <= 131 && y % 64 >= 20 && y % 64 <= 44);
int base = ((y % 32 < 4) ? 14 : 26) + ((x * 13) % 5);
if (isStrip) {
m_texDungeonCeil.pixels[y * TEX_SIZE + x] = Color{ 220, 25, 45, 255 };
} else {
m_texDungeonCeil.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)base,
(unsigned char)(base + 2),
(unsigned char)(base + 6),
255
};
}
}
}
// 4. Dungeon Blast Door: Heavy pneumatic airlock door with yellow warning chevron
makeTex(m_texDungeonDoor);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isSeam = (x == 127 || x == 128 || y == 64 || y == 192);
bool isWarning = (y >= 115 && y <= 140 && ((x + y) / 16) % 2 == 0);
if (isWarning) {
m_texDungeonDoor.pixels[y * TEX_SIZE + x] = Color{ 235, 160, 20, 255 };
} else if (isSeam) {
m_texDungeonDoor.pixels[y * TEX_SIZE + x] = Color{ 14, 18, 24, 255 };
} else {
int base = 48 + ((x * 7 + y * 11) % 6);
m_texDungeonDoor.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)base,
(unsigned char)(base + 2),
(unsigned char)(base + 6),
255
};
}
}
}
// 5. Parquet Floor: Dark smoked walnut planks
makeTex(m_texParquet);
for (int y = 0; y < TEX_SIZE; ++y) {
int row = y / 32;
int rowY = y % 32;
for (int x = 0; x < TEX_SIZE; ++x) {
int shift = (row % 2 == 0) ? 0 : 48;
int col = (x + shift) / 96;
int colX = (x + shift) % 96;
bool isSeam = (rowY == 0 || colX == 0);
if (isSeam) {
m_texParquet.pixels[y * TEX_SIZE + x] = Color{ 16, 12, 10, 255 };
} else {
int base = 42 + ((row * 13 + col * 23) % 11) - 5;
m_texParquet.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)std::clamp(base + 8, 15, 120),
(unsigned char)std::clamp(base + 2, 12, 110),
(unsigned char)std::clamp(base - 2, 10, 95),
255
};
}
}
}
// 6. Rug: Luxury Persian / cyber antique rug
makeTex(m_texRug);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
int mDist = std::max(std::abs(x - 128), std::abs(y - 128));
Color c;
if (mDist > 115) c = Color{ 115, 92, 48, 255 }; // Outer gold border
else if (mDist > 105) c = Color{ 22, 28, 42, 255 }; // Dark groove
else if (mDist > 98) c = Color{ 145, 110, 55, 255 }; // Inner accent
else c = Color{ 36, 44, 62, 255 }; // Midnight blue field
m_texRug.pixels[y * TEX_SIZE + x] = c;
}
}
// 7. Plaster Wall: Dark slate graphite
makeTex(m_texPlasterWall);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
int grain = ((x * 23 + y * 41 + (x ^ y) * 17) % 13) - 6;
int base = 34 + grain;
m_texPlasterWall.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)std::clamp(base, 12, 90),
(unsigned char)std::clamp(base + 4, 15, 95),
(unsigned char)std::clamp(base + 10, 18, 110),
255
};
}
}
// 8. Room Window: Try loading Room_window.jpg
makeTex(m_texWindowView);
bool loadedWindow = false;
const std::vector<std::string> winPaths = {
"assets/Pictures/Room_window.jpg",
"assets/Pictures/Room_window.png",
"../assets/Pictures/Room_window.jpg",
"assets/Room_window.jpg"
};
for (const auto& wp : winPaths) {
if (FileExists(wp.c_str())) {
Image img = LoadImage(wp.c_str());
if (img.data) {
ImageResize(&img, TEX_SIZE, TEX_SIZE);
ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8);
Color* pix = (Color*)img.data;
m_texWindowView.pixels.assign(pix, pix + TEX_SIZE * TEX_SIZE);
UnloadImage(img);
loadedWindow = true;
break;
}
}
}
if (!loadedWindow) {
// Fallback procedural night skyline
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
float t = y / 256.0f;
int b = static_cast<int>(18 + (1.0f - t) * 75);
m_texWindowView.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)(b * 0.7f),
(unsigned char)(b * 0.4f),
(unsigned char)b,
255
};
}
}
}
// 9. Cyber Gate & Other Textures
makeTex(m_texCyberGate);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isFrame = (x < 24 || x > 231 || y < 24);
bool isGateCore = !isFrame && (std::abs(x - 128) < 85);
if (isGateCore) {
m_texCyberGate.pixels[y * TEX_SIZE + x] = Color{ 0, 255, 128, 220 };
} else if (isFrame) {
m_texCyberGate.pixels[y * TEX_SIZE + x] = Color{ 24, 32, 44, 255 };
} else {
m_texCyberGate.pixels[y * TEX_SIZE + x] = Color{ 10, 14, 20, 255 };
}
}
}
// 10. Ancient Palace Textures (Rest Room)
makeTex(m_texAncientWall);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
int brickY = y / 32;
int shift = (brickY % 2 == 0) ? 0 : 32;
int brickX = (x + shift) / 64;
bool isMortar = (y % 32 <= 2 || (x + shift) % 64 <= 2);
int base = isMortar ? 20 : 54 + ((brickX * 17 + brickY * 31) % 15) - 7;
m_texAncientWall.pixels[y * TEX_SIZE + x] = Color{
(unsigned char)(base + 6),
(unsigned char)base,
(unsigned char)(base - 4),
255
};
}
}
m_texAncientFloor = m_texAncientWall;
m_texAncientCeil = m_texAncientWall;
// 11. SPRITES: Drone Sprite
makeTex(m_texDrone);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
int dx = x - 128;
int dy = y - 128;
float r = std::sqrt(float(dx * dx + dy * dy));
if (r < 75.0f) {
// Drone body chassis
if (r < 28.0f) {
m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 240, 245, 255, 255 }; // Glowing core
} else if (std::abs(dy) < 14 && dx > -45 && dx < 45) {
m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 230, 25, 45, 255 }; // Laser visor
} else {
int b = 35 + (int)(r * 0.4f);
m_texDrone.pixels[y * TEX_SIZE + x] = Color{ (unsigned char)b, (unsigned char)(b + 5), (unsigned char)(b + 12), 255 };
}
} else if (r < 82.0f && (std::abs(dx) > 55 || std::abs(dy) > 55)) {
m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 60, 75, 95, 255 }; // Thruster pods
} else {
m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; // Transparent background
}
}
}
// 12. SPRITES: Elite Drone
makeTex(m_texDroneElite);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
int dx = x - 128;
int dy = y - 128;
float r = std::sqrt(float(dx * dx + dy * dy));
if (r < 85.0f) {
if (r < 32.0f) {
m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ 255, 60, 75, 255 }; // Crimson reactor
} else if (r > 65.0f && r < 75.0f) {
m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ 220, 25, 45, 255 }; // Armor shroud
} else {
int b = 25 + (int)(r * 0.35f);
m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ (unsigned char)(b + 15), (unsigned char)b, (unsigned char)b, 255 };
}
} else {
m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 13. SPRITES: Desk with Dual Monitors
makeTex(m_texDesk);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isTable = (y >= 170 && y <= 230 && x >= 30 && x <= 226);
bool isLegs = (y > 230 && ((x >= 35 && x <= 50) || (x >= 206 && x <= 221)));
bool isMonLeft = (y >= 70 && y <= 165 && x >= 45 && x <= 125);
bool isMonRight = (y >= 70 && y <= 165 && x >= 135 && x <= 215);
if (isMonLeft) {
bool isCode = (y % 12 >= 8 && x % 20 < 15);
m_texDesk.pixels[y * TEX_SIZE + x] = isCode ? Color{ 220, 230, 245, 255 } : Color{ 14, 20, 32, 255 };
} else if (isMonRight) {
int rx = x - 175;
int ry = y - 117;
bool isRadar = (std::abs(rx * rx + ry * ry - 900) < 60 || rx == 0 || ry == 0);
m_texDesk.pixels[y * TEX_SIZE + x] = isRadar ? Color{ 0, 255, 180, 255 } : Color{ 8, 16, 26, 255 };
} else if (isTable) {
m_texDesk.pixels[y * TEX_SIZE + x] = Color{ 48, 38, 32, 255 };
} else if (isLegs) {
m_texDesk.pixels[y * TEX_SIZE + x] = Color{ 25, 28, 35, 255 };
} else {
m_texDesk.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 14. SPRITES: Cyber Bed
makeTex(m_texBed);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isBedFrame = (y >= 160 && y <= 245 && x >= 35 && x <= 220);
bool isPillow = (y >= 170 && y <= 200 && x >= 45 && x <= 95);
if (isPillow) {
m_texBed.pixels[y * TEX_SIZE + x] = Color{ 210, 218, 230, 255 };
} else if (isBedFrame) {
m_texBed.pixels[y * TEX_SIZE + x] = Color{ 26, 32, 48, 255 };
} else {
m_texBed.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 15. SPRITES: Loot Cache Crate
makeTex(m_texLootCache);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isBox = (y >= 140 && y <= 240 && x >= 55 && x <= 201);
if (isBox) {
bool isLed = (y >= 175 && y <= 195 && x >= 115 && x <= 141);
if (isLed) {
m_texLootCache.pixels[y * TEX_SIZE + x] = Color{ 255, 30, 50, 255 };
} else {
int b = 38 + ((x % 32 < 2 || y % 32 < 2) ? -12 : 0);
m_texLootCache.pixels[y * TEX_SIZE + x] = Color{ (unsigned char)b, (unsigned char)(b + 4), (unsigned char)(b + 8), 255 };
}
} else {
m_texLootCache.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 16. SPRITES: Exit Portal Beacon
makeTex(m_texExitPortal);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
int dx = x - 128;
bool isCore = (std::abs(dx) < 22 && y >= 60 && y <= 235);
bool isBase = (y >= 235 && std::abs(dx) < 65);
if (isCore) {
m_texExitPortal.pixels[y * TEX_SIZE + x] = Color{ 240, 30, 60, 255 };
} else if (isBase) {
m_texExitPortal.pixels[y * TEX_SIZE + x] = Color{ 30, 38, 52, 255 };
} else {
m_texExitPortal.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 17. SPRITES: Proximity Mine
makeTex(m_texMine);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isPlate = (y >= 210 && y <= 245 && x >= 70 && x <= 186);
bool isBlinker = (y >= 218 && y <= 232 && x >= 118 && x <= 138);
if (isBlinker) {
m_texMine.pixels[y * TEX_SIZE + x] = Color{ 255, 40, 50, 255 };
} else if (isPlate) {
m_texMine.pixels[y * TEX_SIZE + x] = Color{ 36, 42, 54, 255 };
} else {
m_texMine.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 18. SPRITES: Cyber Shrine Terminal
makeTex(m_texShrine);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isMonolith = (y >= 70 && y <= 245 && x >= 85 && x <= 171);
bool isScreen = (y >= 90 && y <= 150 && x >= 95 && x <= 161);
if (isScreen) {
m_texShrine.pixels[y * TEX_SIZE + x] = Color{ 230, 30, 50, 255 };
} else if (isMonolith) {
m_texShrine.pixels[y * TEX_SIZE + x] = Color{ 20, 26, 36, 255 };
} else {
m_texShrine.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
// 19. SPRITES: Laser Beam Strip
makeTex(m_texLaserBeam);
for (int y = 0; y < TEX_SIZE; ++y) {
for (int x = 0; x < TEX_SIZE; ++x) {
bool isBeam = (y >= 120 && y <= 136);
if (isBeam) {
m_texLaserBeam.pixels[y * TEX_SIZE + x] = (y >= 126 && y <= 130) ? WHITE : Color{ 240, 30, 60, 255 };
} else {
m_texLaserBeam.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 };
}
}
}
}
const SoftwareRenderer::CpuTexture& SoftwareRenderer::getTexture(int index) const {
switch (index) {
case 1: return m_texDungeonWall;
case 2: return m_texDungeonFloor;
case 3: return m_texDungeonCeil;
case 4: return m_texDungeonDoor;
case 5: return m_texParquet;
case 6: return m_texRug;
case 7: return m_texPlasterWall;
case 8: return m_texWindowView;
case 9: return m_texCyberGate;
case 10: return m_texAncientWall;
case 11: return m_texAncientFloor;
case 12: return m_texAncientCeil;
case 13: return m_texDrone;
case 14: return m_texDroneElite;
case 15: return m_texDesk;
case 16: return m_texBed;
case 17: return m_texLootCache;
case 18: return m_texExitPortal;
case 19: return m_texMine;
case 20: return m_texShrine;
case 21: return m_texLaserBeam;
default: return m_texDungeonWall;
}
}
// ----------------------------------------------------------------------------
// Core DDA Raycaster with Diminishing Light & Horizontal Spans
// ----------------------------------------------------------------------------
void SoftwareRenderer::renderSceneRaycast(float posX, float posY, float dirX, float dirY,
int mapW, int mapH,
const std::vector<int>& mapGrid,
int wallTex, int floorTex, int ceilTex,
float animTime,
const std::vector<BillboardSprite>& sprites) {
float fov = 0.66f;
float planeX = -dirY * fov;
float planeY = dirX * fov;
std::vector<int> wallDrawStart(FB_WIDTH);
std::vector<int> wallDrawEnd(FB_WIDTH);
const auto& texWall = getTexture(wallTex);
const auto& texFloor = getTexture(floorTex);
const auto& texCeil = getTexture(ceilTex);
// 1. Raycast Wall Columns
for (int x = 0; x < FB_WIDTH; ++x) {
float cameraX = 2.0f * x / float(FB_WIDTH) - 1.0f;
float rayDirX = dirX + planeX * cameraX;
float rayDirY = dirY + planeY * cameraX;
int mapX = int(posX);
int mapY = int(posY);
float deltaDistX = (rayDirX == 0.0f) ? 1e30f : std::abs(1.0f / rayDirX);
float deltaDistY = (rayDirY == 0.0f) ? 1e30f : std::abs(1.0f / rayDirY);
float perpWallDist = 0.0f;
int stepX = 0;
int stepY = 0;
float sideDistX = 0.0f;
float sideDistY = 0.0f;
if (rayDirX < 0) {
stepX = -1;
sideDistX = (posX - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0f - posX) * deltaDistX;
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = (posY - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0f - posY) * deltaDistY;
}
int hit = 0;
int side = 0; // 0 = X-axis, 1 = Y-axis
int hitTile = 1;
while (hit == 0) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX < 0 || mapX >= mapW || mapY < 0 || mapY >= mapH) {
hit = 1;
hitTile = 1;
break;
}
int t = mapGrid[mapY * mapW + mapX];
if (t > 0) {
hit = 1;
hitTile = t;
}
}
if (side == 0) perpWallDist = (sideDistX - deltaDistX);
else perpWallDist = (sideDistY - deltaDistY);
if (perpWallDist < 0.1f) perpWallDist = 0.1f;
m_depthBuffer[x] = perpWallDist;
int lineHeight = int(FB_HEIGHT / perpWallDist);
int drawStart = -lineHeight / 2 + FB_HEIGHT / 2;
if (drawStart < 0) drawStart = 0;
int drawEnd = lineHeight / 2 + FB_HEIGHT / 2;
if (drawEnd >= FB_HEIGHT) drawEnd = FB_HEIGHT - 1;
wallDrawStart[x] = drawStart;
wallDrawEnd[x] = drawEnd;
// Texture coordinate calculation
float wallX = 0.0f;
if (side == 0) wallX = posY + perpWallDist * rayDirY;
else wallX = posX + perpWallDist * rayDirX;
wallX -= std::floor(wallX);
int texX = int(wallX * float(TEX_SIZE));
if (side == 0 && rayDirX > 0) texX = TEX_SIZE - texX - 1;
if (side == 1 && rayDirY < 0) texX = TEX_SIZE - texX - 1;
// Doom diminishing light formula
float shade = 1.0f / (1.0f + perpWallDist * 0.14f);
if (side == 1) shade *= 0.80f; // Ambient directional shadow
// Tactical flashlight cone focus in center
float angleDot = (rayDirX * dirX + rayDirY * dirY);
float cone = std::clamp((angleDot - 0.75f) / 0.25f, 0.0f, 1.0f);
shade = std::clamp(shade * (0.65f + cone * 0.60f), 0.08f, 1.0f);
// Select wall texture (doors use door texture, window wall uses window)
const CpuTexture* currWallTex = &texWall;
if (hitTile == 4) currWallTex = &m_texDungeonDoor;
else if (hitTile == 8) currWallTex = &m_texWindowView;
else if (hitTile == 9) currWallTex = &m_texCyberGate;
float step = 1.0f * TEX_SIZE / lineHeight;
float texPos = (drawStart - FB_HEIGHT / 2 + lineHeight / 2) * step;
for (int y = drawStart; y <= drawEnd; ++y) {
int texY = int(texPos) & (TEX_SIZE - 1);
texPos += step;
Color c = currWallTex->sample(texX, texY);
Color finalCol = {
(unsigned char)(c.r * shade),
(unsigned char)(c.g * shade),
(unsigned char)(c.b * shade),
255
};
m_framebuffer[y * FB_WIDTH + x] = finalCol;
}
}
// 2. Horizontal Floor and Ceiling Spans
float rayDirX0 = dirX - planeX;
float rayDirY0 = dirY - planeY;
float rayDirX1 = dirX + planeX;
float rayDirY1 = dirY + planeY;
for (int y = FB_HEIGHT / 2 + 1; y < FB_HEIGHT; ++y) {
int p = y - FB_HEIGHT / 2;
float posZ = 0.5f * FB_HEIGHT;
float rowDistance = posZ / p;
float floorStepX = rowDistance * (rayDirX1 - rayDirX0) / FB_WIDTH;
float floorStepY = rowDistance * (rayDirY1 - rayDirY0) / FB_WIDTH;
float floorX = posX + rowDistance * rayDirX0;
float floorY = posY + rowDistance * rayDirY0;
float rowShade = std::clamp(1.0f / (1.0f + rowDistance * 0.16f), 0.06f, 1.0f);
for (int x = 0; x < FB_WIDTH; ++x) {
int tx = int(TEX_SIZE * (floorX - std::floor(floorX))) & (TEX_SIZE - 1);
int ty = int(TEX_SIZE * (floorY - std::floor(floorY))) & (TEX_SIZE - 1);
floorX += floorStepX;
floorY += floorStepY;
// Draw floor pixel if below wall column
if (y > wallDrawEnd[x]) {
Color fc = texFloor.sample(tx, ty);
m_framebuffer[y * FB_WIDTH + x] = Color{
(unsigned char)(fc.r * rowShade),
(unsigned char)(fc.g * rowShade),
(unsigned char)(fc.b * rowShade),
255
};
}
// Draw ceiling pixel symmetrically
int ceilY = FB_HEIGHT - y - 1;
if (ceilY < wallDrawStart[x]) {
Color cc = texCeil.sample(tx, ty);
m_framebuffer[ceilY * FB_WIDTH + x] = Color{
(unsigned char)(cc.r * rowShade),
(unsigned char)(cc.g * rowShade),
(unsigned char)(cc.b * rowShade),
255
};
}
}
}
// 3. Depth-Sorted Billboard Sprites
if (!sprites.empty()) {
std::vector<BillboardSprite> sortedSprites = sprites;
for (auto& spr : sortedSprites) {
float dx = spr.x - posX;
float dy = spr.y - posY;
spr.distSq = dx * dx + dy * dy;
}
std::sort(sortedSprites.begin(), sortedSprites.end(), [](const BillboardSprite& a, const BillboardSprite& b) {
return a.distSq > b.distSq;
});
for (const auto& spr : sortedSprites) {
float spriteX = spr.x - posX;
float spriteY = spr.y - posY;
float invDet = 1.0f / (planeX * dirY - dirX * planeY);
float transformX = invDet * (dirY * spriteX - dirX * spriteY);
float transformY = invDet * (-planeY * spriteX + planeX * spriteY);
if (transformY <= 0.15f) continue; // Behind or right on top of camera
int spriteScreenX = int((FB_WIDTH / 2) * (1.0f + transformX / transformY));
int spriteHeight = std::abs(int(FB_HEIGHT / transformY * spr.scale));
int vShift = int(spr.vOffset * spriteHeight);
int drawStartY = -spriteHeight / 2 + FB_HEIGHT / 2 + vShift;
if (drawStartY < 0) drawStartY = 0;
int drawEndY = spriteHeight / 2 + FB_HEIGHT / 2 + vShift;
if (drawEndY >= FB_HEIGHT) drawEndY = FB_HEIGHT - 1;
int spriteWidth = std::abs(int(FB_HEIGHT / transformY * spr.scale));
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0) drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= FB_WIDTH) drawEndX = FB_WIDTH - 1;
float sprShade = std::clamp(1.0f / (1.0f + transformY * 0.14f), 0.12f, 1.0f);
const auto& sprTex = getTexture(spr.texIndex);
for (int stripe = drawStartX; stripe <= drawEndX; ++stripe) {
int texX = int((stripe - (-spriteWidth / 2 + spriteScreenX)) * TEX_SIZE / spriteWidth);
if (texX < 0 || texX >= TEX_SIZE) continue;
if (transformY < m_depthBuffer[stripe]) {
for (int y = drawStartY; y <= drawEndY; ++y) {
int d = (y - vShift) * 256 - FB_HEIGHT * 128 + spriteHeight * 128;
int texY = ((d * TEX_SIZE) / spriteHeight) / 256;
texY = std::clamp(texY, 0, TEX_SIZE - 1);
Color c = sprTex.sample(texX, texY);
if (c.a > 20) {
Color finalCol;
if (spr.tint.r != 255 || spr.tint.g != 255 || spr.tint.b != 255) {
float tr = spr.tint.r / 255.0f;
float tg = spr.tint.g / 255.0f;
float tb = spr.tint.b / 255.0f;
finalCol = Color{
(unsigned char)(c.r * tr * sprShade),
(unsigned char)(c.g * tg * sprShade),
(unsigned char)(c.b * tb * sprShade),
255
};
} else {
finalCol = Color{
(unsigned char)(c.r * sprShade),
(unsigned char)(c.g * sprShade),
(unsigned char)(c.b * sprShade),
255
};
}
m_framebuffer[y * FB_WIDTH + stripe] = finalCol;
}
}
}
}
}
}
}
// ----------------------------------------------------------------------------
// Presentation with 2X Integer Upscaling
// ----------------------------------------------------------------------------
void SoftwareRenderer::presentFramebuffer() {
float dt = GetFrameTime();
if (m_glitchTimer > 0.0f) {
m_glitchTimer -= dt * 2.0f;
// CRT horizontal displacement slice artifact
int sliceY = (std::rand() % (FB_HEIGHT - 30));
int shift = (std::rand() % 15) - 7;
for (int y = sliceY; y < sliceY + 20 && y < FB_HEIGHT; ++y) {
for (int x = 0; x < FB_WIDTH; ++x) {
int srcX = std::clamp(x + shift, 0, FB_WIDTH - 1);
m_framebuffer[y * FB_WIDTH + x] = m_framebuffer[y * FB_WIDTH + srcX];
}
}
}
UpdateTexture(m_screenTexture, m_framebuffer.data());
// 2x Nearest-Neighbor Upscale to 1280x720 window
Rectangle src = { 0.0f, 0.0f, (float)FB_WIDTH, (float)FB_HEIGHT };
Rectangle dst = { 0.0f, 0.0f, (float)GetScreenWidth(), (float)GetScreenHeight() };
DrawTexturePro(m_screenTexture, src, dst, Vector2{ 0.0f, 0.0f }, 0.0f, WHITE);
}
// ----------------------------------------------------------------------------
// Exploration 3D Raycasting
// ----------------------------------------------------------------------------
void SoftwareRenderer::render3D(const Dungeon& dungeon, const Player& player, float animTime) {
if (!m_initialized) init();
int mapW = dungeon.getWidth();
int mapH = dungeon.getHeight();
std::vector<int> grid(mapW * mapH, 0);
for (int y = 0; y < mapH; ++y) {
for (int x = 0; x < mapW; ++x) {
TileType t = dungeon.getTile(x, y);
if (t == TileType::WALL) grid[y * mapW + x] = 1;
else if (t == TileType::DOOR_LOCKED || t == TileType::DOOR_UNLOCKED) grid[y * mapW + x] = 4;
else grid[y * mapW + x] = 0;
}
}
float posX = player.getVisualPosition().x / 2.0f;
float posY = player.getVisualPosition().z / 2.0f;
float rad = player.getVisualYaw() * (3.14159265f / 180.0f);
float dirX = std::sin(rad);
float dirY = -std::cos(rad);
// Build sprites list: Caches, Portals, Mines, Shrines, Lasers, Drone Patrols
std::vector<BillboardSprite> sprites;
for (int y = 0; y < mapH; ++y) {
for (int x = 0; x < mapW; ++x) {
TileType t = dungeon.getTile(x, y);
if (t == TileType::LOOT_CACHE) {
sprites.push_back({ x + 0.5f, y + 0.5f, 17, 0.75f, 0.15f, WHITE, false });
} else if (t == TileType::EXIT_PORTAL) {
sprites.push_back({ x + 0.5f, y + 0.5f, 18, 0.95f, 0.05f, WHITE, false });
}
}
}
for (const auto& l : dungeon.getLasers()) {
if (!l.disarmed) {
sprites.push_back({ l.x + 0.5f, l.y + 0.5f, 21, 0.85f, 0.0f, (l.active ? TacticalColors::BrightRed : Color{ 70, 70, 80, 255 }), false });
}
}
for (const auto& m : dungeon.getMines()) {
if (!m.defused && !m.triggered) {
sprites.push_back({ m.x + 0.5f, m.y + 0.5f, 19, 0.6f, 0.35f, WHITE, false });
}
}
for (const auto& s : dungeon.getShrines()) {
sprites.push_back({ s.x + 0.5f, s.y + 0.5f, 20, 0.9f, 0.05f, (s.used ? TacticalColors::SteelMuted : WHITE), false });
}
auto& enemies = const_cast<Dungeon&>(dungeon).getEnemies();
for (const auto& me : enemies) {
if (me.alive) {
sprites.push_back({ me.x + 0.5f, me.y + 0.5f, me.enemy.isElite() ? 14 : 13, 0.85f, 0.0f, me.enemy.getColor(), false });
}
}
renderSceneRaycast(posX, posY, dirX, dirY, mapW, mapH, grid, 1, 2, 3, animTime, sprites);
presentFramebuffer();
}
// ----------------------------------------------------------------------------
// Combat 3D Raycasting
// ----------------------------------------------------------------------------
void SoftwareRenderer::renderCombat3D(const Dungeon& dungeon, const Player& player, const CombatSystem& combat, float animTime) {
if (!m_initialized) init();
int mapW = dungeon.getWidth();
int mapH = dungeon.getHeight();
std::vector<int> grid(mapW * mapH, 0);
for (int y = 0; y < mapH; ++y) {
for (int x = 0; x < mapW; ++x) {
TileType t = dungeon.getTile(x, y);
grid[y * mapW + x] = (t == TileType::WALL) ? 1 : 0;
}
}
float posX = player.getVisualPosition().x / 2.0f;
float posY = player.getVisualPosition().z / 2.0f;
float rad = player.getVisualYaw() * (3.14159265f / 180.0f);
float dirX = std::sin(rad);
float dirY = -std::cos(rad);
std::vector<BillboardSprite> sprites;
const auto& enemy = combat.getEnemy();
if (enemy && enemy->isAlive()) {
float bob = std::sin(animTime * 3.5f) * 0.05f;
float distFront = 2.2f;
float droneX = posX + dirX * distFront;
float droneY = posY + dirY * distFront;
bool isHit = (combat.getPhase() == CombatPhase::PLAYER_ACTION_ANIM);
Color tint = isHit ? WHITE : enemy->getColor();
sprites.push_back({ droneX, droneY, enemy->isElite() ? 14 : 13, 1.05f, bob, tint, false });
}
renderSceneRaycast(posX, posY, dirX, dirY, mapW, mapH, grid, 1, 2, 3, animTime, sprites);
presentFramebuffer();
}
// ----------------------------------------------------------------------------
// Apartment Hub 2.5D Raycasting
// ----------------------------------------------------------------------------
void SoftwareRenderer::renderHub(const HubRoom& hub, const Player& player, float animTime) {
if (!m_initialized) init();
// 6x6 cozy apartment room
const int roomW = 6;
const int roomH = 6;
std::vector<int> grid(roomW * roomH, 0);
for (int y = 0; y < roomH; ++y) {
for (int x = 0; x < roomW; ++x) {
if (x == 0 || y == 5) grid[y * roomW + x] = 7; // Plaster Wall
else if (x == 5) grid[y * roomW + x] = 8; // Room Window (East wall with Room_window.jpg!)
else if (y == 0) grid[y * roomW + x] = 9; // Cyber Gate (North wall)
else grid[y * roomW + x] = 0;
}
}
float posX = std::clamp(player.getVisualPosition().x / 2.0f, 1.2f, 4.8f);
float posY = std::clamp(player.getVisualPosition().z / 2.0f, 1.2f, 4.8f);
float rad = player.getVisualYaw() * (3.14159265f / 180.0f);
float dirX = std::sin(rad);
float dirY = -std::cos(rad);
// Place apartment furniture as authentic 2.5D Doom-style sprites
std::vector<BillboardSprite> sprites = {
{ 3.0f, 1.3f, 15, 1.2f, 0.10f, WHITE, false }, // Workstation Desk with Dual Glowing Monitors
{ 1.4f, 4.0f, 16, 1.2f, 0.15f, WHITE, false } // Midnight Indigo Cyber Bed
};
renderSceneRaycast(posX, posY, dirX, dirY, roomW, roomH, grid, 7, 5, 7, animTime, sprites);
presentFramebuffer();
}
// ----------------------------------------------------------------------------
// Safe Haven Rest Room 2.5D Raycasting
// ----------------------------------------------------------------------------
void SoftwareRenderer::renderRestRoom(const RestRoom& restRoom, const Player& player, float animTime) {
if (!m_initialized) init();
const int roomW = 6;
const int roomH = 6;
std::vector<int> grid(roomW * roomH, 0);
for (int y = 0; y < roomH; ++y) {
for (int x = 0; x < roomW; ++x) {
if (x == 0 || x == 5 || y == 0 || y == 5) {
grid[y * roomW + x] = 10; // Ancient Palace Wall
} else {
grid[y * roomW + x] = 0;
}
}
}
float posX = std::clamp(player.getVisualPosition().x / 2.0f, 1.2f, 4.8f);
float posY = std::clamp(player.getVisualPosition().z / 2.0f, 1.2f, 4.8f);
float rad = player.getVisualYaw() * (3.14159265f / 180.0f);
float dirX = std::sin(rad);
float dirY = -std::cos(rad);
// Workbench and Cyber Portal
std::vector<BillboardSprite> sprites = {
{ 3.0f, 1.2f, 15, 1.1f, 0.10f, WHITE, false },
{ 3.0f, 4.6f, 18, 0.95f, 0.05f, WHITE, false }
};
renderSceneRaycast(posX, posY, dirX, dirY, roomW, roomH, grid, 10, 11, 12, animTime, sprites);
presentFramebuffer();
}
} // namespace dnr

View file

@ -0,0 +1,113 @@
#pragma once
#include "core/Common.hpp"
#include "dungeon/Dungeon.hpp"
#include "dungeon/Player.hpp"
#include <vector>
#include <memory>
#include <string>
namespace dnr {
class HubRoom;
class RestRoom;
class CombatSystem;
class SoftwareRenderer {
public:
SoftwareRenderer();
~SoftwareRenderer();
void init();
void cleanup();
void render3D(const Dungeon& dungeon, const Player& player, float animTime);
void renderCombat3D(const Dungeon& dungeon, const Player& player, const CombatSystem& combat, float animTime);
void renderHub(const HubRoom& hub, const Player& player, float animTime);
void renderRestRoom(const RestRoom& restRoom, const Player& player, float animTime);
void triggerGlitch(float intensity = 0.7f);
void setGraphicsQuality(GraphicsQuality q) { m_graphicsQuality = q; }
GraphicsQuality getGraphicsQuality() const { return m_graphicsQuality; }
private:
static constexpr int FB_WIDTH = 640;
static constexpr int FB_HEIGHT = 360;
static constexpr int TEX_SIZE = 256;
struct CpuTexture {
int width = TEX_SIZE;
int height = TEX_SIZE;
std::vector<Color> pixels;
Color sample(int x, int y) const {
if (pixels.empty()) return MAGENTA;
int px = (x >= 0 ? x : 0) % width;
int py = (y >= 0 ? y : 0) % height;
return pixels[py * width + px];
}
};
struct BillboardSprite {
float x = 0.0f;
float y = 0.0f;
int texIndex = 0;
float scale = 1.0f;
float vOffset = 0.0f; // vertical shift in height units
Color tint = WHITE;
bool additive = false;
float distSq = 0.0f;
};
void generateCpuTextures();
void renderSceneRaycast(float posX, float posY, float dirX, float dirY,
int mapW, int mapH,
const std::vector<int>& mapGrid,
int wallTex, int floorTex, int ceilTex,
float animTime,
const std::vector<BillboardSprite>& sprites);
void presentFramebuffer();
// Framebuffer & Raylib Texture
std::vector<Color> m_framebuffer;
std::vector<float> m_depthBuffer;
Image m_screenImage{};
Texture2D m_screenTexture{};
bool m_initialized = false;
GraphicsQuality m_graphicsQuality = GraphicsQuality::BALANCED;
float m_glitchTimer = 0.0f;
// CPU Textures
CpuTexture m_texDungeonWall;
CpuTexture m_texDungeonFloor;
CpuTexture m_texDungeonCeil;
CpuTexture m_texDungeonDoor;
CpuTexture m_texParquet;
CpuTexture m_texRug;
CpuTexture m_texPlasterWall;
CpuTexture m_texWindowView;
CpuTexture m_texPoster;
CpuTexture m_texBed;
CpuTexture m_texDesk;
CpuTexture m_texCyberGate;
CpuTexture m_texAncientWall;
CpuTexture m_texAncientFloor;
CpuTexture m_texAncientCeil;
CpuTexture m_texWorkbench;
// Sprite Textures
CpuTexture m_texDrone;
CpuTexture m_texDroneElite;
CpuTexture m_texLootCache;
CpuTexture m_texExitPortal;
CpuTexture m_texLaserBeam;
CpuTexture m_texMine;
CpuTexture m_texShrine;
const CpuTexture& getTexture(int index) const;
};
} // namespace dnr