import 'dart:math' as math; import 'package:wolf_dart/classes/coordinate_2d.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/enemies/enemy.dart'; import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/entities/map_objects.dart'; class HansGrosse extends Enemy { static const double speed = 0.04; static const int _baseSprite = 291; bool _hasFiredThisCycle = false; HansGrosse({ required super.x, required super.y, required super.angle, required super.mapId, required Difficulty difficulty, }) : super( spriteIndex: _baseSprite, state: EntityState.idle, ) { // Boss health scales heavily with difficulty health = switch (difficulty.level) { 0 => 850, 1 => 950, 2 => 1050, _ => 1200, }; damage = 20; // Dual chainguns hit hard! } static HansGrosse? trySpawn( int objId, double x, double y, Difficulty difficulty, ) { if (objId == MapObject.bossHansGrosse) { return HansGrosse( x: x, y: y, angle: MapObject.getAngle(objId), mapId: objId, difficulty: difficulty, ); } return null; } @override void takeDamage(int amount, int currentTime) { if (state == EntityState.dead) return; health -= amount; lastActionTime = currentTime; if (health <= 0) { state = EntityState.dead; isDying = true; } // Note: Bosses do NOT have a pain state! They never flinch. } @override ({Coordinate2D movement, double newAngle}) update({ required int elapsedMs, required Coordinate2D playerPosition, required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, required void Function(int x, int y) tryOpenDoor, }) { Coordinate2D movement = const Coordinate2D(0, 0); // Bosses lack directional sprites, they always look straight at the player double newAngle = position.angleTo(playerPosition); checkWakeUp( elapsedMs: elapsedMs, playerPosition: playerPosition, isWalkable: isWalkable, baseReactionMs: 50, ); double distance = position.distanceTo(playerPosition); switch (state) { case EntityState.idle: spriteIndex = _baseSprite; break; case EntityState.patrolling: if (distance > 1.5) { double moveX = math.cos(newAngle) * speed; double moveY = math.sin(newAngle) * speed; movement = getValidMovement( Coordinate2D(moveX, moveY), isWalkable, tryOpenDoor, ); } int walkFrame = (elapsedMs ~/ 150) % 4; spriteIndex = (_baseSprite + 1) + walkFrame; if (distance < 8.0 && elapsedMs - lastActionTime > 1000) { if (hasLineOfSight(playerPosition, isWalkable)) { state = EntityState.attacking; lastActionTime = elapsedMs; _hasFiredThisCycle = false; } } break; case EntityState.attacking: int timeShooting = elapsedMs - lastActionTime; if (timeShooting < 150) { spriteIndex = _baseSprite + 5; // Aiming } else if (timeShooting < 300) { spriteIndex = _baseSprite + 6; // Firing if (!_hasFiredThisCycle) { onDamagePlayer(damage); _hasFiredThisCycle = true; } } else if (timeShooting < 450) { spriteIndex = _baseSprite + 7; // Recoil } else { state = EntityState.patrolling; lastActionTime = elapsedMs; } break; case EntityState.dead: if (isDying) { int deathFrame = (elapsedMs - lastActionTime) ~/ 150; if (deathFrame < 4) { spriteIndex = (_baseSprite + 8) + deathFrame; } else { spriteIndex = _baseSprite + 11; // Final dead frame isDying = false; } } else { spriteIndex = _baseSprite + 11; } break; default: break; } return (movement: movement, newAngle: newAngle); } }