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 createState() => _WolfRendererState(); } class _WolfRendererState extends State with SingleTickerProviderStateMixin { late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); late WolfMap gameMap; late Matrix 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 _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, textures: gameMap.textures, player: player, playerAngle: playerAngle, fov: fov, ), ); }, ), ), ); } }