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'; import 'package:wolf_dart/features/renderer/color_palette.dart'; class RaycasterPainter extends CustomPainter { final Level map; final List textures; final Player player; final double fov; final Map doorOffsets; final Pushwall? activePushwall; final List sprites; final List entities; RaycasterPainter({ required this.map, required this.textures, required this.player, required this.fov, required this.doorOffsets, this.activePushwall, required this.sprites, required this.entities, }); @override void paint(Canvas canvas, Size size) { final Paint bgPaint = Paint()..isAntiAlias = false; // 1. Draw Ceiling & Floor canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height / 2), bgPaint..color = Colors.blueGrey[900]!, ); canvas.drawRect( Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2), bgPaint..color = Colors.brown[900]!, ); const int renderWidth = 320; double columnWidth = size.width / renderWidth; final Paint columnPaint = Paint() ..isAntiAlias = false ..strokeWidth = columnWidth + 0.5; List zBuffer = List.filled(renderWidth, 0.0); 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 < renderWidth; x++) { double cameraX = 2 * x / renderWidth - 1.0; Coordinate2D rayDir = dir + (plane * cameraX); int mapX = player.x.toInt(); int mapY = player.y.toInt(); double sideDistX; double sideDistY; double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs(); double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs(); double perpWallDist = 0.0; int stepX; int stepY; bool hit = false; bool hitOutOfBounds = false; int side = 0; int hitWallId = 0; double textureOffset = 0.0; // Replaces doorOffset to handle both bool customDistCalculated = false; // Flag to skip standard distance 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 passed through the open door gap } } 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); // Did we hit the face that is being pushed deeper? if (side == 0 && pDirX != 0) { if (pDirX == stepX) { double intersect = perpWallDist + pOffset * deltaDistX; if (intersect < sideDistY) { perpWallDist = intersect; // Hit the recessed front face } else { side = 1; // Missed the front face, hit the newly exposed side! 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 { // We hit the side of the sliding block. Did the ray slip behind it? 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; // Stick the texture to the block } } else { if (pDirX == 1 && wallFraction < pOffset) hit = false; if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false; if (hit) { textureOffset = pOffset * pDirX; // Stick the texture to the block } } } if (!hit) continue; // The ray slipped past! Keep looping. customDistCalculated = true; // Lock in our custom distance math } // --- STANDARD WALL --- else { hit = true; hitWallId = map[mapY][mapX]; } } } if (hitOutOfBounds) continue; // Apply standard math ONLY if we didn't calculate a sub-tile pushwall distance if (!customDistCalculated) { if (side == 0) { perpWallDist = (sideDistX - deltaDistX); } else { perpWallDist = (sideDistY - deltaDistY); } } zBuffer[x] = perpWallDist; double wallX = (side == 0) ? player.y + perpWallDist * rayDir.y : player.x + perpWallDist * rayDir.x; wallX -= wallX.floor(); double drawX = x * columnWidth; _drawTexturedColumn( canvas, drawX, perpWallDist, wallX, side, size, hitWallId, textures, textureOffset, columnPaint, ); } // --- 2. DRAW SPRITES --- // (Keep your existing sprite rendering logic exactly the same) List activeSprites = List.from(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 = ((renderWidth / 2) * (1 + transformX / transformY)) .toInt(); int spriteHeight = (size.height / transformY).abs().toInt(); int spriteColumnWidth = (spriteHeight / columnWidth).toInt(); int drawStartX = -spriteColumnWidth ~/ 2 + spriteScreenX; int drawEndX = spriteColumnWidth ~/ 2 + spriteScreenX; int clipStartX = math.max(0, drawStartX); int clipEndX = math.min(renderWidth - 1, drawEndX); for (int stripe = clipStartX; stripe < clipEndX; stripe++) { if (transformY < zBuffer[stripe]) { double texXDouble = (stripe - drawStartX) * 64 / spriteColumnWidth; int texX = texXDouble.toInt().clamp(0, 63); double startY = (size.height / 2) - (spriteHeight / 2); double stepY = spriteHeight / 64.0; double drawX = stripe * columnWidth; int safeIndex = entity.spriteIndex.clamp(0, sprites.length - 1); Sprite spritePixels = sprites[safeIndex]; for (int ty = 0; ty < 64; ty++) { int colorByte = spritePixels[texX][ty]; if (colorByte != 255) { double endY = startY + stepY + 0.5; if (endY > 0 && startY < size.height) { columnPaint.color = ColorPalette.vga[colorByte]; canvas.drawLine( Offset(drawX, startY), Offset(drawX, endY), columnPaint, ); } } startY += stepY; } } } } } } void _drawTexturedColumn( Canvas canvas, double drawX, double distance, double wallX, int side, Size size, int hitWallId, List textures, double textureOffset, Paint paint, ) { if (distance <= 0.01) distance = 0.01; double wallHeight = size.height / distance; int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt(); int texNum; int texX; if (hitWallId >= 90) { // DOORS texNum = 98.clamp(0, textures.length - 1); texX = ((wallX - textureOffset) * 64).toInt().clamp(0, 63); } else { // WALLS & PUSHWALLS texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); if (side == 1) texNum += 1; // We apply the modulo % 1.0 to handle negative texture offsets smoothly! 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; double startY = drawStart.toDouble(); double stepY = wallHeight / 64.0; for (int ty = 0; ty < 64; ty++) { int colorByte = textures[texNum][texX][ty]; paint.color = ColorPalette.vga[colorByte]; double endY = startY + stepY + 0.5; if (endY > 0 && startY < size.height) { canvas.drawLine( Offset(drawX, startY), Offset(drawX, endY), paint, ); } startY += stepY; } } @override bool shouldRepaint(RaycasterPainter oldDelegate) => true; }