diff --git a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart index ec9c694..58ebdfd 100644 --- a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart @@ -3,7 +3,6 @@ /// More dartdocs go here. library; -export 'src/ascii_rasterizer.dart'; export 'src/engine_audio.dart'; export 'src/engine_input.dart'; export 'src/managers/door_manager.dart'; diff --git a/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart b/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart new file mode 100644 index 0000000..c5c7d63 --- /dev/null +++ b/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart @@ -0,0 +1,241 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +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'; + +class ColoredChar { + final String char; + final Color color; + ColoredChar(this.char, this.color); +} + +class AsciiRasterizer { + static const String _charset = "@%#*+=-:. "; + + // NEW: Helper to safely convert and artificially boost your raw memory colors + Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) { + int r = vgaColor & 0xFF; + int g = (vgaColor >> 8) & 0xFF; + int b = (vgaColor >> 16) & 0xFF; + + // Apply the boost and clamp to 255 to prevent color overflow + r = (r * brightnessBoost).toInt().clamp(0, 255); + g = (g * brightnessBoost).toInt().clamp(0, 255); + b = (b * brightnessBoost).toInt().clamp(0, 255); + + // Force Alpha to 255 (fully opaque) + return Color.fromARGB(255, r, g, b); + } + + List> render(WolfEngine engine, FrameBuffer framebuffer) { + final int width = framebuffer.width; + final int height = framebuffer.height; + + // Grab ceiling and floor colors from the original palette + final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]); + final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]); + + final List> screen = List.generate( + height, + (_) => List.filled(width, ColoredChar(' ', ceilingColor)), + ); + + final List zBuffer = List.filled(width, 0.0); + + final Player player = engine.player; + final SpriteMap map = engine.currentLevel; + final List wallTextures = engine.data.walls; + + 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); + + // 1. CAST WALLS + 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; + int stepX, stepY, side = 0, hitWallId = 0; + bool hit = false; + + 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; + } + + 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) { + break; + } + if (map[mapY][mapX] > 0) { + hit = true; + hitWallId = map[mapY][mapX]; + } + } + + double perpWallDist = (side == 0) + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + if (perpWallDist < 0.1) perpWallDist = 0.1; + + zBuffer[x] = perpWallDist; + + double wallX = (side == 0) + ? player.y + perpWallDist * rayDir.y + : player.x + perpWallDist * rayDir.x; + wallX -= wallX.floor(); + int texX = (wallX * 64).toInt().clamp(0, 63); + + int texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2); + if (side == 1) texNum += 1; + Sprite texture = wallTextures[texNum]; + + int columnHeight = (height / perpWallDist).toInt(); + int drawStart = (-columnHeight ~/ 2 + height ~/ 2).clamp(0, height); + int drawEnd = (columnHeight ~/ 2 + height ~/ 2).clamp(0, height); + + double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0); + String wallChar = + _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( + 0, + _charset.length - 1, + )]; + + for (int y = 0; y < height; y++) { + if (y >= drawStart && y < drawEnd) { + double relativeY = (y - drawStart) / (drawEnd - drawStart); + int texY = (relativeY * 64).toInt().clamp(0, 63); + + int colorByte = texture.pixels[texX * 64 + texY]; + + // Use our new color conversion! + Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]); + + // Optional: slightly darken the Y-side walls for a faux-lighting effect + // if (side == 1) { + // pixelColor = Color.fromARGB( + // 255, + // (pixelColor.r * 0.7).toInt(), + // (pixelColor.g * 0.7).toInt(), + // (pixelColor.b * 0.7).toInt(), + // ); + // } + + screen[y][x] = ColoredChar(wallChar, pixelColor); + } else if (y >= drawEnd) { + // Floor + screen[y][x] = ColoredChar('.', floorColor); + } else { + // Ceiling + screen[y][x] = ColoredChar(' ', ceilingColor); + } + } + } + + // 2. CAST SPRITES (Enemies/Items) + final List activeSprites = List.from(engine.entities); + activeSprites.sort((a, b) { + double distA = player.position.distanceTo(a.position); + double distB = player.position.distanceTo(b.position); + return distB.compareTo(distA); + }); + + 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); + + if (transformY > 0) { + int spriteScreenX = ((width / 2) * (1 + transformX / transformY)) + .toInt(); + int spriteHeight = (height / transformY).abs().toInt(); + int spriteWidth = (spriteHeight * (width / height) * 0.6).toInt(); + + int drawStartY = -spriteHeight ~/ 2 + height ~/ 2; + int drawEndY = spriteHeight ~/ 2 + height ~/ 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 spritePixels = engine.data.sprites[safeIndex]; + + double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0); + String spriteChar = + _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( + 0, + _charset.length - 1, + )]; + + for (int stripe = clipStartX; stripe < clipEndX; stripe++) { + if (transformY < zBuffer[stripe]) { + int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); + + for ( + int y = math.max(0, drawStartY); + y < math.min(height, drawEndY); + y++ + ) { + double relativeY = (y - drawStartY) / (drawEndY - drawStartY); + int texY = (relativeY * 64).toInt().clamp(0, 63); + + int colorByte = spritePixels.pixels[texX * 64 + texY]; + + if (colorByte != 255) { + // Apply the safe color conversion here as well + Color pixelColor = _vgaToColor( + ColorPalette.vga32Bit[colorByte], + ); + screen[y][stripe] = ColoredChar(spriteChar, pixelColor); + } + } + } + } + } + } + + return screen; + } +} diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart new file mode 100644 index 0000000..5b02179 --- /dev/null +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; +import 'package:wolf_3d_engine/wolf_3d_engine.dart'; +import 'package:wolf_3d_input/wolf_3d_input.dart'; +import 'package:wolf_3d_renderer/ascii_rasterizer.dart'; + +class WolfAsciiRenderer extends StatefulWidget { + const WolfAsciiRenderer( + this.data, { + required this.difficulty, + required this.startingEpisode, + required this.audio, + super.key, + }); + + final WolfensteinData data; + final Difficulty difficulty; + final int startingEpisode; + final EngineAudio audio; + + @override + State createState() => _WolfAsciiRendererState(); +} + +class _WolfAsciiRendererState extends State + with SingleTickerProviderStateMixin { + final WolfInput inputManager = WolfInput(); + late final WolfEngine engine; + late Ticker _gameLoop; + final FocusNode _focusNode = FocusNode(); + + // Changed from String to List> + List> _asciiFrame = []; + final AsciiRasterizer _asciiRasterizer = AsciiRasterizer(); + + @override + void initState() { + super.initState(); + + engine = WolfEngine( + data: widget.data, + difficulty: widget.difficulty, + startingEpisode: widget.startingEpisode, + audio: widget.audio, + onGameWon: () { + Navigator.of(context).pop(); + }, + ); + + engine.init(); + + _gameLoop = createTicker(_tick)..start(); + _focusNode.requestFocus(); + } + + void _tick(Duration elapsed) { + if (!engine.isInitialized) return; + + inputManager.update(); + engine.tick(elapsed, inputManager.currentInput); + + // Calculate frame synchronously and trigger UI rebuild + setState(() { + // 120x40 is a great sweet spot for text density vs aspect ratio + _asciiFrame = _asciiRasterizer.render(engine, FrameBuffer(120, 40)); + }); + } + + @override + void dispose() { + _gameLoop.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!engine.isInitialized) { + return const Center(child: CircularProgressIndicator(color: Colors.teal)); + } + + return Scaffold( + backgroundColor: Colors.black, + body: KeyboardListener( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: (_) {}, + // Added the missing argument here + child: _asciiFrame.isEmpty + ? const SizedBox.shrink() + : _buildAsciiFrame(_asciiFrame), + ), + ); + } + + Widget _buildAsciiFrame(List> frameData) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: frameData.map((row) { + return RichText( + text: TextSpan( + style: const TextStyle( + fontFamily: 'Courier', + fontSize: 8, // Bumped slightly for better visibility + height: 1.0, + letterSpacing: 2.0, + ), + children: row.map((cell) { + return TextSpan( + text: cell.char, + style: TextStyle(color: cell.color), + ); + }).toList(), + ), + ); + }).toList(), + ); + } +}