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:
@@ -1,6 +1,7 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
|
||||
@@ -36,6 +37,7 @@ class Player {
|
||||
|
||||
// Classic face animation (UpdateFace/FACETICS random glance frames)
|
||||
math.Random _faceRng = math.Random(0);
|
||||
int _faceSeed = 0;
|
||||
int _faceFrame = 0;
|
||||
double _faceCountTics = 0.0;
|
||||
int _nextFaceChangeThreshold = 0;
|
||||
@@ -81,6 +83,7 @@ class Player {
|
||||
int get hudFaceFrame => _faceFrame;
|
||||
|
||||
void setHudFaceAnimationSeed(int seed) {
|
||||
_faceSeed = seed;
|
||||
_faceRng = math.Random(seed);
|
||||
_faceFrame = 0;
|
||||
_faceCountTics = 0.0;
|
||||
@@ -93,6 +96,99 @@ class Player {
|
||||
_godModeFaceEnabled = enabled;
|
||||
}
|
||||
|
||||
PlayerSaveState toSaveState() {
|
||||
final Map<WeaponType, WeaponSaveState> weaponStates =
|
||||
<WeaponType, WeaponSaveState>{};
|
||||
for (final MapEntry<WeaponType, Weapon?> entry in weapons.entries) {
|
||||
final Weapon? weapon = entry.value;
|
||||
if (weapon == null) {
|
||||
continue;
|
||||
}
|
||||
weaponStates[entry.key] = weapon.toSaveState();
|
||||
}
|
||||
|
||||
return PlayerSaveState(
|
||||
x: x,
|
||||
y: y,
|
||||
angle: angle,
|
||||
health: health,
|
||||
ammo: ammo,
|
||||
score: score,
|
||||
lives: lives,
|
||||
damageFlash: damageFlash,
|
||||
bonusFlash: bonusFlash,
|
||||
chaingunPickupFaceMsRemaining: _chaingunPickupFaceMsRemaining,
|
||||
mutantDeathFaceActive: _mutantDeathFaceActive,
|
||||
godModeFaceEnabled: _godModeFaceEnabled,
|
||||
faceSeed: _faceSeed,
|
||||
faceFrame: _faceFrame,
|
||||
faceCountTics: _faceCountTics,
|
||||
nextFaceChangeThreshold: _nextFaceChangeThreshold,
|
||||
hasGoldKey: hasGoldKey,
|
||||
hasSilverKey: hasSilverKey,
|
||||
hasMachineGun: hasMachineGun,
|
||||
hasChainGun: hasChainGun,
|
||||
currentWeaponType: currentWeapon.type,
|
||||
weaponStates: weaponStates,
|
||||
switchStateIndex: switchState.index,
|
||||
pendingWeaponType: pendingWeaponType,
|
||||
weaponAnimOffset: weaponAnimOffset,
|
||||
);
|
||||
}
|
||||
|
||||
void restoreFromSaveState(PlayerSaveState saveState) {
|
||||
x = saveState.x;
|
||||
y = saveState.y;
|
||||
angle = saveState.angle;
|
||||
health = saveState.health;
|
||||
ammo = saveState.ammo;
|
||||
score = saveState.score;
|
||||
lives = saveState.lives;
|
||||
damageFlash = saveState.damageFlash;
|
||||
bonusFlash = saveState.bonusFlash;
|
||||
_chaingunPickupFaceMsRemaining = saveState.chaingunPickupFaceMsRemaining;
|
||||
_mutantDeathFaceActive = saveState.mutantDeathFaceActive;
|
||||
_godModeFaceEnabled = saveState.godModeFaceEnabled;
|
||||
_faceSeed = saveState.faceSeed;
|
||||
_faceRng = math.Random(_faceSeed);
|
||||
_faceFrame = saveState.faceFrame;
|
||||
_faceCountTics = saveState.faceCountTics;
|
||||
_nextFaceChangeThreshold = saveState.nextFaceChangeThreshold;
|
||||
hasGoldKey = saveState.hasGoldKey;
|
||||
hasSilverKey = saveState.hasSilverKey;
|
||||
hasMachineGun = saveState.hasMachineGun;
|
||||
hasChainGun = saveState.hasChainGun;
|
||||
switchState = WeaponSwitchState.values[saveState.switchStateIndex];
|
||||
pendingWeaponType = saveState.pendingWeaponType;
|
||||
weaponAnimOffset = saveState.weaponAnimOffset;
|
||||
|
||||
weapons.updateAll((_, _) => null);
|
||||
for (final MapEntry<WeaponType, WeaponSaveState> entry
|
||||
in saveState.weaponStates.entries) {
|
||||
final weapon = _createWeapon(entry.key);
|
||||
weapon.restoreFromSaveState(entry.value);
|
||||
weapons[entry.key] = weapon;
|
||||
}
|
||||
|
||||
currentWeapon =
|
||||
weapons[saveState.currentWeaponType] ??
|
||||
_createWeapon(saveState.currentWeaponType);
|
||||
final WeaponSaveState? currentWeaponState =
|
||||
saveState.weaponStates[saveState.currentWeaponType];
|
||||
if (currentWeaponState != null) {
|
||||
currentWeapon.restoreFromSaveState(currentWeaponState);
|
||||
}
|
||||
}
|
||||
|
||||
Weapon _createWeapon(WeaponType weaponType) {
|
||||
return switch (weaponType) {
|
||||
WeaponType.knife => Knife(),
|
||||
WeaponType.pistol => Pistol(),
|
||||
WeaponType.machineGun => MachineGun(),
|
||||
WeaponType.chainGun => ChainGun(),
|
||||
};
|
||||
}
|
||||
|
||||
// --- General Update ---
|
||||
|
||||
void tick(Duration elapsed) {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
library;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/door.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/weapon/weapon.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
class WeaponSaveState {
|
||||
const WeaponSaveState({
|
||||
required this.type,
|
||||
required this.state,
|
||||
required this.frameIndex,
|
||||
required this.lastFrameTime,
|
||||
required this.triggerReleased,
|
||||
});
|
||||
|
||||
final WeaponType type;
|
||||
final WeaponState state;
|
||||
final int frameIndex;
|
||||
final int lastFrameTime;
|
||||
final bool triggerReleased;
|
||||
}
|
||||
|
||||
class PlayerSaveState {
|
||||
const PlayerSaveState({
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.angle,
|
||||
required this.health,
|
||||
required this.ammo,
|
||||
required this.score,
|
||||
required this.lives,
|
||||
required this.damageFlash,
|
||||
required this.bonusFlash,
|
||||
required this.chaingunPickupFaceMsRemaining,
|
||||
required this.mutantDeathFaceActive,
|
||||
required this.godModeFaceEnabled,
|
||||
required this.faceSeed,
|
||||
required this.faceFrame,
|
||||
required this.faceCountTics,
|
||||
required this.nextFaceChangeThreshold,
|
||||
required this.hasGoldKey,
|
||||
required this.hasSilverKey,
|
||||
required this.hasMachineGun,
|
||||
required this.hasChainGun,
|
||||
required this.currentWeaponType,
|
||||
required this.weaponStates,
|
||||
required this.switchStateIndex,
|
||||
required this.pendingWeaponType,
|
||||
required this.weaponAnimOffset,
|
||||
});
|
||||
|
||||
final double x;
|
||||
final double y;
|
||||
final double angle;
|
||||
final int health;
|
||||
final int ammo;
|
||||
final int score;
|
||||
final int lives;
|
||||
final double damageFlash;
|
||||
final double bonusFlash;
|
||||
final int chaingunPickupFaceMsRemaining;
|
||||
final bool mutantDeathFaceActive;
|
||||
final bool godModeFaceEnabled;
|
||||
final int faceSeed;
|
||||
final int faceFrame;
|
||||
final double faceCountTics;
|
||||
final int nextFaceChangeThreshold;
|
||||
final bool hasGoldKey;
|
||||
final bool hasSilverKey;
|
||||
final bool hasMachineGun;
|
||||
final bool hasChainGun;
|
||||
final WeaponType currentWeaponType;
|
||||
final Map<WeaponType, WeaponSaveState> weaponStates;
|
||||
final int switchStateIndex;
|
||||
final WeaponType? pendingWeaponType;
|
||||
final double weaponAnimOffset;
|
||||
}
|
||||
|
||||
class EntitySaveState {
|
||||
const EntitySaveState({
|
||||
required this.kind,
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.spriteIndex,
|
||||
required this.angle,
|
||||
required this.state,
|
||||
required this.mapId,
|
||||
required this.lastActionTime,
|
||||
this.extraData = const <String, Object?>{},
|
||||
});
|
||||
|
||||
final String kind;
|
||||
final double x;
|
||||
final double y;
|
||||
final int spriteIndex;
|
||||
final double angle;
|
||||
final EntityState state;
|
||||
final int mapId;
|
||||
final int lastActionTime;
|
||||
final Map<String, Object?> extraData;
|
||||
}
|
||||
|
||||
class DoorSaveState {
|
||||
const DoorSaveState({
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.mapId,
|
||||
required this.state,
|
||||
required this.offset,
|
||||
required this.openTime,
|
||||
});
|
||||
|
||||
final int x;
|
||||
final int y;
|
||||
final int mapId;
|
||||
final DoorState state;
|
||||
final double offset;
|
||||
final int openTime;
|
||||
}
|
||||
|
||||
class PushwallSaveState {
|
||||
const PushwallSaveState({
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.mapId,
|
||||
required this.dirX,
|
||||
required this.dirY,
|
||||
required this.offset,
|
||||
required this.tilesMoved,
|
||||
required this.isActive,
|
||||
});
|
||||
|
||||
final int x;
|
||||
final int y;
|
||||
final int mapId;
|
||||
final int dirX;
|
||||
final int dirY;
|
||||
final double offset;
|
||||
final int tilesMoved;
|
||||
final bool isActive;
|
||||
}
|
||||
|
||||
class GameSessionSnapshot {
|
||||
const GameSessionSnapshot({
|
||||
required this.currentGameIndex,
|
||||
required this.currentEpisodeIndex,
|
||||
required this.currentLevelIndex,
|
||||
required this.returnLevelIndex,
|
||||
required this.difficulty,
|
||||
required this.timeAliveMs,
|
||||
required this.lastAcousticAlertTime,
|
||||
required this.isMapOverlayVisible,
|
||||
required this.isMenuOverlayVisible,
|
||||
required this.player,
|
||||
required this.currentLevel,
|
||||
required this.areaGrid,
|
||||
required this.areasByPlayer,
|
||||
required this.entities,
|
||||
required this.doors,
|
||||
required this.pushwalls,
|
||||
});
|
||||
|
||||
final int currentGameIndex;
|
||||
final int currentEpisodeIndex;
|
||||
final int currentLevelIndex;
|
||||
final int? returnLevelIndex;
|
||||
final Difficulty difficulty;
|
||||
final int timeAliveMs;
|
||||
final int lastAcousticAlertTime;
|
||||
final bool isMapOverlayVisible;
|
||||
final bool isMenuOverlayVisible;
|
||||
final PlayerSaveState player;
|
||||
final List<List<int>> currentLevel;
|
||||
final List<List<int>> areaGrid;
|
||||
final List<bool> areasByPlayer;
|
||||
final List<EntitySaveState> entities;
|
||||
final List<DoorSaveState> doors;
|
||||
final List<PushwallSaveState> pushwalls;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.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';
|
||||
@@ -70,6 +71,12 @@ class WolfEngine {
|
||||
List<WolfensteinData> get availableGames =>
|
||||
List.unmodifiable(_availableGames);
|
||||
|
||||
int get currentGameIndex => _currentGameIndex;
|
||||
|
||||
int get currentEpisodeIndex => _currentEpisodeIndex;
|
||||
|
||||
int get currentLevelIndex => _currentLevelIndex;
|
||||
|
||||
int _currentGameIndex = 0;
|
||||
|
||||
/// The currently active game data set.
|
||||
@@ -233,6 +240,125 @@ class WolfEngine {
|
||||
/// Whether the current gameplay session can be resumed from the main menu.
|
||||
bool get canResumeGame => _hasActiveSession;
|
||||
|
||||
GameSessionSnapshot captureSaveState() {
|
||||
if (!_hasActiveSession || difficulty == null) {
|
||||
throw StateError('Cannot capture save state without an active session.');
|
||||
}
|
||||
|
||||
return GameSessionSnapshot(
|
||||
currentGameIndex: _currentGameIndex,
|
||||
currentEpisodeIndex: _currentEpisodeIndex,
|
||||
currentLevelIndex: _currentLevelIndex,
|
||||
returnLevelIndex: _returnLevelIndex,
|
||||
difficulty: difficulty!,
|
||||
timeAliveMs: _timeAliveMs,
|
||||
lastAcousticAlertTime: _lastAcousticAlertTime,
|
||||
isMapOverlayVisible: isMapOverlayVisible,
|
||||
isMenuOverlayVisible: _isMenuOverlayVisible,
|
||||
player: player.toSaveState(),
|
||||
currentLevel: _cloneGrid(currentLevel),
|
||||
areaGrid: _cloneGrid(_areaGrid),
|
||||
areasByPlayer: List<bool>.from(_areasByPlayer),
|
||||
entities: entities.map(_captureEntityState).toList(growable: false),
|
||||
doors: doorManager.doors.values
|
||||
.map(
|
||||
(door) => DoorSaveState(
|
||||
x: door.x,
|
||||
y: door.y,
|
||||
mapId: door.mapId,
|
||||
state: door.state,
|
||||
offset: door.offset,
|
||||
openTime: door.openTime,
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
pushwalls: pushwallManager.pushwalls.values
|
||||
.map(
|
||||
(pushwall) => PushwallSaveState(
|
||||
x: pushwall.x,
|
||||
y: pushwall.y,
|
||||
mapId: pushwall.mapId,
|
||||
dirX: pushwall.dirX,
|
||||
dirY: pushwall.dirY,
|
||||
offset: pushwall.offset,
|
||||
tilesMoved: pushwall.tilesMoved,
|
||||
isActive: identical(pushwallManager.activePushwall, pushwall),
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
void restoreSaveState(GameSessionSnapshot snapshot) {
|
||||
if (snapshot.currentGameIndex < 0 ||
|
||||
snapshot.currentGameIndex >= _availableGames.length) {
|
||||
throw RangeError(
|
||||
'Snapshot game index ${snapshot.currentGameIndex} is out of range.',
|
||||
);
|
||||
}
|
||||
|
||||
_currentGameIndex = snapshot.currentGameIndex;
|
||||
audio.activeGame = data;
|
||||
onGameSelected?.call(data);
|
||||
|
||||
_currentEpisodeIndex = snapshot.currentEpisodeIndex;
|
||||
_currentLevelIndex = snapshot.currentLevelIndex;
|
||||
_returnLevelIndex = snapshot.returnLevelIndex;
|
||||
difficulty = snapshot.difficulty;
|
||||
_timeAliveMs = snapshot.timeAliveMs;
|
||||
_lastAcousticAlertTime = snapshot.lastAcousticAlertTime;
|
||||
isMapOverlayVisible = snapshot.isMapOverlayVisible;
|
||||
_isMenuOverlayVisible = snapshot.isMenuOverlayVisible;
|
||||
_hasActiveSession = true;
|
||||
|
||||
_loadLevel(preservePlayerState: false);
|
||||
|
||||
currentLevel = _cloneGrid(snapshot.currentLevel);
|
||||
_areaGrid = _cloneGrid(snapshot.areaGrid);
|
||||
_areaCount = snapshot.areasByPlayer.length;
|
||||
_areasByPlayer = List<bool>.from(snapshot.areasByPlayer);
|
||||
_lastPatrolTileByEnemy.clear();
|
||||
|
||||
player.restoreFromSaveState(snapshot.player);
|
||||
|
||||
doorManager.doors.clear();
|
||||
for (final doorState in snapshot.doors) {
|
||||
final door = Door(x: doorState.x, y: doorState.y, mapId: doorState.mapId)
|
||||
..state = doorState.state
|
||||
..offset = doorState.offset
|
||||
..openTime = doorState.openTime;
|
||||
doorManager.doors[((door.y & 0xFFFF) << 16) | (door.x & 0xFFFF)] = door;
|
||||
}
|
||||
|
||||
pushwallManager.pushwalls.clear();
|
||||
pushwallManager.activePushwall = null;
|
||||
for (final pushwallState in snapshot.pushwalls) {
|
||||
final pushwall =
|
||||
Pushwall(
|
||||
pushwallState.x,
|
||||
pushwallState.y,
|
||||
pushwallState.mapId,
|
||||
)
|
||||
..dirX = pushwallState.dirX
|
||||
..dirY = pushwallState.dirY
|
||||
..offset = pushwallState.offset
|
||||
..tilesMoved = pushwallState.tilesMoved;
|
||||
pushwallManager.pushwalls['${pushwall.x},${pushwall.y}'] = pushwall;
|
||||
if (pushwallState.isActive) {
|
||||
pushwallManager.activePushwall = pushwall;
|
||||
}
|
||||
}
|
||||
|
||||
entities = snapshot.entities
|
||||
.map(_restoreEntityState)
|
||||
.whereType<Entity>()
|
||||
.toList(growable: true);
|
||||
|
||||
if (_isMenuOverlayVisible) {
|
||||
menuManager.showMainMenu(hasResumableGame: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the shared framebuffer when dimensions change.
|
||||
void setFrameBuffer(int width, int height) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
@@ -1332,6 +1458,79 @@ class WolfEngine {
|
||||
}
|
||||
}
|
||||
|
||||
static SpriteMap _cloneGrid(SpriteMap grid) {
|
||||
return List<List<int>>.generate(
|
||||
grid.length,
|
||||
(int y) => List<int>.from(grid[y]),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
|
||||
EntitySaveState _captureEntityState(Entity entity) {
|
||||
if (entity is Enemy) {
|
||||
return entity.toSaveState();
|
||||
}
|
||||
|
||||
final Map<String, Object?> extraData = <String, Object?>{};
|
||||
if (entity is AmmoCollectible) {
|
||||
extraData['ammoAmount'] = entity.ammoAmount;
|
||||
}
|
||||
|
||||
return EntitySaveState(
|
||||
kind: entity.runtimeType.toString(),
|
||||
x: entity.x,
|
||||
y: entity.y,
|
||||
spriteIndex: entity.spriteIndex,
|
||||
angle: entity.angle,
|
||||
state: entity.state,
|
||||
mapId: entity.mapId,
|
||||
lastActionTime: entity.lastActionTime,
|
||||
extraData: extraData,
|
||||
);
|
||||
}
|
||||
|
||||
Entity? _restoreEntityState(EntitySaveState entityState) {
|
||||
final Entity? entity = switch (entityState.kind) {
|
||||
'SmallAmmoCollectible' => SmallAmmoCollectible(
|
||||
x: entityState.x,
|
||||
y: entityState.y,
|
||||
),
|
||||
'AmmoCollectible' => AmmoCollectible(
|
||||
x: entityState.x,
|
||||
y: entityState.y,
|
||||
ammoAmount: (entityState.extraData['ammoAmount'] as num?)?.toInt() ?? 8,
|
||||
),
|
||||
_ => EntityRegistry.spawn(
|
||||
entityState.mapId,
|
||||
entityState.x,
|
||||
entityState.y,
|
||||
difficulty!,
|
||||
data.sprites.length,
|
||||
isSharewareMode: data.version == GameVersion.shareware,
|
||||
registry: data.registry,
|
||||
),
|
||||
};
|
||||
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entity is Enemy) {
|
||||
entity.restoreFromSaveState(entityState);
|
||||
return entity;
|
||||
}
|
||||
|
||||
entity
|
||||
..x = entityState.x
|
||||
..y = entityState.y
|
||||
..spriteIndex = entityState.spriteIndex
|
||||
..angle = entityState.angle
|
||||
..state = entityState.state
|
||||
..mapId = entityState.mapId
|
||||
..lastActionTime = entityState.lastActionTime;
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// Returns true if a tile is empty or contains a door that is sufficiently open.
|
||||
bool isWalkable(int x, int y) {
|
||||
// 1. Boundary Guard: Prevent range errors by checking if coordinates are on the map
|
||||
|
||||
@@ -34,6 +34,25 @@ class Dog extends Enemy {
|
||||
health = type.hitPointsFor(difficulty);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> exportExtraSaveState() {
|
||||
return <String, Object?>{
|
||||
'dodgeAngleOffset': _dodgeAngleOffset,
|
||||
'dodgeTicTimer': _dodgeTicTimer,
|
||||
'stuckFrames': _stuckFrames,
|
||||
'wasMoving': _wasMoving,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void importExtraSaveState(Map<String, Object?> saveState) {
|
||||
_dodgeAngleOffset =
|
||||
(saveState['dodgeAngleOffset'] as num?)?.toDouble() ?? 0.0;
|
||||
_dodgeTicTimer = (saveState['dodgeTicTimer'] as num?)?.toInt() ?? 0;
|
||||
_stuckFrames = (saveState['stuckFrames'] as num?)?.toInt() ?? 0;
|
||||
_wasMoving = saveState['wasMoving'] as bool? ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
EnemyAnimation animationForState(EntityState state) {
|
||||
return switch (state) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.dart';
|
||||
@@ -146,6 +147,78 @@ abstract class Enemy extends Entity {
|
||||
int _patrolDirX = 0;
|
||||
int _patrolDirY = 0;
|
||||
|
||||
EntitySaveState toSaveState() {
|
||||
return EntitySaveState(
|
||||
kind: runtimeType.toString(),
|
||||
x: x,
|
||||
y: y,
|
||||
spriteIndex: spriteIndex,
|
||||
angle: angle,
|
||||
state: state,
|
||||
mapId: mapId,
|
||||
lastActionTime: lastActionTime,
|
||||
extraData: <String, Object?>{
|
||||
'ticCount': _ticCount,
|
||||
'ticAccumulator': _ticAccumulator,
|
||||
'currentFrame': currentFrame,
|
||||
'health': health,
|
||||
'damage': damage,
|
||||
'isDying': isDying,
|
||||
'hasDroppedItem': hasDroppedItem,
|
||||
'hasPlayedDeathSound': hasPlayedDeathSound,
|
||||
'isAlerted': isAlerted,
|
||||
'reactionTimeMs': reactionTimeMs,
|
||||
'patrolTargetTileX': _patrolTargetTile?.x,
|
||||
'patrolTargetTileY': _patrolTargetTile?.y,
|
||||
'patrolDirX': _patrolDirX,
|
||||
'patrolDirY': _patrolDirY,
|
||||
...exportExtraSaveState(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void restoreFromSaveState(EntitySaveState saveState) {
|
||||
x = saveState.x;
|
||||
y = saveState.y;
|
||||
spriteIndex = saveState.spriteIndex;
|
||||
angle = saveState.angle;
|
||||
state = saveState.state;
|
||||
mapId = saveState.mapId;
|
||||
lastActionTime = saveState.lastActionTime;
|
||||
|
||||
_ticCount = (saveState.extraData['ticCount'] as num?)?.toInt() ?? 0;
|
||||
_ticAccumulator =
|
||||
(saveState.extraData['ticAccumulator'] as num?)?.toDouble() ?? 0.0;
|
||||
currentFrame = (saveState.extraData['currentFrame'] as num?)?.toInt() ?? 0;
|
||||
health = (saveState.extraData['health'] as num?)?.toInt() ?? health;
|
||||
damage = (saveState.extraData['damage'] as num?)?.toInt() ?? damage;
|
||||
isDying = saveState.extraData['isDying'] as bool? ?? false;
|
||||
hasDroppedItem = saveState.extraData['hasDroppedItem'] as bool? ?? false;
|
||||
hasPlayedDeathSound =
|
||||
saveState.extraData['hasPlayedDeathSound'] as bool? ?? false;
|
||||
isAlerted = saveState.extraData['isAlerted'] as bool? ?? false;
|
||||
reactionTimeMs =
|
||||
(saveState.extraData['reactionTimeMs'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final int? patrolTargetTileX =
|
||||
(saveState.extraData['patrolTargetTileX'] as num?)?.toInt();
|
||||
final int? patrolTargetTileY =
|
||||
(saveState.extraData['patrolTargetTileY'] as num?)?.toInt();
|
||||
if (patrolTargetTileX != null && patrolTargetTileY != null) {
|
||||
_patrolTargetTile = (x: patrolTargetTileX, y: patrolTargetTileY);
|
||||
} else {
|
||||
_patrolTargetTile = null;
|
||||
}
|
||||
_patrolDirX = (saveState.extraData['patrolDirX'] as num?)?.toInt() ?? 0;
|
||||
_patrolDirY = (saveState.extraData['patrolDirY'] as num?)?.toInt() ?? 0;
|
||||
|
||||
importExtraSaveState(saveState.extraData);
|
||||
}
|
||||
|
||||
Map<String, Object?> exportExtraSaveState() => const <String, Object?>{};
|
||||
|
||||
void importExtraSaveState(Map<String, Object?> saveState) {}
|
||||
|
||||
/// Processes elapsed time and returns true if the enemy's animation frame has completed.
|
||||
///
|
||||
/// Movement is applied continuously during the frame, but state changes (like deciding
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
@@ -54,6 +55,23 @@ abstract class Weapon {
|
||||
_triggerReleased = true;
|
||||
}
|
||||
|
||||
WeaponSaveState toSaveState() {
|
||||
return WeaponSaveState(
|
||||
type: type,
|
||||
state: state,
|
||||
frameIndex: frameIndex,
|
||||
lastFrameTime: lastFrameTime,
|
||||
triggerReleased: _triggerReleased,
|
||||
);
|
||||
}
|
||||
|
||||
void restoreFromSaveState(WeaponSaveState saveState) {
|
||||
state = saveState.state;
|
||||
frameIndex = saveState.frameIndex;
|
||||
lastFrameTime = saveState.lastFrameTime;
|
||||
_triggerReleased = saveState.triggerReleased;
|
||||
}
|
||||
|
||||
bool fire(int currentTime, {required int currentAmmo}) {
|
||||
if (state == WeaponState.idle && currentAmmo > 0) {
|
||||
if (!isAutomatic && !_triggerReleased) {
|
||||
|
||||
@@ -14,4 +14,5 @@ export 'src/engine/player/player.dart';
|
||||
export 'src/engine/player_locomotion_constants.dart';
|
||||
export 'src/engine/rendering/renderer_settings.dart';
|
||||
export 'src/engine/rendering/renderer_settings_persistence.dart';
|
||||
export 'src/engine/save/game_session_snapshot.dart';
|
||||
export 'src/engine/wolf_3d_engine_base.dart';
|
||||
|
||||
Reference in New Issue
Block a user