import 'dart:math' as math; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_entities/wolf_3d_entities.dart'; abstract class Rasterizer { late List zBuffer; late int width; late int height; late int viewHeight; /// A multiplier to adjust the width of sprites. /// Pixel renderers usually keep this at 1.0. /// ASCII renderers can override this (e.g., 0.6) to account for tall characters. double get aspectMultiplier => 1.0; /// A multiplier to counteract tall pixel formats (like 1:2 terminal fonts). /// Defaults to 1.0 (no squish) for standard pixel rendering. double get verticalStretch => 1.0; /// The main entry point called by the game loop. /// Orchestrates the mathematical rendering pipeline. dynamic render(WolfEngine engine, FrameBuffer buffer) { width = buffer.width; height = buffer.height; // The 3D view typically takes up the top 80% of the screen viewHeight = (height * 0.8).toInt(); zBuffer = List.filled(width, 0.0); // 1. Setup the frame (clear screen, draw floor/ceiling) prepareFrame(engine); // 2. Do the heavy math for Raycasting Walls _castWalls(engine); // 3. Do the heavy math for Projecting Sprites _castSprites(engine); // 4. Draw 2D Overlays drawWeapon(engine); drawHud(engine); // 5. Finalize and return the frame data (Buffer or String/List) return finalizeFrame(); } // =========================================================================== // ABSTRACT METHODS (Implemented by the child renderers) // =========================================================================== /// Initialize buffers, clear the screen, and draw the floor/ceiling. void prepareFrame(WolfEngine engine); /// Draw a single vertical column of a wall. void drawWallColumn( int x, int drawStart, int drawEnd, int columnHeight, Sprite texture, int texX, double perpWallDist, int side, ); /// Draw a single vertical stripe of a sprite (enemy/item). void drawSpriteStripe( int stripeX, int drawStartY, int drawEndY, int spriteHeight, Sprite texture, int texX, double transformY, ); /// Draw the player's weapon overlay at the bottom of the 3D view. void drawWeapon(WolfEngine engine); /// Draw the 2D status bar at the bottom 20% of the screen. void drawHud(WolfEngine engine); /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). dynamic finalizeFrame(); // =========================================================================== // SHARED LIGHTING MATH // =========================================================================== /// Calculates depth-based lighting falloff (0.0 to 1.0). /// While the original Wolf3D didn't use depth fog, this provides a great /// atmospheric effect for custom renderers (like ASCII dithering). double calculateDepthBrightness(double distance) { return (10.0 / (distance + 2.0)).clamp(0.0, 1.0); } // =========================================================================== // CORE ENGINE MATH (Shared across all renderers) // =========================================================================== void _castWalls(WolfEngine engine) { final Player player = engine.player; final SpriteMap map = engine.currentLevel; final List wallTextures = engine.data.walls; final Map doorOffsets = engine.doorManager .getOffsetsForRenderer(); final Pushwall? activePushwall = engine.pushwallManager.activePushwall; final double fov = math.pi / 3; Coordinate2D dir = Coordinate2D( math.cos(player.angle), math.sin(player.angle), ); Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2); for (int x = 0; x < width; x++) { double cameraX = 2 * x / width - 1.0; Coordinate2D rayDir = dir + (plane * cameraX); int mapX = player.x.toInt(); int mapY = player.y.toInt(); double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs(); double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs(); double sideDistX, sideDistY, perpWallDist = 0.0; int stepX, stepY, side = 0, hitWallId = 0; bool hit = false, hitOutOfBounds = false, customDistCalculated = false; double textureOffset = 0.0; Set ignoredDoors = {}; if (rayDir.x < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; } if (rayDir.y < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } // DDA Loop while (!hit) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } if (mapY < 0 || mapY >= map.length || mapX < 0 || mapX >= map[0].length) { hit = true; hitOutOfBounds = true; } else if (map[mapY][mapX] > 0) { String mapKey = '$mapX,$mapY'; // DOOR LOGIC if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) { double currentOffset = doorOffsets[mapKey] ?? 0.0; if (currentOffset > 0.0) { double perpWallDistTemp = (side == 0) ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); double wallXTemp = (side == 0) ? player.y + perpWallDistTemp * rayDir.y : player.x + perpWallDistTemp * rayDir.x; wallXTemp -= wallXTemp.floor(); if (wallXTemp < currentOffset) { ignoredDoors.add(mapKey); continue; // Ray passes through the open part of the door } } hit = true; hitWallId = map[mapY][mapX]; textureOffset = currentOffset; } // PUSHWALL LOGIC else if (activePushwall != null && mapX == activePushwall.x && mapY == activePushwall.y) { hit = true; hitWallId = map[mapY][mapX]; double pOffset = activePushwall.offset; int pDirX = activePushwall.dirX; int pDirY = activePushwall.dirY; perpWallDist = (side == 0) ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); if (side == 0 && pDirX != 0) { if (pDirX == stepX) { double intersect = perpWallDist + pOffset * deltaDistX; if (intersect < sideDistY) { perpWallDist = intersect; } else { side = 1; perpWallDist = sideDistY - deltaDistY; } } else { perpWallDist -= (1.0 - pOffset) * deltaDistX; } } else if (side == 1 && pDirY != 0) { if (pDirY == stepY) { double intersect = perpWallDist + pOffset * deltaDistY; if (intersect < sideDistX) { perpWallDist = intersect; } else { side = 0; perpWallDist = sideDistX - deltaDistX; } } else { perpWallDist -= (1.0 - pOffset) * deltaDistY; } } else { double wallFraction = (side == 0) ? player.y + perpWallDist * rayDir.y : player.x + perpWallDist * rayDir.x; wallFraction -= wallFraction.floor(); if (side == 0) { if (pDirY == 1 && wallFraction < pOffset) hit = false; if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false; if (hit) textureOffset = pOffset * pDirY; } else { if (pDirX == 1 && wallFraction < pOffset) hit = false; if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false; if (hit) textureOffset = pOffset * pDirX; } } if (!hit) continue; customDistCalculated = true; } else { hit = true; hitWallId = map[mapY][mapX]; } } } if (hitOutOfBounds) continue; if (!customDistCalculated) { perpWallDist = (side == 0) ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); } if (perpWallDist < 0.1) perpWallDist = 0.1; // Save for sprite depth checks zBuffer[x] = perpWallDist; // Calculate Texture X Coordinate double wallX = (side == 0) ? player.y + perpWallDist * rayDir.y : player.x + perpWallDist * rayDir.x; wallX -= wallX.floor(); int texNum; if (hitWallId >= 90) { texNum = 98.clamp(0, wallTextures.length - 1); } else { texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2); if (side == 1) texNum += 1; } Sprite texture = wallTextures[texNum]; // Texture flipping for specific orientations int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63); if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX; if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX; // Calculate drawing dimensions int columnHeight = ((viewHeight / perpWallDist) * verticalStretch) .toInt(); int drawStart = (-columnHeight ~/ 2 + viewHeight ~/ 2).clamp( 0, viewHeight, ); int drawEnd = (columnHeight ~/ 2 + viewHeight ~/ 2).clamp(0, viewHeight); // Tell the implementation to draw this column drawWallColumn( x, drawStart, drawEnd, columnHeight, texture, texX, perpWallDist, side, ); } } void _castSprites(WolfEngine engine) { final Player player = engine.player; final List activeSprites = List.from(engine.entities); // Sort from furthest to closest (Painter's Algorithm) activeSprites.sort((a, b) { double distA = player.position.distanceTo(a.position); double distB = player.position.distanceTo(b.position); return distB.compareTo(distA); }); Coordinate2D dir = Coordinate2D( math.cos(player.angle), math.sin(player.angle), ); Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2); for (Entity entity in activeSprites) { Coordinate2D spritePos = entity.position - player.position; double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y); double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y); double transformY = invDet * (-plane.y * spritePos.x + plane.x * spritePos.y); // Only process if the sprite is in front of the camera if (transformY > 0) { int spriteScreenX = ((width / 2) * (1 + transformX / transformY)) .toInt(); int spriteHeight = ((viewHeight / transformY).abs() * verticalStretch) .toInt(); // Scale width based on the aspectMultiplier (useful for ASCII) int spriteWidth = (spriteHeight * aspectMultiplier / verticalStretch) .toInt(); int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2; int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2; int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; int drawEndX = spriteWidth ~/ 2 + spriteScreenX; int clipStartX = math.max(0, drawStartX); int clipEndX = math.min(width - 1, drawEndX); int safeIndex = entity.spriteIndex.clamp( 0, engine.data.sprites.length - 1, ); Sprite texture = engine.data.sprites[safeIndex]; // Loop through the visible vertical stripes for (int stripe = clipStartX; stripe < clipEndX; stripe++) { // Check the Z-Buffer to see if a wall is in front of this stripe if (transformY < zBuffer[stripe]) { int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); // Tell the implementation to draw this stripe drawSpriteStripe( stripe, drawStartY, drawEndY, spriteHeight, texture, texX, transformY, ); } } } } } /// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha int shadeColor(int color) { int r = (color & 0xFF) * 7 ~/ 10; int g = ((color >> 8) & 0xFF) * 7 ~/ 10; int b = ((color >> 16) & 0xFF) * 7 ~/ 10; return (0xFF000000) | (b << 16) | (g << 8) | r; } }