From 12e2e7e3a83cbbd583e5a8f87d44efa32a9529f8 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sat, 14 Mar 2026 19:37:27 +0100 Subject: [PATCH] Added E1 boss Signed-off-by: Hans Kokx --- .../entities/enemies/bosses/hans_grosse.dart | 154 ++++++++++++++++++ lib/features/entities/entity_registry.dart | 4 + 2 files changed, 158 insertions(+) create mode 100644 lib/features/entities/enemies/bosses/hans_grosse.dart diff --git a/lib/features/entities/enemies/bosses/hans_grosse.dart b/lib/features/entities/enemies/bosses/hans_grosse.dart new file mode 100644 index 0000000..590b289 --- /dev/null +++ b/lib/features/entities/enemies/bosses/hans_grosse.dart @@ -0,0 +1,154 @@ +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); + } +} diff --git a/lib/features/entities/entity_registry.dart b/lib/features/entities/entity_registry.dart index db4a0b2..d86dc36 100644 --- a/lib/features/entities/entity_registry.dart +++ b/lib/features/entities/entity_registry.dart @@ -1,6 +1,7 @@ import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/entities/collectible.dart'; import 'package:wolf_dart/features/entities/decorative.dart'; +import 'package:wolf_dart/features/entities/enemies/bosses/hans_grosse.dart'; import 'package:wolf_dart/features/entities/enemies/dog.dart'; import 'package:wolf_dart/features/entities/enemies/guard.dart'; import 'package:wolf_dart/features/entities/enemies/mutant.dart'; @@ -26,6 +27,9 @@ abstract class EntityRegistry { Mutant.trySpawn, Dog.trySpawn, + // Bosses + HansGrosse.trySpawn, + // Everything else Collectible.trySpawn, Decorative.trySpawn,