Files
wolf_dart/lib/features/entities/enemies/bosses/hans_grosse.dart
2026-03-14 19:37:27 +01:00

155 lines
4.1 KiB
Dart

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);
}
}