@@ -2,7 +2,7 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// Handles identity and difficulty-based logic for enemies in map data.
|
||||
///
|
||||
/// In Wolf3D, each enemy type (Guard, SS, etc.) occupies a block of 36 IDs
|
||||
/// In Wolf3D, each enemy type (Guard, SS, etc.) occupies a block of 48 IDs
|
||||
/// in the map object layer to account for behavior, direction, and difficulty.
|
||||
class EnemyMapData {
|
||||
/// The starting ID for this specific enemy type (e.g., 108 for Guards).
|
||||
@@ -10,28 +10,28 @@ class EnemyMapData {
|
||||
|
||||
const EnemyMapData(this.baseId);
|
||||
|
||||
/// Returns true if the provided [id] belongs to this enemy type's 36-ID block.
|
||||
bool claimsId(int id) => id >= baseId && id < baseId + 36;
|
||||
/// Returns true if the provided [id] belongs to this enemy type's 48-ID block.
|
||||
bool claimsId(int id) => id >= baseId && id < baseId + 48;
|
||||
|
||||
/// Returns true if the ID represents a standing (static) version for the [difficulty].
|
||||
bool isStaticForDifficulty(int id, Difficulty difficulty) {
|
||||
// Standing enemies occupy the first 12 IDs (4 per difficulty level)
|
||||
/// Returns true if the ID represents a patrolling version for the [difficulty].
|
||||
bool isPatrolForDifficulty(int id, Difficulty difficulty) {
|
||||
// Patrolling enemies occupy the first 16 IDs (4 per difficulty level)
|
||||
int start = baseId + (difficulty.level * 4);
|
||||
return id >= start && id < start + 4;
|
||||
}
|
||||
|
||||
/// Returns true if the ID represents a patrolling version for the [difficulty].
|
||||
bool isPatrolForDifficulty(int id, Difficulty difficulty) {
|
||||
// Patrolling enemies occupy the next 12 IDs
|
||||
int start = baseId + 12 + (difficulty.level * 4);
|
||||
/// Returns true if the ID represents a standing (static) version for the [difficulty].
|
||||
bool isStaticForDifficulty(int id, Difficulty difficulty) {
|
||||
// Standing enemies occupy the next 16 IDs
|
||||
int start = baseId + 16 + (difficulty.level * 4);
|
||||
return id >= start && id < start + 4;
|
||||
}
|
||||
|
||||
/// Returns true if the ID represents an "Ambush" version for the [difficulty].
|
||||
/// Ambush enemies wait for the player to enter their line of sight before moving.
|
||||
bool isAmbushForDifficulty(int id, Difficulty difficulty) {
|
||||
// Ambush enemies occupy the final 12 IDs of the block
|
||||
int start = baseId + 24 + (difficulty.level * 4);
|
||||
// Ambush enemies occupy the final 16 IDs of the block
|
||||
int start = baseId + 32 + (difficulty.level * 4);
|
||||
return id >= start && id < start + 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,122 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
|
||||
enum EnemyAnimation { idle, walking, attacking, pain, dying, dead }
|
||||
|
||||
/// A mapping container that defines the sprite index ranges for an enemy's life cycle.
|
||||
///
|
||||
/// Wolfenstein 3D enemies use specific ranges within the global sprite list
|
||||
/// to handle state transitions like walking, attacking, or dying.
|
||||
class EnemyAnimationMap {
|
||||
final SpriteFrameRange idle;
|
||||
final SpriteFrameRange walking;
|
||||
final SpriteFrameRange attacking;
|
||||
final SpriteFrameRange pain;
|
||||
final SpriteFrameRange dying;
|
||||
final SpriteFrameRange dead;
|
||||
|
||||
const EnemyAnimationMap({
|
||||
required this.idle,
|
||||
required this.walking,
|
||||
required this.attacking,
|
||||
required this.pain,
|
||||
required this.dying,
|
||||
required this.dead,
|
||||
});
|
||||
|
||||
/// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to.
|
||||
///
|
||||
/// Returns `null` if the index is outside this enemy's defined animation ranges.
|
||||
EnemyAnimation? getAnimation(int spriteIndex) {
|
||||
if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
|
||||
if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
|
||||
if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
|
||||
if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
|
||||
if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
|
||||
if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the [SpriteFrameRange] associated with a specific [animation] state.
|
||||
SpriteFrameRange getRange(EnemyAnimation animation) {
|
||||
return switch (animation) {
|
||||
EnemyAnimation.idle => idle,
|
||||
EnemyAnimation.walking => walking,
|
||||
EnemyAnimation.attacking => attacking,
|
||||
EnemyAnimation.pain => pain,
|
||||
EnemyAnimation.dying => dying,
|
||||
EnemyAnimation.dead => dead,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if any animation range in this map overlaps with any range in [other].
|
||||
bool overlapsWith(EnemyAnimationMap other) {
|
||||
final myRanges = [idle, walking, attacking, pain, dying, dead];
|
||||
final otherRanges = [
|
||||
other.idle,
|
||||
other.walking,
|
||||
other.attacking,
|
||||
other.pain,
|
||||
other.dying,
|
||||
other.dead,
|
||||
];
|
||||
|
||||
for (final myRange in myRanges) {
|
||||
for (final otherRange in otherRanges) {
|
||||
if (myRange.overlaps(otherRange)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Checks if any animation ranges within this specific enemy overlap with each other.
|
||||
///
|
||||
/// Useful for validating that an enemy isn't trying to use the same sprite
|
||||
/// for two different states (e.g., walking and attacking).
|
||||
bool hasInternalOverlaps() {
|
||||
final ranges = [idle, walking, attacking, pain, dying, dead];
|
||||
|
||||
// Compare every unique pair of ranges
|
||||
for (int i = 0; i < ranges.length; i++) {
|
||||
for (int j = i + 1; j < ranges.length; j++) {
|
||||
if (ranges[i].overlaps(ranges[j])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void validateEnemyAnimations() {
|
||||
bool hasErrors = false;
|
||||
|
||||
for (final enemy in EnemyType.values) {
|
||||
// 1. Check for internal overlaps (e.g., Guard walking overlaps Guard attacking)
|
||||
if (enemy.animations.hasInternalOverlaps()) {
|
||||
print(
|
||||
'❌ ERROR: ${enemy.name} has overlapping internal animation states!',
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// 2. Check for external overlaps (e.g., Guard sprites overlap SS sprites)
|
||||
for (final otherEnemy in EnemyType.values) {
|
||||
if (enemy != otherEnemy && enemy.sharesFramesWith(otherEnemy)) {
|
||||
print(
|
||||
'❌ ERROR: ${enemy.name} shares sprite frames with ${otherEnemy.name}!',
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
print(
|
||||
'✅ All enemy animations validated successfully! No overlaps found.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,124 +3,6 @@ import 'dart:math' as math;
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// A mapping container that defines the sprite index ranges for an enemy's life cycle.
|
||||
///
|
||||
/// Wolfenstein 3D enemies use specific ranges within the global sprite list
|
||||
/// to handle state transitions like walking, attacking, or dying.
|
||||
class EnemyAnimationMap {
|
||||
final SpriteFrameRange idle;
|
||||
final SpriteFrameRange walking;
|
||||
final SpriteFrameRange attacking;
|
||||
final SpriteFrameRange pain;
|
||||
final SpriteFrameRange dying;
|
||||
final SpriteFrameRange dead;
|
||||
|
||||
const EnemyAnimationMap({
|
||||
required this.idle,
|
||||
required this.walking,
|
||||
required this.attacking,
|
||||
required this.pain,
|
||||
required this.dying,
|
||||
required this.dead,
|
||||
});
|
||||
|
||||
/// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to.
|
||||
///
|
||||
/// Returns `null` if the index is outside this enemy's defined animation ranges.
|
||||
EnemyAnimation? getAnimation(int spriteIndex) {
|
||||
if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
|
||||
if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
|
||||
if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
|
||||
if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
|
||||
if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
|
||||
if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the [SpriteFrameRange] associated with a specific [animation] state.
|
||||
SpriteFrameRange getRange(EnemyAnimation animation) {
|
||||
return switch (animation) {
|
||||
EnemyAnimation.idle => idle,
|
||||
EnemyAnimation.walking => walking,
|
||||
EnemyAnimation.attacking => attacking,
|
||||
EnemyAnimation.pain => pain,
|
||||
EnemyAnimation.dying => dying,
|
||||
EnemyAnimation.dead => dead,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if any animation range in this map overlaps with any range in [other].
|
||||
bool overlapsWith(EnemyAnimationMap other) {
|
||||
final myRanges = [idle, walking, attacking, pain, dying, dead];
|
||||
final otherRanges = [
|
||||
other.idle,
|
||||
other.walking,
|
||||
other.attacking,
|
||||
other.pain,
|
||||
other.dying,
|
||||
other.dead,
|
||||
];
|
||||
|
||||
for (final myRange in myRanges) {
|
||||
for (final otherRange in otherRanges) {
|
||||
if (myRange.overlaps(otherRange)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Checks if any animation ranges within this specific enemy overlap with each other.
|
||||
///
|
||||
/// Useful for validating that an enemy isn't trying to use the same sprite
|
||||
/// for two different states (e.g., walking and attacking).
|
||||
bool hasInternalOverlaps() {
|
||||
final ranges = [idle, walking, attacking, pain, dying, dead];
|
||||
|
||||
// Compare every unique pair of ranges
|
||||
for (int i = 0; i < ranges.length; i++) {
|
||||
for (int j = i + 1; j < ranges.length; j++) {
|
||||
if (ranges[i].overlaps(ranges[j])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void validateEnemyAnimations() {
|
||||
bool hasErrors = false;
|
||||
|
||||
for (final enemy in EnemyType.values) {
|
||||
// 1. Check for internal overlaps (e.g., Guard walking overlaps Guard attacking)
|
||||
if (enemy.animations.hasInternalOverlaps()) {
|
||||
print(
|
||||
'❌ ERROR: ${enemy.name} has overlapping internal animation states!',
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// 2. Check for external overlaps (e.g., Guard sprites overlap SS sprites)
|
||||
for (final otherEnemy in EnemyType.values) {
|
||||
if (enemy != otherEnemy && enemy.sharesFramesWith(otherEnemy)) {
|
||||
print(
|
||||
'❌ ERROR: ${enemy.name} shares sprite frames with ${otherEnemy.name}!',
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
print(
|
||||
'✅ All enemy animations validated successfully! No overlaps found.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the primary enemy types and their behavioral metadata.
|
||||
///
|
||||
/// This enum maps level object IDs to their corresponding sprite ranges and
|
||||
@@ -211,9 +93,12 @@ enum EnemyType {
|
||||
|
||||
/// Identifies an [EnemyType] based on a tile ID from the map's object layer.
|
||||
static EnemyType? fromMapId(int id) {
|
||||
for (final type in EnemyType.values) {
|
||||
if (type.mapData.claimsId(id)) return type;
|
||||
}
|
||||
// Each enemy occupies exactly 48 IDs across 3 states.
|
||||
if (id >= 108 && id <= 155) return EnemyType.guard;
|
||||
if (id >= 156 && id <= 203) return EnemyType.dog;
|
||||
if (id >= 204 && id <= 251) return EnemyType.ss;
|
||||
if (id >= 252 && id <= 299) return EnemyType.mutant;
|
||||
if (id >= 300 && id <= 347) return EnemyType.officer;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user