From bf8d9d7eb178f367ad274a1ceef267de6a69c4e1 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 13 Mar 2026 18:06:40 +0100 Subject: [PATCH] WIP: Mapping sprite IDs (broken) Signed-off-by: Hans Kokx --- .../renderer => classes}/color_palette.dart | 0 lib/classes/sprite.dart | 1 + lib/features/map/vswap_parser.dart | 2 +- lib/features/renderer/raycast_painter.dart | 119 +++++++++++++++--- lib/features/renderer/renderer.dart | 58 +++++++-- lib/main.dart | 7 +- lib/sprite_gallery.dart | 64 ++++++++++ 7 files changed, 220 insertions(+), 31 deletions(-) rename lib/{features/renderer => classes}/color_palette.dart (100%) create mode 100644 lib/classes/sprite.dart create mode 100644 lib/sprite_gallery.dart diff --git a/lib/features/renderer/color_palette.dart b/lib/classes/color_palette.dart similarity index 100% rename from lib/features/renderer/color_palette.dart rename to lib/classes/color_palette.dart diff --git a/lib/classes/sprite.dart b/lib/classes/sprite.dart new file mode 100644 index 0000000..9225c16 --- /dev/null +++ b/lib/classes/sprite.dart @@ -0,0 +1 @@ +typedef Sprite = ({double x, double y, int spriteIndex}); diff --git a/lib/features/map/vswap_parser.dart b/lib/features/map/vswap_parser.dart index b502a6e..7194115 100644 --- a/lib/features/map/vswap_parser.dart +++ b/lib/features/map/vswap_parser.dart @@ -43,7 +43,7 @@ class VswapParser { /// Extracts the compiled scaled sprites from VSWAP.WL1 static List> parseSprites(ByteData vswap) { int chunks = vswap.getUint16(0, Endian.little); - int spriteStart = vswap.getUint16(2, Endian.little); + int spriteStart = vswap.getUint16(2, Endian.little); // 64 int soundStart = vswap.getUint16(4, Endian.little); List offsets = []; diff --git a/lib/features/renderer/raycast_painter.dart b/lib/features/renderer/raycast_painter.dart index 34f84db..2972a1c 100644 --- a/lib/features/renderer/raycast_painter.dart +++ b/lib/features/renderer/raycast_painter.dart @@ -1,9 +1,10 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:wolf_dart/classes/color_palette.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'; +import 'package:wolf_dart/classes/sprite.dart'; class RaycasterPainter extends CustomPainter { final Matrix map; @@ -12,6 +13,8 @@ class RaycasterPainter extends CustomPainter { final double playerAngle; final double fov; final Map doorOffsets; + final List> sprites; + final List entities; RaycasterPainter({ required this.map, @@ -20,6 +23,8 @@ class RaycasterPainter extends CustomPainter { required this.playerAngle, required this.fov, required this.doorOffsets, + required this.sprites, + required this.entities, }); @override @@ -36,11 +41,15 @@ class RaycasterPainter extends CustomPainter { int screenWidth = size.width.toInt(); + // The 1D Z-Buffer + List zBuffer = List.filled(screenWidth, 0.0); + double dirX = math.cos(playerAngle); double dirY = math.sin(playerAngle); double planeX = -dirY * math.tan(fov / 2); double planeY = dirX * math.tan(fov / 2); + // --- 1. CAST WALLS --- for (int x = 0; x < screenWidth; x++) { double cameraX = 2 * x / screenWidth - 1.0; double rayDirX = dirX + planeX * cameraX; @@ -51,23 +60,18 @@ class RaycasterPainter extends CustomPainter { double sideDistX; double sideDistY; - double deltaDistX = (rayDirX == 0) ? 1e30 : (1.0 / rayDirX).abs(); double deltaDistY = (rayDirY == 0) ? 1e30 : (1.0 / rayDirY).abs(); double perpWallDist; int stepX; int stepY; - bool hit = false; bool hitOutOfBounds = false; int side = 0; int hitWallId = 0; - double doorOffset = - 0.0; // Track the slide amount to pass to the texture drawer - - Set ignoredDoors = - {}; // Prevent the ray from checking the same door gap twice + double doorOffset = 0.0; + Set ignoredDoors = {}; if (rayDirX < 0) { stepX = -1; @@ -84,7 +88,6 @@ class RaycasterPainter extends CustomPainter { sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } - // 3. The True DDA Loop while (!hit) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; @@ -103,13 +106,10 @@ class RaycasterPainter extends CustomPainter { hit = true; hitOutOfBounds = true; } else if (map[mapY][mapX] > 0) { - // NEW: DOOR PASS-THROUGH LOGIC String doorKey = '$mapX,$mapY'; if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(doorKey)) { double currentOffset = doorOffsets[doorKey] ?? 0.0; - if (currentOffset > 0.0) { - // Calculate exactly where the ray hit the block double perpWallDistTemp = (side == 0) ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); @@ -117,23 +117,18 @@ class RaycasterPainter extends CustomPainter { ? player.y + perpWallDistTemp * rayDirY : player.x + perpWallDistTemp * rayDirX; wallXTemp -= wallXTemp.floor(); - - // If the hit coordinate is less than the open amount, the ray passes through! if (wallXTemp < currentOffset) { ignoredDoors.add(doorKey); - continue; // Skip the rest of this loop iteration and keep raycasting! + continue; } } - doorOffset = - currentOffset; // Save the offset so we can slide the texture + doorOffset = currentOffset; } - hit = true; hitWallId = map[mapY][mapX]; } } - // If the ray escaped the map, don't draw garbage, just draw the background! if (hitOutOfBounds) continue; if (side == 0) { @@ -142,6 +137,9 @@ class RaycasterPainter extends CustomPainter { perpWallDist = (sideDistY - deltaDistY); } + // STORE THE DISTANCE IN THE Z-BUFFER! + zBuffer[x] = perpWallDist; + double wallX; if (side == 0) { wallX = player.y + perpWallDist * rayDirY; @@ -162,6 +160,89 @@ class RaycasterPainter extends CustomPainter { doorOffset, ); } + + // --- 2. DRAW SPRITES --- + + // Sort sprites from furthest to closest (Painter's Algorithm) + List activeSprites = List.from(entities); + activeSprites.sort((a, b) { + double distA = + math.pow(player.x - a.x, 2) + math.pow(player.y - a.y, 2).toDouble(); + double distB = + math.pow(player.x - b.x, 2) + math.pow(player.y - b.y, 2).toDouble(); + return distB.compareTo(distA); + }); + + for (Sprite sprite in activeSprites) { + // Translate sprite position to relative to camera + double spriteX = sprite.x - player.x; + double spriteY = sprite.y - player.y; + + // Inverse camera matrix (Transform to screen space) + double invDet = 1.0 / (planeX * dirY - dirX * planeY); + double transformX = invDet * (dirY * spriteX - dirX * spriteY); + double transformY = invDet * (-planeY * spriteX + planeX * spriteY); + + // print("Sprite at ${sprite.x}, ${sprite.y} transformY: $transformY"); + + // Is the sprite in front of the camera? + if (transformY > 0) { + // print( + // "Attempting to draw Sprite Index: ${sprite.spriteIndex} (Total Sprites Loaded: ${sprites.length})", + // ); + + int spriteScreenX = ((screenWidth / 2) * (1 + transformX / transformY)) + .toInt(); + + // Calculate height and width (Sprites are 64x64 squares) + int spriteHeight = (size.height / transformY).abs().toInt(); + int spriteWidth = spriteHeight; + + int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; + int drawEndX = spriteWidth ~/ 2 + spriteScreenX; + + // Clip to screen boundaries + int clipStartX = math.max(0, drawStartX); + int clipEndX = math.min(screenWidth - 1, drawEndX); + + for (int stripe = clipStartX; stripe < clipEndX; stripe++) { + // THE Z-BUFFER CHECK! + // Only draw if the sprite is closer to the camera than the wall at this pixel column + if (transformY < zBuffer[stripe]) { + int texX = ((stripe - drawStartX) * 64 / spriteWidth).toInt().clamp( + 0, + 63, + ); + + double startY = (size.height / 2) - (spriteHeight / 2); + double stepY = spriteHeight / 64.0; + + // Safeguard against bad sprite indices + int safeIndex = sprite.spriteIndex.clamp(0, sprites.length - 1); + Matrix spritePixels = sprites[safeIndex]; + + for (int ty = 0; ty < 64; ty++) { + int colorByte = spritePixels[texX][ty]; + + // Only draw if the pixel is NOT 255 (Magenta Transparency) + if (colorByte != 255) { + double endY = startY + stepY; + if (endY > 0 && startY < size.height) { + canvas.drawLine( + Offset(stripe.toDouble(), startY), + Offset(stripe.toDouble(), endY), + Paint() + ..color = ColorPalette.vga[colorByte] + ..strokeWidth = 1.1, + ); + } + } + startY += stepY; + } + } + } + } + } } void _drawTexturedColumn( diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index ea35738..1d0e486 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -5,11 +5,20 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; +import 'package:wolf_dart/classes/sprite.dart'; import 'package:wolf_dart/features/map/wolf_map.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart'; +import 'package:wolf_dart/sprite_gallery.dart'; class WolfRenderer extends StatefulWidget { - const WolfRenderer({super.key}); + const WolfRenderer({ + super.key, + this.showSpriteGallery = false, + this.isDemo = true, + }); + + final bool showSpriteGallery; + final bool isDemo; @override State createState() => _WolfRendererState(); @@ -30,6 +39,8 @@ class _WolfRendererState extends State bool _isLoading = true; bool _spaceWasPressed = false; + List entities = []; + // Track door animations // Key is "X,Y" coordinate. Value is how far open it is (0.0 to 1.0) Map doorOffsets = {}; @@ -50,28 +61,49 @@ class _WolfRendererState extends State final Matrix objectLevel = gameMap.levels[0].objectGrid; - // 1. SCAN FOR PLAYER SPAWN + // Adjusted mapping for WL1 Shareware + // These numbers represent the delta from the FIRST sprite chunk in VSWAP + Map staticObjects = { + 23: widget.isDemo ? 0 : 23, // Water Puddle + 24: widget.isDemo ? 1 : 24, // Green Barrel + 25: widget.isDemo ? 2 : 25, // Chairs + 26: widget.isDemo ? 3 : 26, // Green Plant + 27: widget.isDemo ? 4 : 27, // Skeleton / Bones + 28: widget.isDemo ? 5 : 28, // Blue Lamp + 29: widget.isDemo ? 6 : 29, // Chandelier + 30: widget.isDemo ? 7 : 30, // Dog food + 31: widget.isDemo ? 8 : 31, // White pillar + 32: widget.isDemo ? 9 : 32, // Hanged man + 50: widget.isDemo ? 42 : 95, // Standing Guard (Front-facing frame) + }; + + // 1. SCAN FOR PLAYER SPAWN & ENTITIES for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { int objId = objectLevel[y][x]; - // In Wolf3D, IDs 19-22 represent the player's spawn point and facing direction. + // Player Spawn if (objId >= 19 && objId <= 22) { - // Place the player perfectly in the center of the block player = (x: x + 0.5, y: y + 0.5); - - // Map the ID to standard radians switch (objId) { case 19: - playerAngle = 3 * math.pi / 2; // North (Facing up the Y-axis) + playerAngle = 3 * math.pi / 2; case 20: - playerAngle = 0.0; // East (Facing right) + playerAngle = 0.0; case 21: - playerAngle = math.pi / 2; // South (Facing down) + playerAngle = math.pi / 2; case 22: - playerAngle = math.pi; // West (Facing left) + playerAngle = math.pi; } } + // NEW: Populate the Entities! + else if (staticObjects.containsKey(objId)) { + entities.add(( + x: x + 0.5, + y: y + 0.5, + spriteIndex: staticObjects[objId]!, + )); + } } } @@ -236,6 +268,10 @@ class _WolfRendererState extends State return const Center(child: CircularProgressIndicator(color: Colors.teal)); } + if (widget.showSpriteGallery) { + return SpriteGallery(sprites: gameMap.sprites); + } + return Scaffold( backgroundColor: Colors.black, body: KeyboardListener( @@ -253,6 +289,8 @@ class _WolfRendererState extends State playerAngle: playerAngle, fov: fov, doorOffsets: doorOffsets, + entities: entities, + sprites: gameMap.sprites, ), ); }, diff --git a/lib/main.dart b/lib/main.dart index 85bd872..f964c8a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,11 @@ import 'package:wolf_dart/features/renderer/renderer.dart'; void main() { runApp( - const MaterialApp(home: WolfRenderer()), + const MaterialApp( + home: WolfRenderer( + showSpriteGallery: false, + isDemo: true, + ), + ), ); } diff --git a/lib/sprite_gallery.dart b/lib/sprite_gallery.dart new file mode 100644 index 0000000..72b2d3f --- /dev/null +++ b/lib/sprite_gallery.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:wolf_dart/classes/color_palette.dart'; +import 'package:wolf_dart/classes/matrix.dart'; + +class SpriteGallery extends StatelessWidget { + final List> sprites; + + const SpriteGallery({super.key, required this.sprites}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("VSWAP Sprite Gallery")), + backgroundColor: Colors.black, + body: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, // 8 sprites per row + ), + itemCount: sprites.length, + itemBuilder: (context, index) { + return Column( + children: [ + Text( + "Idx: $index", + style: const TextStyle(color: Colors.white, fontSize: 10), + ), + Expanded( + child: CustomPaint( + painter: SingleSpritePainter(sprite: sprites[index]), + size: const Size(64, 64), + ), + ), + ], + ); + }, + ), + ); + } +} + +class SingleSpritePainter extends CustomPainter { + final Matrix sprite; + SingleSpritePainter({required this.sprite}); + + @override + void paint(Canvas canvas, Size size) { + double pixelSize = size.width / 64; + for (int x = 0; x < 64; x++) { + for (int y = 0; y < 64; y++) { + int colorByte = sprite[x][y]; + if (colorByte != 255) { + // Skip transparency + canvas.drawRect( + Rect.fromLTWH(x * pixelSize, y * pixelSize, pixelSize, pixelSize), + Paint()..color = ColorPalette.vga[colorByte], + ); + } + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +}