feat: Implement save and restore functionality for game session state, including player and entity states

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 14:37:58 +01:00
parent 7cb3f25c74
commit 1a93b7d4a2
8 changed files with 813 additions and 0 deletions
@@ -0,0 +1,227 @@
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() {
test(
'captureSaveState and restoreSaveState round-trip live session state',
() {
final engine = _buildEngine();
engine.init();
engine.player
..health = 47
..ammo = 33
..score = 1200
..lives = 5
..x = 10.5
..y = 11.5
..angle = 1.25
..hasGoldKey = true
..hasMachineGun = true
..weapons[WeaponType.machineGun] = MachineGun()
..currentWeapon = MachineGun();
engine.currentLevel[8][8] = 0;
engine.currentLevel[5][6] = 98;
final door = engine.doorManager.doors.values.first
..state = DoorState.opening
..offset = 0.42
..openTime = 1337;
final pushwall = engine.pushwallManager.pushwalls.values.first
..dirX = 1
..dirY = 0
..offset = 0.5
..tilesMoved = 1;
engine.pushwallManager.activePushwall = pushwall;
final guard =
EntityRegistry.spawn(
MapObject.guardStart,
12.5,
13.5,
Difficulty.medium,
engine.data.sprites.length,
registry: engine.data.registry,
)!
as Guard
..health = 17
..isAlerted = true
..state = EntityState.attacking
..currentFrame = 2
..lastActionTime = 222;
final droppedAmmo = SmallAmmoCollectible(x: 7.5, y: 9.5)
..spriteIndex = 999 % engine.data.sprites.length;
engine.entities = <Entity>[guard, droppedAmmo];
final snapshot = engine.captureSaveState();
engine.player
..health = 1
..ammo = 0
..score = 0
..x = 2.5
..y = 2.5
..hasGoldKey = false;
engine.currentLevel[8][8] = 55;
door
..state = DoorState.closed
..offset = 0.0
..openTime = 0;
engine.pushwallManager.activePushwall = null;
engine.entities = <Entity>[];
engine.restoreSaveState(snapshot);
expect(engine.currentGameIndex, 0);
expect(engine.currentEpisodeIndex, 0);
expect(engine.currentLevelIndex, 0);
expect(engine.player.health, 47);
expect(engine.player.ammo, 33);
expect(engine.player.score, 1200);
expect(engine.player.lives, 5);
expect(engine.player.x, closeTo(10.5, 0.001));
expect(engine.player.y, closeTo(11.5, 0.001));
expect(engine.player.angle, closeTo(1.25, 0.001));
expect(engine.player.hasGoldKey, isTrue);
expect(engine.player.hasMachineGun, isTrue);
expect(engine.player.currentWeapon.type, WeaponType.machineGun);
expect(engine.currentLevel[8][8], 0);
expect(engine.currentLevel[5][6], 98);
final restoredDoor = engine.doorManager.doors.values.first;
expect(restoredDoor.state, DoorState.opening);
expect(restoredDoor.offset, closeTo(0.42, 0.001));
expect(restoredDoor.openTime, 1337);
expect(engine.pushwallManager.activePushwall, isNotNull);
expect(
engine.pushwallManager.activePushwall!.offset,
closeTo(0.5, 0.001),
);
expect(engine.pushwallManager.activePushwall!.tilesMoved, 1);
expect(engine.pushwallManager.activePushwall!.dirX, 1);
expect(engine.entities, hasLength(2));
expect(engine.entities.first, isA<Guard>());
final restoredGuard = engine.entities.first as Guard;
expect(restoredGuard.health, 17);
expect(restoredGuard.isAlerted, isTrue);
expect(restoredGuard.state, EntityState.attacking);
expect(restoredGuard.currentFrame, 2);
expect(engine.entities.last, isA<SmallAmmoCollectible>());
},
);
}
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 dispose() {}
@override
void playLevelMusic(Music music) {}
@override
void playMenuMusic() {}
@override
void playSoundEffect(SoundEffect effect) {}
@override
void playSoundEffectId(int sfxId) {}
@override
void stopMusic() {}
@override
Future<void> stopAllAudio() async {}
}
WolfEngine _buildEngine() {
final wallGrid = _buildGrid();
final objectGrid = _buildGrid();
_fillBoundaries(wallGrid, 2);
objectGrid[2][2] = MapObject.playerEast;
objectGrid[4][4] = MapObject.pushwallTrigger;
wallGrid[2][3] = 90;
wallGrid[4][4] = 5;
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: 'Level 1',
wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid,
music: Music.level01,
),
],
),
],
),
difficulty: Difficulty.medium,
startingEpisode: 0,
frameBuffer: FrameBuffer(64, 64),
input: _TestInput(),
onGameWon: () {},
engineAudio: _SilentAudio(),
);
}
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 paletteIndex) {
return Sprite(Uint8List.fromList(<int>[paletteIndex]));
}