import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/door_manager.dart'; import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/entity_registry.dart'; import 'package:wolf_dart/features/input/input_manager.dart'; import 'package:wolf_dart/features/map/wolf_map.dart'; import 'package:wolf_dart/features/player/player.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart'; import 'package:wolf_dart/features/renderer/weapon_painter.dart'; import 'package:wolf_dart/features/ui/hud.dart'; import 'package:wolf_dart/sprite_gallery.dart'; class WolfRenderer extends StatefulWidget { const WolfRenderer({ super.key, required this.difficulty, this.showSpriteGallery = false, this.isDemo = true, }); final Difficulty difficulty; final bool showSpriteGallery; final bool isDemo; @override State createState() => _WolfRendererState(); } class _WolfRendererState extends State with SingleTickerProviderStateMixin { final InputManager inputManager = InputManager(); final DoorManager doorManager = DoorManager(); late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); late WolfMap gameMap; late Matrix currentLevel; final double fov = math.pi / 3; late Player player; bool _isLoading = true; double damageFlashOpacity = 0.0; List entities = []; @override void initState() { super.initState(); _initGame(demo: widget.isDemo); } Future _initGame({bool demo = true}) async { gameMap = demo ? await WolfMap.loadDemo() : await WolfMap.load(); currentLevel = gameMap.levels[0].wallGrid; doorManager.initDoors(currentLevel); final Matrix objectLevel = gameMap.levels[0].objectGrid; for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { int objId = objectLevel[y][x]; if (objId >= 19 && objId <= 22) { double spawnAngle = 0.0; switch (objId) { case 19: spawnAngle = 3 * math.pi / 2; break; case 20: spawnAngle = 0.0; break; case 21: spawnAngle = math.pi / 2; break; case 22: spawnAngle = math.pi; break; } player = Player( x: x + 0.5, y: y + 0.5, angle: spawnAngle, ); } else { Entity? newEntity = EntityRegistry.spawn( objId, x + 0.5, y + 0.5, widget.difficulty.level, gameMap.sprites.length, ); if (newEntity != null) { entities.add(newEntity); } } } } for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { int id = currentLevel[y][x]; if ((id >= 1 && id <= 63) || (id >= 90 && id <= 101)) { // Leave walls and doors solid } else { currentLevel[y][x] = 0; } } } _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; Coordinate2D nearestSafeSpot = Coordinate2D(1.5, 1.5); for (int y = 0; y < currentLevel.length; y++) { for (int x = 0; x < currentLevel[y].length; x++) { if (currentLevel[y][x] == 0) { Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5); double dist = safeSpot.distanceTo(player.position); if (dist < shortestDist) { shortestDist = dist; nearestSafeSpot = safeSpot; } } } } player.x = nearestSafeSpot.x; player.y = nearestSafeSpot.y; } } bool _isWalkable(int x, int y) { if (currentLevel[y][x] == 0) return true; if (currentLevel[y][x] >= 90) { return doorManager.isDoorOpenEnough(x, y); } return false; } // --- ORCHESTRATOR --- void _tick(Duration elapsed) { // 1. Process intentions and receive movement vectors final inputResult = _processInputs(elapsed); doorManager.update(elapsed); // 2. Explicit State Updates player.updateWeaponSwitch(); player.angle += inputResult.dAngle; // Keep the angle neatly clamped between 0 and 2*PI if (player.angle < 0) player.angle += 2 * math.pi; if (player.angle >= 2 * math.pi) player.angle -= 2 * math.pi; _applyMovementAndCollision(inputResult.movement); _updateEntities(elapsed); // Explicit reassignment from a pure(r) function damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity); // 3. Combat player.updateWeapon( currentTime: elapsed.inMilliseconds, entities: entities, isWalkable: _isWalkable, ); // 4. Render setState(() {}); } void _takeDamage(int damage) { player.takeDamage(damage); damageFlashOpacity = 0.5; } // Returns a Record containing both movement delta and rotation delta ({Coordinate2D movement, double dAngle}) _processInputs(Duration elapsed) { inputManager.update(); const double moveSpeed = 0.16; const double turnSpeed = 0.12; Coordinate2D movement = const Coordinate2D(0, 0); double dAngle = 0.0; if (inputManager.requestedWeapon != null) { player.requestWeaponSwitch(inputManager.requestedWeapon!); } if (inputManager.isFiring) { player.fire(elapsed.inMilliseconds); } // Calculate intended rotation if (inputManager.isTurningLeft) dAngle -= turnSpeed; if (inputManager.isTurningRight) dAngle += turnSpeed; // Calculate intended movement based on CURRENT angle Coordinate2D forwardVec = Coordinate2D( math.cos(player.angle), math.sin(player.angle), ); if (inputManager.isMovingForward) { movement += forwardVec * moveSpeed; } if (inputManager.isMovingBackward) { movement -= forwardVec * moveSpeed; } if (inputManager.isInteracting) { doorManager.handleInteraction( player.x, player.y, player.angle, ); } return (movement: movement, dAngle: dAngle); } void _applyMovementAndCollision(Coordinate2D movement) { const double margin = 0.3; // Calculate the intended new position Coordinate2D newPos = player.position + movement; // Check X axis independently (allows for sliding along walls) if (movement.x != 0) { int checkX = (movement.x > 0) ? (newPos.x + margin).toInt() : (newPos.x - margin).toInt(); if (_isWalkable(checkX, player.position.y.toInt())) { player.x = newPos.x; } } // Check Y axis independently if (movement.y != 0) { int checkY = (movement.y > 0) ? (newPos.y + margin).toInt() : (newPos.y - margin).toInt(); if (_isWalkable(player.position.x.toInt(), checkY)) { player.y = newPos.y; } } } void _updateEntities(Duration elapsed) { List itemsToRemove = []; for (Entity entity in entities) { if (entity is Enemy) { entity.update( elapsedMs: elapsed.inMilliseconds, playerPosition: player.position, isWalkable: _isWalkable, onDamagePlayer: _takeDamage, ); } else if (entity is Collectible) { if (player.position.distanceTo(entity.position) < 0.5) { if (player.tryPickup(entity)) { itemsToRemove.add(entity); } } } } if (itemsToRemove.isNotEmpty) { entities.removeWhere((e) => itemsToRemove.contains(e)); } } // Takes an input and returns a value instead of implicitly changing state double _calculateScreenEffects(double currentOpacity) { if (currentOpacity > 0) { return math.max(0.0, currentOpacity - 0.05); } return currentOpacity; } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator(color: Colors.teal)); } if (widget.showSpriteGallery) { return SpriteGallery(sprites: gameMap.sprites); } return Scaffold( backgroundColor: Colors.black, body: KeyboardListener( focusNode: _focusNode, autofocus: true, onKeyEvent: (_) {}, child: Column( children: [ Expanded( child: LayoutBuilder( builder: (context, constraints) { return Center( child: AspectRatio( aspectRatio: 16 / 10, child: Stack( children: [ CustomPaint( size: Size( constraints.maxWidth, constraints.maxHeight, ), painter: RaycasterPainter( map: currentLevel, textures: gameMap.textures, player: player, fov: fov, doorOffsets: doorManager.getOffsetsForRenderer(), entities: entities, sprites: gameMap.sprites, ), ), Positioned( bottom: -20, left: 0, right: 0, child: Center( child: Transform.translate( offset: Offset( // Replaced hidden step variables with a direct intention check! (inputManager.isMovingForward || inputManager.isMovingBackward) ? math.sin( DateTime.now() .millisecondsSinceEpoch / 100, ) * 12 : 0, player.weaponAnimOffset, ), child: SizedBox( width: 500, height: 500, child: CustomPaint( painter: WeaponPainter( sprite: gameMap.sprites[player .currentWeapon .currentSprite], ), ), ), ), ), ), if (damageFlashOpacity > 0) Positioned.fill( child: Container( color: Colors.red.withValues( alpha: damageFlashOpacity, ), ), ), ], ), ), ); }, ), ), Hud(player: player), ], ), ), ); } }