15
lib/features/map/wolf_level.dart
Normal file
15
lib/features/map/wolf_level.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:wolf_dart/classes/matrix.dart';
|
||||
|
||||
class WolfLevel {
|
||||
final String name;
|
||||
final int width; // Always 64 in standard Wolf3D
|
||||
final int height; // Always 64
|
||||
final Matrix<int> wallGrid;
|
||||
|
||||
WolfLevel({
|
||||
required this.name,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.wallGrid,
|
||||
});
|
||||
}
|
||||
24
lib/features/map/wolf_map.dart
Normal file
24
lib/features/map/wolf_map.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/services.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<WolfLevel> levels;
|
||||
|
||||
// A private constructor so we can only instantiate this from the async loader
|
||||
WolfMap._(this.levels);
|
||||
|
||||
/// Asynchronously loads the map files and parses them into a new WolfMap instance.
|
||||
static Future<WolfMap> load() async {
|
||||
// 1. Load the binary data
|
||||
final mapHead = await rootBundle.load("assets/MAPHEAD.WL1");
|
||||
final gameMaps = await rootBundle.load("assets/GAMEMAPS.WL1");
|
||||
|
||||
// 2. Parse the data using the parser we just built
|
||||
final parsedLevels = WolfMapParser.parseMaps(mapHead, gameMaps);
|
||||
|
||||
// 3. Return the populated instance!
|
||||
return WolfMap._(parsedLevels);
|
||||
}
|
||||
}
|
||||
164
lib/features/map/wolf_map_parser.dart
Normal file
164
lib/features/map/wolf_map_parser.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:wolf_dart/classes/matrix.dart';
|
||||
import 'package:wolf_dart/features/map/wolf_level.dart';
|
||||
|
||||
abstract class WolfMapParser {
|
||||
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
|
||||
static List<WolfLevel> parseMaps(ByteData mapHead, ByteData gameMaps) {
|
||||
List<WolfLevel> levels = [];
|
||||
|
||||
// 1. READ MAPHEAD
|
||||
// The very first 16-bit word in MAPHEAD is the RLEW tag (usually 0xABCD)
|
||||
// We will need this later for decompression!
|
||||
int rlewTag = mapHead.getUint16(0, Endian.little);
|
||||
|
||||
// MAPHEAD contains up to 100 levels.
|
||||
// Starting at byte 2, there are 100 32-bit integers representing
|
||||
// the byte offset of each level's header inside GAMEMAPS.
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int mapOffset = mapHead.getUint32(2 + (i * 4), Endian.little);
|
||||
|
||||
// An offset of 0 means the level doesn't exist (end of the list)
|
||||
if (mapOffset == 0) continue;
|
||||
|
||||
// 2. READ GAMEMAPS HEADER
|
||||
// Jump to the offset in GAMEMAPS to read the 38-byte Level Header
|
||||
|
||||
// Pointers to the compressed data for the 3 planes (Walls, Objects, Extra)
|
||||
int plane0Offset = gameMaps.getUint32(mapOffset + 0, Endian.little);
|
||||
int plane1Offset = gameMaps.getUint32(mapOffset + 4, Endian.little);
|
||||
// Plane 2 (offset + 8) is usually unused in standard Wolf3D
|
||||
|
||||
// Lengths of the compressed data for each plane
|
||||
int plane0Length = gameMaps.getUint16(mapOffset + 12, Endian.little);
|
||||
int plane1Length = gameMaps.getUint16(mapOffset + 14, Endian.little);
|
||||
|
||||
// Dimensions (Always 64x64, but we read it anyway for accuracy)
|
||||
int width = gameMaps.getUint16(mapOffset + 18, Endian.little);
|
||||
int height = gameMaps.getUint16(mapOffset + 20, Endian.little);
|
||||
|
||||
// Map Name (16 bytes of ASCII text)
|
||||
List<int> nameBytes = [];
|
||||
for (int n = 0; n < 16; n++) {
|
||||
int charCode = gameMaps.getUint8(mapOffset + 22 + n);
|
||||
if (charCode == 0) break; // Null terminator
|
||||
nameBytes.add(charCode);
|
||||
}
|
||||
String name = ascii.decode(nameBytes);
|
||||
|
||||
// 3. EXTRACT AND DECOMPRESS THE WALL DATA
|
||||
final compressedWallData = gameMaps.buffer.asUint8List(
|
||||
plane0Offset,
|
||||
plane0Length,
|
||||
);
|
||||
|
||||
// 1st Pass: Un-Carmack
|
||||
Uint16List carmackExpanded = _expandCarmack(compressedWallData);
|
||||
// 2nd Pass: Un-RLEW
|
||||
List<int> flatGrid = _expandRlew(carmackExpanded, rlewTag);
|
||||
|
||||
// Convert the flat List<int> (4096 items) into a Matrix<int> (64x64 grid)
|
||||
Matrix<int> wallGrid = [];
|
||||
for (int y = 0; y < height; y++) {
|
||||
List<int> row = [];
|
||||
for (int x = 0; x < width; x++) {
|
||||
// Note: In original Wolf3D, empty space is usually ID 90 or 106,
|
||||
// but we can map them down to 0 for your raycaster logic later.
|
||||
row.add(flatGrid[y * width + x]);
|
||||
}
|
||||
wallGrid.add(row);
|
||||
}
|
||||
|
||||
levels.add(
|
||||
WolfLevel(
|
||||
name: name,
|
||||
width: width,
|
||||
height: height,
|
||||
wallGrid: wallGrid, // Pass the fully decompressed matrix!
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
// --- ALGORITHM 1: CARMACK EXPANSION ---
|
||||
static Uint16List _expandCarmack(Uint8List compressed) {
|
||||
ByteData data = ByteData.sublistView(compressed);
|
||||
|
||||
// The first 16-bit word is the total length of the expanded data in BYTES.
|
||||
int expandedLengthBytes = data.getUint16(0, Endian.little);
|
||||
int expandedLengthWords = expandedLengthBytes ~/ 2;
|
||||
Uint16List expanded = Uint16List(expandedLengthWords);
|
||||
|
||||
int inIdx = 2; // Skip the length word we just read
|
||||
int outIdx = 0;
|
||||
|
||||
while (outIdx < expandedLengthWords && inIdx < compressed.length) {
|
||||
int word = data.getUint16(inIdx, Endian.little);
|
||||
inIdx += 2;
|
||||
|
||||
int highByte = word >> 8;
|
||||
int lowByte = word & 0xFF;
|
||||
|
||||
// 0xA7 and 0xA8 are the Carmack Pointer Tags
|
||||
if (highByte == 0xA7 || highByte == 0xA8) {
|
||||
if (lowByte == 0) {
|
||||
// Exception Rule: If the length (lowByte) is 0, it's not a pointer.
|
||||
// It's literally just the tag byte followed by another byte.
|
||||
int nextByte = data.getUint8(inIdx++);
|
||||
expanded[outIdx++] = (nextByte << 8) | highByte;
|
||||
} else if (highByte == 0xA7) {
|
||||
// 0xA7 = Near Pointer (look back a few spaces)
|
||||
int offset = data.getUint8(inIdx++);
|
||||
int copyFrom = outIdx - offset;
|
||||
for (int i = 0; i < lowByte; i++) {
|
||||
expanded[outIdx++] = expanded[copyFrom++];
|
||||
}
|
||||
} else if (highByte == 0xA8) {
|
||||
// 0xA8 = Far Pointer (absolute offset from the very beginning)
|
||||
int offset = data.getUint16(inIdx, Endian.little);
|
||||
inIdx += 2;
|
||||
for (int i = 0; i < lowByte; i++) {
|
||||
expanded[outIdx++] = expanded[offset++];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal, uncompressed word
|
||||
expanded[outIdx++] = word;
|
||||
}
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
|
||||
// --- ALGORITHM 2: RLEW EXPANSION ---
|
||||
static List<int> _expandRlew(Uint16List carmackExpanded, int rlewTag) {
|
||||
// The first word is the expanded length in BYTES
|
||||
int expandedLengthBytes = carmackExpanded[0];
|
||||
int expandedLengthWords = expandedLengthBytes ~/ 2;
|
||||
List<int> rlewExpanded = List<int>.filled(expandedLengthWords, 0);
|
||||
|
||||
int inIdx = 1; // Skip the length word
|
||||
int outIdx = 0;
|
||||
|
||||
while (outIdx < expandedLengthWords && inIdx < carmackExpanded.length) {
|
||||
int word = carmackExpanded[inIdx++];
|
||||
|
||||
if (word == rlewTag) {
|
||||
// We found an RLEW tag!
|
||||
// The next word is the count, the word after that is the value.
|
||||
int count = carmackExpanded[inIdx++];
|
||||
int value = carmackExpanded[inIdx++];
|
||||
for (int i = 0; i < count; i++) {
|
||||
rlewExpanded[outIdx++] = value;
|
||||
}
|
||||
} else {
|
||||
// Normal word
|
||||
rlewExpanded[outIdx++] = word;
|
||||
}
|
||||
}
|
||||
return rlewExpanded;
|
||||
}
|
||||
}
|
||||
232
lib/features/renderer/raycast_painter.dart
Normal file
232
lib/features/renderer/raycast_painter.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
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';
|
||||
|
||||
class RaycasterPainter extends CustomPainter {
|
||||
final Matrix<int> map;
|
||||
final LinearCoordinates player;
|
||||
final double playerAngle;
|
||||
final double fov;
|
||||
|
||||
RaycasterPainter({
|
||||
required this.map,
|
||||
required this.player,
|
||||
required this.playerAngle,
|
||||
required this.fov,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// 1. Draw Ceiling & Floor
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height / 2),
|
||||
Paint()..color = Colors.blueGrey[900]!,
|
||||
);
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2),
|
||||
Paint()..color = Colors.brown[900]!,
|
||||
);
|
||||
|
||||
int screenWidth = size.width.toInt();
|
||||
|
||||
// 2. Camera Plane Setup
|
||||
// Direction vector of the player
|
||||
double dirX = math.cos(playerAngle);
|
||||
double dirY = math.sin(playerAngle);
|
||||
|
||||
// The camera plane is perpendicular to the direction vector.
|
||||
// Multiplying by tan(fov/2) scales the plane to match our field of view.
|
||||
double planeX = -dirY * math.tan(fov / 2);
|
||||
double planeY = dirX * math.tan(fov / 2);
|
||||
|
||||
for (int x = 0; x < screenWidth; x++) {
|
||||
// Calculate where on the camera plane this ray passes (-1 is left edge, 1 is right edge)
|
||||
double cameraX = 2 * x / screenWidth - 1.0;
|
||||
double rayDirX = dirX + planeX * cameraX;
|
||||
double rayDirY = dirY + planeY * cameraX;
|
||||
|
||||
// Current map box we are in
|
||||
int mapX = player.x.toInt();
|
||||
int mapY = player.y.toInt();
|
||||
|
||||
// Length of ray from current position to next x or y-side
|
||||
double sideDistX;
|
||||
double sideDistY;
|
||||
|
||||
// Length of ray from one x or y-side to next x or y-side
|
||||
double deltaDistX = (rayDirX == 0)
|
||||
? double.infinity
|
||||
: (1.0 / rayDirX).abs();
|
||||
double deltaDistY = (rayDirY == 0)
|
||||
? double.infinity
|
||||
: (1.0 / rayDirY).abs();
|
||||
double perpWallDist;
|
||||
|
||||
// Direction to step in x or y direction (+1 or -1)
|
||||
int stepX;
|
||||
int stepY;
|
||||
|
||||
bool hit = false;
|
||||
// 0 for North/South (vertical) walls, 1 for East/West (horizontal) walls
|
||||
int side = 0;
|
||||
int hitWallId = 0;
|
||||
|
||||
// Calculate step and initial sideDist
|
||||
if (rayDirX < 0) {
|
||||
stepX = -1;
|
||||
sideDistX = (player.x - mapX) * deltaDistX;
|
||||
} else {
|
||||
stepX = 1;
|
||||
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
|
||||
}
|
||||
if (rayDirY < 0) {
|
||||
stepY = -1;
|
||||
sideDistY = (player.y - mapY) * deltaDistY;
|
||||
} else {
|
||||
stepY = 1;
|
||||
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
|
||||
}
|
||||
|
||||
// 3. The True DDA Loop
|
||||
while (!hit) {
|
||||
// Jump to next map square, either in x-direction, or in y-direction
|
||||
if (sideDistX < sideDistY) {
|
||||
sideDistX += deltaDistX;
|
||||
mapX += stepX;
|
||||
side = 0;
|
||||
} else {
|
||||
sideDistY += deltaDistY;
|
||||
mapY += stepY;
|
||||
side = 1;
|
||||
}
|
||||
|
||||
// Check bounds and wall collisions
|
||||
if (mapY < 0 ||
|
||||
mapY >= map.length ||
|
||||
mapX < 0 ||
|
||||
mapX >= map[0].length) {
|
||||
hit = true;
|
||||
perpWallDist = 20.0; // Out of bounds fallback
|
||||
} else if (map[mapY][mapX] > 0) {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate distance projected on camera direction (No fisheye effect!)
|
||||
if (side == 0) {
|
||||
perpWallDist = (sideDistX - deltaDistX);
|
||||
} else {
|
||||
perpWallDist = (sideDistY - deltaDistY);
|
||||
}
|
||||
|
||||
// 4. Calculate exact wall hit coordinate for textures
|
||||
double wallX;
|
||||
if (side == 0) {
|
||||
wallX = player.y + perpWallDist * rayDirY;
|
||||
} else {
|
||||
wallX = player.x + perpWallDist * rayDirX;
|
||||
}
|
||||
wallX -= wallX.floor(); // Get just the fractional part (0.0 to 0.99)
|
||||
|
||||
_drawTexturedColumn(
|
||||
canvas,
|
||||
x,
|
||||
perpWallDist,
|
||||
wallX,
|
||||
side,
|
||||
size,
|
||||
hitWallId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawTexturedColumn(
|
||||
Canvas canvas,
|
||||
int x,
|
||||
double distance,
|
||||
double wallX,
|
||||
int side,
|
||||
Size size,
|
||||
int hitWallId,
|
||||
) {
|
||||
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);
|
||||
|
||||
// --- PROCEDURAL TEXTURE LOGIC ---
|
||||
Color baseColor;
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
bool shouldRepaint(covariant RaycasterPainter oldDelegate) {
|
||||
return oldDelegate.player != player ||
|
||||
oldDelegate.playerAngle != playerAngle;
|
||||
}
|
||||
}
|
||||
171
lib/features/renderer/renderer.dart
Normal file
171
lib/features/renderer/renderer.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
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/features/map/wolf_map.dart';
|
||||
import 'package:wolf_dart/features/renderer/raycast_painter.dart';
|
||||
|
||||
class WolfRenderer extends StatefulWidget {
|
||||
const WolfRenderer({super.key});
|
||||
|
||||
@override
|
||||
State<WolfRenderer> createState() => _WolfRendererState();
|
||||
}
|
||||
|
||||
class _WolfRendererState extends State<WolfRenderer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late Ticker _gameLoop;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late WolfMap gameMap;
|
||||
late Matrix<int> currentLevel;
|
||||
|
||||
bool _isLoading = true;
|
||||
|
||||
LinearCoordinates player = (x: 2.5, y: 2.5);
|
||||
double playerAngle = 0.0;
|
||||
final double fov = math.pi / 3;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initGame();
|
||||
}
|
||||
|
||||
Future<void> _initGame() async {
|
||||
// 1. Load the entire WAD/WL1 data
|
||||
gameMap = await WolfMap.load();
|
||||
|
||||
// 2. Extract Level 1 (E1M1)
|
||||
currentLevel = gameMap.levels[0].wallGrid;
|
||||
|
||||
// 3. (Optional) Remap the Wolf3D floor IDs so they work with your raycaster.
|
||||
// In Wolf3D, 90 through 106 are usually empty floor. Your raycaster currently
|
||||
// expects 0 to be empty space. Let's force them to 0 for now.
|
||||
for (int y = 0; y < 64; y++) {
|
||||
for (int x = 0; x < 64; x++) {
|
||||
// In Wolf3D, wall values are 1 through ~63.
|
||||
// Values 90+ represent empty floor spaces and doors.
|
||||
// Let's zero out anything 90 or above, and LEAVE the walls alone.
|
||||
if (currentLevel[y][x] >= 90) {
|
||||
currentLevel[y][x] = 0; // Empty space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Start the game!
|
||||
_bumpPlayerIfStuck();
|
||||
_gameLoop = createTicker(_tick)..start();
|
||||
_focusNode.requestFocus();
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_gameLoop.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _bumpPlayerIfStuck() {
|
||||
int pX = player.x.toInt();
|
||||
int pY = player.y.toInt();
|
||||
|
||||
if (pY < 0 ||
|
||||
pY >= currentLevel.length ||
|
||||
pX < 0 ||
|
||||
pX >= currentLevel[0].length ||
|
||||
currentLevel[pY][pX] > 0) {
|
||||
double shortestDist = double.infinity;
|
||||
LinearCoordinates nearestSafeSpot = (x: 1.5, y: 1.5);
|
||||
|
||||
for (int y = 0; y < currentLevel.length; y++) {
|
||||
for (int x = 0; x < currentLevel[y].length; x++) {
|
||||
if (currentLevel[y][x] == 0) {
|
||||
double safeX = x + 0.5;
|
||||
double safeY = y + 0.5;
|
||||
double dist = math.sqrt(
|
||||
math.pow(safeX - player.x, 2) + math.pow(safeY - player.y, 2),
|
||||
);
|
||||
|
||||
if (dist < shortestDist) {
|
||||
shortestDist = dist;
|
||||
nearestSafeSpot = (x: safeX, y: safeY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
player = nearestSafeSpot;
|
||||
}
|
||||
}
|
||||
|
||||
void _tick(Duration elapsed) {
|
||||
const double moveSpeed = 0.05;
|
||||
const double turnSpeed = 0.04;
|
||||
|
||||
double newX = player.x;
|
||||
double newY = player.y;
|
||||
|
||||
final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed;
|
||||
|
||||
if (pressedKeys.contains(LogicalKeyboardKey.keyW)) {
|
||||
newX += math.cos(playerAngle) * moveSpeed;
|
||||
newY += math.sin(playerAngle) * moveSpeed;
|
||||
}
|
||||
if (pressedKeys.contains(LogicalKeyboardKey.keyS)) {
|
||||
newX -= math.cos(playerAngle) * moveSpeed;
|
||||
newY -= math.sin(playerAngle) * moveSpeed;
|
||||
}
|
||||
|
||||
if (pressedKeys.contains(LogicalKeyboardKey.keyA)) {
|
||||
playerAngle -= turnSpeed;
|
||||
}
|
||||
if (pressedKeys.contains(LogicalKeyboardKey.keyD)) {
|
||||
playerAngle += turnSpeed;
|
||||
}
|
||||
|
||||
// Keep the angle mapped cleanly between 0 and 2*PI (optional, but good practice)
|
||||
if (playerAngle < 0) playerAngle += 2 * math.pi;
|
||||
if (playerAngle > 2 * math.pi) playerAngle -= 2 * math.pi;
|
||||
|
||||
if (currentLevel[newY.toInt()][newX.toInt()] == 0) {
|
||||
player = (x: newX, y: newY);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.teal));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: KeyboardListener(
|
||||
focusNode: _focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: (_) {},
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return CustomPaint(
|
||||
size: Size(constraints.maxWidth, constraints.maxHeight),
|
||||
painter: RaycasterPainter(
|
||||
map: currentLevel,
|
||||
player: player,
|
||||
playerAngle: playerAngle,
|
||||
fov: fov,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user