import 'dart:math' as math; import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/entity.dart'; class BrownGuard extends Enemy { static const double speed = 0.03; bool _hasFiredThisCycle = false; BrownGuard({ required super.x, required super.y, required super.angle, required super.mapId, }) : super( // Default front-facing idle spriteIndex: 50, state: EntityState.idle, ); static BrownGuard? trySpawn( int objId, double x, double y, int difficultyLevel, ) { bool canSpawn = false; switch (difficultyLevel) { case 0: canSpawn = objId >= 108 && objId <= 115; break; case 1: canSpawn = objId >= 144 && objId <= 151; break; case 2: canSpawn = objId >= 180 && objId <= 187; break; case 3: canSpawn = objId >= 216 && objId <= 223; break; } if (canSpawn) { return BrownGuard( x: x, y: y, angle: Enemy.getInitialAngle(objId), mapId: objId, ); } return null; // Not a Brown Guard! } @override @override void update({ required int elapsedMs, required Coordinate2D playerPosition, required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, }) { // 1. Wake up logic if (state == EntityState.idle && hasLineOfSight(playerPosition, isWalkable)) { state = EntityState.patrolling; lastActionTime = elapsedMs; } // 2. Pre-calculate angles (needed for almost all states) double dx = playerPosition.x - x; double dy = playerPosition.y - y; double distance = math.sqrt(dx * dx + dy * dy); double angleToPlayer = math.atan2(dy, dx); // Face the player if active if (state != EntityState.idle && state != EntityState.dead) { angle = angleToPlayer; } double diff = angle - angleToPlayer; while (diff <= -math.pi) { diff += 2 * math.pi; } while (diff > math.pi) { diff -= 2 * math.pi; } int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; if (octant < 0) octant += 8; // 3. State Machine switch (state) { case EntityState.idle: spriteIndex = 50 + octant; break; case EntityState.patrolling: // Movement if (distance > 0.8) { double moveX = x + math.cos(angle) * speed; double moveY = y + math.sin(angle) * speed; if (isWalkable(moveX.toInt(), y.toInt())) x = moveX; if (isWalkable(x.toInt(), moveY.toInt())) y = moveY; } // Animation int walkFrame = (elapsedMs ~/ 150) % 4; spriteIndex = 58 + (walkFrame * 8) + octant; // Shooting transition if (distance < 5.0 && elapsedMs - lastActionTime > 2000) { if (hasLineOfSight(playerPosition, isWalkable)) { state = EntityState.shooting; lastActionTime = elapsedMs; _hasFiredThisCycle = false; } } break; case EntityState.shooting: int timeShooting = elapsedMs - lastActionTime; if (timeShooting < 150) { spriteIndex = 96; } else if (timeShooting < 300) { spriteIndex = 97; if (!_hasFiredThisCycle) { onDamagePlayer(10); _hasFiredThisCycle = true; } } else if (timeShooting < 450) { spriteIndex = 98; } else { state = EntityState.patrolling; lastActionTime = elapsedMs; } break; case EntityState.pain: spriteIndex = 90; if (elapsedMs - lastActionTime > 250) { // Slight delay state = EntityState.patrolling; lastActionTime = elapsedMs; // Reset so they don't immediately shoot } break; case EntityState.dead: if (isDying) { // Use max(0, ...) to prevent negative numbers if currentTime is wonky int timeSinceDeath = math.max(0, elapsedMs - lastActionTime); int deathFrame = timeSinceDeath ~/ 150; if (deathFrame < 4) { spriteIndex = 91 + deathFrame; } else { spriteIndex = 95; // The final corpse isDying = false; // Stop animating } } else { spriteIndex = 95; // Keep showing the corpse } break; default: break; } } }