diff --git a/analysis_options.yaml b/analysis_options.yaml index f9b3034..85ac016 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,4 @@ include: package:flutter_lints/flutter.yaml + +formatter: + trailing_commas: preserve diff --git a/assets/VSWAP.WL1 b/assets/VSWAP.WL1 new file mode 100644 index 0000000..b295b32 Binary files /dev/null and b/assets/VSWAP.WL1 differ diff --git a/lib/features/map/vswap_parser.dart b/lib/features/map/vswap_parser.dart new file mode 100644 index 0000000..9ee3a00 --- /dev/null +++ b/lib/features/map/vswap_parser.dart @@ -0,0 +1,42 @@ +import 'dart:typed_data'; + +import 'package:wolf_dart/classes/matrix.dart'; + +class VswapParser { + /// Extracts the 64x64 wall textures from VSWAP.WL1 + static List> parseWalls(ByteData vswap) { + // 1. Read Header + int chunks = vswap.getUint16(0, Endian.little); + int spriteStart = vswap.getUint16(2, Endian.little); + // int soundStart = vswap.getUint16(4, Endian.little); // We don't need this yet + + // 2. Read Offsets (Where does each chunk start in the file?) + List offsets = []; + for (int i = 0; i < spriteStart; i++) { + offsets.add(vswap.getUint32(6 + (i * 4), Endian.little)); + } + + // 3. Extract the Wall Textures + List>> textures = []; + + // Walls are chunks 0 through (spriteStart - 1) + for (int i = 0; i < spriteStart; i++) { + int offset = offsets[i]; + if (offset == 0) continue; // Empty chunk + + // Walls are always exactly 64x64 pixels (4096 bytes) + // Note: Wolf3D stores pixels in COLUMN-MAJOR order (Top to bottom, then left to right) + List> texture = List.generate(64, (_) => List.filled(64, 0)); + + for (int x = 0; x < 64; x++) { + for (int y = 0; y < 64; y++) { + int byteIndex = offset + (x * 64) + y; + texture[x][y] = vswap.getUint8(byteIndex); + } + } + textures.add(texture); + } + + return textures; + } +} diff --git a/lib/features/map/wolf_map.dart b/lib/features/map/wolf_map.dart index c78f133..83d958f 100644 --- a/lib/features/map/wolf_map.dart +++ b/lib/features/map/wolf_map.dart @@ -1,24 +1,29 @@ import 'package:flutter/services.dart'; +import 'package:wolf_dart/classes/matrix.dart'; +import 'package:wolf_dart/features/map/vswap_parser.dart'; import 'package:wolf_dart/features/map/wolf_level.dart'; import 'package:wolf_dart/features/map/wolf_map_parser.dart'; class WolfMap { /// The fully parsed and decompressed levels from the game files. final List levels; + final List> textures; // A private constructor so we can only instantiate this from the async loader - WolfMap._(this.levels); + WolfMap._(this.levels, this.textures); /// Asynchronously loads the map files and parses them into a new WolfMap instance. static Future load() async { // 1. Load the binary data final mapHead = await rootBundle.load("assets/MAPHEAD.WL1"); final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1"); + final vswap = await rootBundle.load("assets/VSWAP.WL1"); // 2. Parse the data using the parser we just built final parsedLevels = WolfMapParser.parseMaps(mapHead, gameMaps); + final parsedTextures = VswapParser.parseWalls(vswap); // 3. Return the populated instance! - return WolfMap._(parsedLevels); + return WolfMap._(parsedLevels, parsedTextures); } } diff --git a/lib/features/renderer/color_palette.dart b/lib/features/renderer/color_palette.dart new file mode 100644 index 0000000..1e17732 --- /dev/null +++ b/lib/features/renderer/color_palette.dart @@ -0,0 +1,293 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +extension WolfPaletteMatch on Color { + /// Finds the index of the closest color in the wolfPalette + int findClosestIndex(List palette) { + int closestIndex = 0; + double minDistance = double.infinity; + + for (int i = 0; i < palette.length; i++) { + final Color pColor = palette[i]; + + // Calculate squared Euclidean distance (skipping sqrt for performance) + double distance = + pow(r - pColor.r, 2).toDouble() + + pow(g - pColor.g, 2).toDouble() + + pow(b - pColor.b, 2).toDouble(); + + if (distance < minDistance) { + minDistance = distance; + closestIndex = i; + } + } + return closestIndex; + } + + /// Returns the actual Color object from the palette that matches best + Color toWolfColor(List palette) { + return palette[findClosestIndex(palette)]; + } +} + +abstract class ColorPalette { + static const List vga = [ + Color(0xFF000000), + Color(0xFF0000AA), + Color(0xFF00AA00), + Color(0xFF00AAAA), + Color(0xFFAA0000), + Color(0xFFAA00AA), + Color(0xFFAA5500), + Color(0xFFAAAAAA), + Color(0xFF555555), + Color(0xFF5555FF), + Color(0xFF55FF55), + Color(0xFF55FFFF), + Color(0xFFFF5555), + Color(0xFFFF55FF), + Color(0xFFFFFF55), + Color(0xFFFFFFFF), + Color(0xFFEEEEEE), + Color(0xFFDEDEDE), + Color(0xFFD2D2D2), + Color(0xFFC2C2C2), + Color(0xFFB6B6B6), + Color(0xFFAAAAAA), + Color(0xFF999999), + Color(0xFF8D8D8D), + Color(0xFF7D7D7D), + Color(0xFF717171), + Color(0xFF656565), + Color(0xFF555555), + Color(0xFF484848), + Color(0xFF383838), + Color(0xFF2C2C2C), + Color(0xFF202020), + Color(0xFFFF0000), + Color(0xFFEE0000), + Color(0xFFE20000), + Color(0xFFD60000), + Color(0xFFCA0000), + Color(0xFFBE0000), + Color(0xFFB20000), + Color(0xFFA50000), + Color(0xFF990000), + Color(0xFF890000), + Color(0xFF7D0000), + Color(0xFF710000), + Color(0xFF650000), + Color(0xFF590000), + Color(0xFF4C0000), + Color(0xFF400000), + Color(0xFFFFDADA), + Color(0xFFFFBABA), + Color(0xFFFF9D9D), + Color(0xFFFF7D7D), + Color(0xFFFF5D5D), + Color(0xFFFF4040), + Color(0xFFFF2020), + Color(0xFFFF0000), + Color(0xFFFFAA5D), + Color(0xFFFF9940), + Color(0xFFFF8920), + Color(0xFFFF7900), + Color(0xFFE66D00), + Color(0xFFCE6100), + Color(0xFFB65500), + Color(0xFF9D4C00), + Color(0xFFFFFFDA), + Color(0xFFFFFFBA), + Color(0xFFFFFF9D), + Color(0xFFFFFF7D), + Color(0xFFFFFA5D), + Color(0xFFFFF640), + Color(0xFFFFF620), + Color(0xFFFFF600), + Color(0xFFE6DA00), + Color(0xFFCEC600), + Color(0xFFB6AE00), + Color(0xFF9D9D00), + Color(0xFF858500), + Color(0xFF716D00), + Color(0xFF595500), + Color(0xFF404000), + Color(0xFFD2FF5D), + Color(0xFFC6FF40), + Color(0xFFB6FF20), + Color(0xFFA1FF00), + Color(0xFF91E600), + Color(0xFF81CE00), + Color(0xFF75B600), + Color(0xFF619D00), + Color(0xFFDAFFDA), + Color(0xFFBEFFBA), + Color(0xFF9DFF9D), + Color(0xFF81FF7D), + Color(0xFF61FF5D), + Color(0xFF40FF40), + Color(0xFF20FF20), + Color(0xFF00FF00), + Color(0xFF00FF00), + Color(0xFF00EE00), + Color(0xFF00E200), + Color(0xFF00D600), + Color(0xFF04CA00), + Color(0xFF04BE00), + Color(0xFF04B200), + Color(0xFF04A500), + Color(0xFF049900), + Color(0xFF048900), + Color(0xFF047D00), + Color(0xFF047100), + Color(0xFF046500), + Color(0xFF045900), + Color(0xFF044C00), + Color(0xFF044000), + Color(0xFFDAFFFF), + Color(0xBAFFFFFF), + Color(0x9DFFFFFF), + Color(0x7DFFFAFF), + Color(0xFF5DFFFF), + Color(0x40FFFFFF), + Color(0x20FFFFFF), + Color(0x00FFFFFF), + Color(0x00E6E6FF), + Color(0x00CECEFF), + Color(0x00B6B6FF), + Color(0x009D9DFF), + Color(0x008585FF), + Color(0x007171FF), + Color(0x005959FF), + Color(0x004040FF), + Color(0xFF5DBEFF), + Color(0x40B2FFFF), + Color(0x20AAFFFF), + Color(0x009DFFFF), + Color(0x008DE6FF), + Color(0x007DCEFF), + Color(0x006DB6FF), + Color(0x005D9DFF), + Color(0xDADADAFF), + Color(0xBABEFFFF), + Color(0x9D9DFFFF), + Color(0x7D81FFFF), + Color(0x5d61ffff), + Color(0x4040FFFF), + Color(0x2024FFFF), + Color(0x0004FFFF), + Color(0xFF0000FF), + Color(0x0000EEFF), + Color(0x0000E2FF), + Color(0x0000D6FF), + Color(0xFF0000CA), + Color(0x0000BEFF), + Color(0x0000B2FF), + Color(0x0000A5FF), + Color(0xFF000099), + Color(0x000089FF), + Color(0x00007DFF), + Color(0x000071FF), + Color(0xFF000065), + Color(0x000059FF), + Color(0x00004CFF), + Color(0x000040FF), + Color(0xFF282828), + Color(0xFFFFE234), + Color(0xFFFFD624), + Color(0xFFFFCE18), + Color(0xFFFFC208), + Color(0xFFFFB600), + Color(0xFFB620FF), + Color(0xAA00FFFF), + Color(0xFF9900E6), + Color(0x8100CEFF), + Color(0x7500B6FF), + Color(0x61009DFF), + Color(0xFF500085), + Color(0x440071FF), + Color(0x340059FF), + Color(0x280040FF), + Color(0xFFFFDAFF), + Color(0xFFFFBAFF), + Color(0xFFFF9DFF), + Color(0xFFFF7DFF), + Color(0xFFFF5DFF), + Color(0xFFFF40FF), + Color(0xFFFF20FF), + Color(0xFFFF00FF), + Color(0xFFE200E6), + Color(0xCA00CEFF), + Color(0xB600B6FF), + Color(0x9D009DFF), + Color(0xFF850085), + Color(0x6D0071FF), + Color(0x590059FF), + Color(0x400040FF), + Color(0xFFFFEADE), + Color(0xFFFFE2D2), + Color(0xFFFFDAC6), + Color(0xFFFFD6BE), + Color(0xFFFFCEB2), + Color(0xFFFFC6A5), + Color(0xFFFFBE9D), + Color(0xFFFFBA91), + Color(0xFFFFB281), + Color(0xFFFA571F), + Color(0xFFFF9D61), + Color(0xFFF2955D), + Color(0xFFEA8D59), + Color(0xFFDE8955), + Color(0xFFD28150), + Color(0xFFCA7D4C), + Color(0xFFBE7948), + Color(0xFFB67144), + Color(0xFFAA6940), + Color(0xFFA1653C), + Color(0xFF9D6138), + Color(0xFF915D34), + Color(0xFF895930), + Color(0xFF81502C), + Color(0xFF754C28), + Color(0xFF6D4824), + Color(0xFF5D4020), + Color(0xFF553C1C), + Color(0xFF483818), + Color(0x403018FF), + Color(0x382C14FF), + Color(0x28200CFF), + Color(0xFF610065), + Color(0x006565FF), + Color(0x006161FF), + Color(0x00001CFF), + Color(0xFF00002C), + Color(0x302410FF), + Color(0x480048FF), + Color(0x500050FF), + Color(0xFF000034), + Color(0x1C1C1CFF), + Color(0x4C4C4CFF), + Color(0x5D5D5DFF), + Color(0xFF404040), + Color(0x303030FF), + Color(0x343434FF), + Color(0xDAF6F6FF), + Color(0xBAEAEAFF), + Color(0x00009dde), + Color(0x00075cac), + Color(0x48C2C2FF), + Color(0xFF20B6B6), + Color(0x20B2B2FF), + Color(0x00A5A5FF), + Color(0x009999FF), + Color(0xFF008D8D), + Color(0x008585FF), + Color(0x007D7DFF), + Color(0x007979FF), + Color(0xFF007575), + Color(0x007171FF), + Color(0x006D6DFF), + Color(0x990089FF), + ]; +} diff --git a/lib/features/renderer/raycast_painter.dart b/lib/features/renderer/raycast_painter.dart index eb97da3..f4f0675 100644 --- a/lib/features/renderer/raycast_painter.dart +++ b/lib/features/renderer/raycast_painter.dart @@ -3,15 +3,18 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; +import 'package:wolf_dart/features/renderer/color_palette.dart'; class RaycasterPainter extends CustomPainter { final Matrix map; + final List> textures; final LinearCoordinates player; final double playerAngle; final double fov; RaycasterPainter({ required this.map, + required this.textures, required this.player, required this.playerAngle, required this.fov, @@ -139,6 +142,7 @@ class RaycasterPainter extends CustomPainter { side, size, hitWallId, + textures, ); } } @@ -151,77 +155,47 @@ class RaycasterPainter extends CustomPainter { int side, Size size, int hitWallId, + List> textures, ) { if (distance <= 0.01) distance = 0.01; double wallHeight = size.height / distance; - double drawStart = (size.height / 2) - (wallHeight / 2); - double drawEnd = (size.height / 2) + (wallHeight / 2); + int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt(); - // --- PROCEDURAL TEXTURE LOGIC --- - Color baseColor; + // 1. PERFECT TEXTURE MAPPING + // Wolf3D stores textures in pairs. Even = N/S (Light), Odd = E/W (Dark). + int texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); + if (side == 1) texNum += 1; // Instantly use the native dark texture! - // Draw a dark edge on the sides of the block to create "tiles" - if (wallX < 0.05 || wallX > 0.95) { - baseColor = Colors.black87; - } else { - switch (hitWallId) { - case 1: - case 2: - case 3: - baseColor = Colors.grey[600]!; // Standard Grey Stone - break; - case 7: - case 8: - case 19: - baseColor = Colors.brown[600]!; // Wood Paneling - break; - case 9: - case 10: - baseColor = Colors.indigo[800]!; // Blue Stone - break; - case 17: - baseColor = Colors.red[900]!; // Red Brick - break; - case 41: - case 42: - baseColor = Colors.blueGrey; // Elevator walls - break; - default: - baseColor = Colors.teal; // Fallback for unknown IDs + int texX = (wallX * 64).toInt().clamp(0, 63); + + if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX; + if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX; + + double startY = drawStart.toDouble(); + double stepY = wallHeight / 64.0; + + for (int ty = 0; ty < 64; ty++) { + int colorByte = textures[texNum][texX][ty]; + + // 2. NO MORE SHADOW MATH + // Because we selected the correct dark texture above, we just draw the raw color! + Color pixelColor = ColorPalette.vga[colorByte]; + + double endY = startY + stepY; + + if (endY > 0 && startY < size.height) { + canvas.drawLine( + Offset(x.toDouble(), startY), + Offset(x.toDouble(), endY), + Paint() + ..color = pixelColor + ..strokeWidth = 1.1, + ); } + + startY += stepY; } - - // Faux-Lighting: Darken East/West walls to give a 3D pop to corners - if (side == 1) { - baseColor = Color.fromARGB( - 255, - ((baseColor.r * 255).round().clamp(0, 255) * 0.7).toInt(), - ((baseColor.g * 255).round().clamp(0, 255) * 0.7).toInt(), - ((baseColor.b * 255).round().clamp(0, 255) * 0.7).toInt(), - ); - } - - // Depth cueing: Dim colors as they get further away - double dimFactor = (1.0 - (distance / 15)).clamp(0.0, 1.0); - Color finalColor = Color.fromARGB( - 255, - ((baseColor.r * 255).round().clamp(0, 255) * dimFactor).toInt(), - ((baseColor.g * 255).round().clamp(0, 255) * dimFactor).toInt(), - ((baseColor.b * 255).round().clamp(0, 255) * dimFactor).toInt(), - ); - - final paint = Paint() - ..color = finalColor - ..strokeWidth = - 1.1 // Prevent transparent gaps between line strokes - ..style = PaintingStyle.stroke; - - canvas.drawLine( - Offset(x.toDouble(), drawStart), - Offset(x.toDouble(), drawEnd), - paint, - ); } @override diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 47d094b..85d9019 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -158,6 +158,7 @@ class _WolfRendererState extends State size: Size(constraints.maxWidth, constraints.maxHeight), painter: RaycasterPainter( map: currentLevel, + textures: gameMap.textures, player: player, playerAngle: playerAngle, fov: fov, diff --git a/lib/main.dart b/lib/main.dart index 81025ef..85bd872 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; import 'package:wolf_dart/features/renderer/renderer.dart'; -void main() => runApp(const MaterialApp(home: WolfRenderer())); +void main() { + runApp( + const MaterialApp(home: WolfRenderer()), + ); +}