feat: Implement difficulty scaling for enemy damage and enhance enemy behaviors

- Added `scaleIncomingEnemyDamage` method to `Difficulty` enum for handling damage scaling based on difficulty level.
- Introduced `lives` attribute to `Player` class with methods to manage lives.
- Updated enemy classes (`Dog`, `Guard`, `Mutant`, `Officer`, `SS`, `HansGrosse`) to utilize difficulty settings for health and damage calculations.
- Modified enemy drop logic to reflect new collectible types and behaviors.
- Created tests for verifying enemy drop parity, collectible values, and difficulty damage scaling.
- Adjusted enemy spawn logic to ensure standing enemies remain idle until alerted.
- Enhanced scoring system to reflect updated score values for different enemy types.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 16:02:36 +01:00
parent 8bf0dbd57c
commit 225840f3ee
17 changed files with 644 additions and 63 deletions

View File

@@ -0,0 +1,148 @@
import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
void main() {
group('Canonical enemy drop parity', () {
test('SS drops machine gun if player does not have one', () {
final engine = _buildEngine();
engine.init();
final ss = SS(x: 3.5, y: 2.5, angle: 0, mapId: MapObject.ssStart);
engine.entities.add(ss);
ss.takeDamage(999, engine.timeAliveMs);
engine.tick(const Duration(milliseconds: 16));
final droppedMachineGun =
engine.entities.whereType<WeaponCollectible>().any(
(item) => item.mapId == MapObject.machineGun,
);
expect(droppedMachineGun, isTrue);
});
test('SS drops small clip if player already has machine gun', () {
final engine = _buildEngine();
engine.init();
engine.player.hasMachineGun = true;
final ss = SS(x: 3.5, y: 2.5, angle: 0, mapId: MapObject.ssStart);
engine.entities.add(ss);
ss.takeDamage(999, engine.timeAliveMs);
engine.tick(const Duration(milliseconds: 16));
expect(engine.entities.whereType<SmallAmmoCollectible>(), hasLength(1));
expect(engine.entities.whereType<WeaponCollectible>(), isEmpty);
});
test('dog does not drop items on death', () {
final engine = _buildEngine();
engine.init();
final dog = Dog(x: 3.5, y: 2.5, angle: 0, mapId: MapObject.dogStart);
engine.entities.add(dog);
dog.takeDamage(999, engine.timeAliveMs);
engine.tick(const Duration(milliseconds: 16));
expect(engine.entities.whereType<Collectible>(), isEmpty);
});
});
}
WolfEngine _buildEngine() {
final wallGrid = _buildGrid();
final objectGrid = _buildGrid();
_fillBoundaries(wallGrid, 2);
objectGrid[2][2] = MapObject.playerEast;
return WolfEngine(
data: WolfensteinData(
version: GameVersion.retail,
dataVersion: DataVersion.unknown,
registry: RetailAssetRegistry(),
walls: [
_solidSprite(1),
_solidSprite(1),
_solidSprite(2),
_solidSprite(2),
],
sprites: List.generate(436, (_) => _solidSprite(255)),
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
adLibSounds: const [],
music: const [],
vgaImages: const [],
episodes: [
Episode(
name: 'Episode 1',
levels: [
WolfLevel(
name: 'Test Level',
wallGrid: wallGrid,
objectGrid: objectGrid,
musicIndex: 0,
),
],
),
],
),
difficulty: Difficulty.hard,
startingEpisode: 0,
frameBuffer: FrameBuffer(64, 64),
input: _TestInput(),
engineAudio: _SilentAudio(),
onGameWon: () {},
);
}
class _TestInput extends Wolf3dInput {
@override
void update() {}
}
class _SilentAudio implements EngineAudio {
@override
WolfensteinData? activeGame;
@override
Future<void> debugSoundTest() async {}
@override
Future<void> init() async {}
@override
void playLevelMusic(WolfLevel level) {}
@override
void playMenuMusic() {}
@override
void playSoundEffect(int sfxId) {}
@override
void stopMusic() {}
@override
void dispose() {}
}
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
void _fillBoundaries(SpriteMap grid, int wallId) {
for (int i = 0; i < 64; i++) {
grid[0][i] = wallId;
grid[63][i] = wallId;
grid[i][0] = wallId;
grid[i][63] = wallId;
}
}
Sprite _solidSprite(int colorIndex) {
return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex)));
}