Handle exit elevators and secret levels
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -66,6 +66,10 @@ abstract class MapObject {
|
|||||||
static const int secretExitTrigger = 99;
|
static const int secretExitTrigger = 99;
|
||||||
static const int normalExitTrigger = 100;
|
static const int normalExitTrigger = 100;
|
||||||
|
|
||||||
|
// --- Wall Textures (From VSWAP/MAPHEAD) ---
|
||||||
|
static const int normalElevatorSwitch = 21;
|
||||||
|
static const int secretElevatorSwitch = 41;
|
||||||
|
|
||||||
// Bosses (Shared between WL1 and WL6)
|
// Bosses (Shared between WL1 and WL6)
|
||||||
static const int bossHansGrosse = 214;
|
static const int bossHansGrosse = 214;
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
|
|
||||||
double damageFlashOpacity = 0.0;
|
double damageFlashOpacity = 0.0;
|
||||||
|
|
||||||
late int _currentMapIndex;
|
late int _currentEpisodeIndex;
|
||||||
late WolfLevel _currentLevel;
|
late int _currentLevelIndex;
|
||||||
|
int? _returnLevelIndex;
|
||||||
|
|
||||||
List<Entity> entities = [];
|
List<Entity> entities = [];
|
||||||
|
|
||||||
@@ -65,52 +66,47 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
_initGame();
|
_initGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadLevel(int mapIndex) {
|
Future<void> _initGame() async {
|
||||||
// Grab the specific level from the singleton
|
// 1. Setup our starting indices
|
||||||
_currentLevel = Wolf3d.I.levels[mapIndex];
|
_currentEpisodeIndex = widget.startingEpisode;
|
||||||
|
_currentLevelIndex = 0;
|
||||||
|
|
||||||
// Play the exact track id Software intended for this level!
|
// 2. Load the first floor!
|
||||||
Wolf3d.I.audio.playLevelMusic(_currentLevel);
|
_loadLevel();
|
||||||
|
|
||||||
// TODO: Initialize player position, spawn enemies based on difficulty, etc.
|
_gameLoop = createTicker(_tick)..start();
|
||||||
debugPrint("Loaded Level: ${_currentLevel.name}");
|
_focusNode.requestFocus();
|
||||||
}
|
|
||||||
|
|
||||||
void _onLevelCompleted() {
|
|
||||||
Wolf3d.I.audio.stopMusic();
|
|
||||||
// When the player hits the elevator switch, advance the map
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentMapIndex++;
|
_isLoading = false;
|
||||||
|
|
||||||
// Check if they beat the episode (each episode is 10 levels)
|
|
||||||
int maxLevelForEpisode = (widget.startingEpisode * 10) + 9;
|
|
||||||
|
|
||||||
if (_currentMapIndex > maxLevelForEpisode) {
|
|
||||||
// TODO: Handle episode completion (show victory screen, return to menu)
|
|
||||||
debugPrint("Episode Completed!");
|
|
||||||
} else {
|
|
||||||
_loadLevel(_currentMapIndex);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initGame() async {
|
void _loadLevel() {
|
||||||
// 1. Calculate the starting index
|
// 1. Clean up the previous level's state
|
||||||
_currentMapIndex = widget.startingEpisode * 10;
|
entities.clear();
|
||||||
|
damageFlashOpacity = 0.0;
|
||||||
|
|
||||||
// 2. Load the initial level data
|
// 2. Grab the exact level from our new Episode hierarchy
|
||||||
_loadLevel(_currentMapIndex);
|
final episode = widget.data.episodes[_currentEpisodeIndex];
|
||||||
|
activeLevel = episode.levels[_currentLevelIndex];
|
||||||
|
|
||||||
// Get the first level out of the data class
|
// 3. DEEP COPY the wall grid! If we don't do this, destroying walls/doors
|
||||||
activeLevel = widget.data.levels.first;
|
// will permanently corrupt the map data in the Wolf3d singleton.
|
||||||
|
currentLevel = List.generate(
|
||||||
// Set up your grids directly from the active level
|
64,
|
||||||
currentLevel = activeLevel.wallGrid;
|
(y) => List.from(activeLevel.wallGrid[y]),
|
||||||
|
);
|
||||||
final Level objectLevel = activeLevel.objectGrid;
|
final Level objectLevel = activeLevel.objectGrid;
|
||||||
|
|
||||||
|
// 4. Initialize Managers
|
||||||
doorManager.initDoors(currentLevel);
|
doorManager.initDoors(currentLevel);
|
||||||
pushwallManager.initPushwalls(currentLevel, objectLevel);
|
pushwallManager.initPushwalls(currentLevel, objectLevel);
|
||||||
|
|
||||||
|
// 5. Play Music
|
||||||
|
Wolf3d.I.audio.playLevelMusic(activeLevel);
|
||||||
|
|
||||||
|
// 6. Spawn Player and Entities
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
int objId = objectLevel[y][x];
|
int objId = objectLevel[y][x];
|
||||||
@@ -119,25 +115,17 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
|
|
||||||
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
|
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
|
||||||
double spawnAngle = 0.0;
|
double spawnAngle = 0.0;
|
||||||
switch (objId) {
|
if (objId == MapObject.playerNorth) {
|
||||||
case MapObject.playerNorth:
|
|
||||||
spawnAngle = 3 * math.pi / 2;
|
spawnAngle = 3 * math.pi / 2;
|
||||||
break;
|
} else if (objId == MapObject.playerEast) {
|
||||||
case MapObject.playerEast:
|
|
||||||
spawnAngle = 0.0;
|
spawnAngle = 0.0;
|
||||||
break;
|
} else if (objId == MapObject.playerSouth) {
|
||||||
case MapObject.playerSouth:
|
|
||||||
spawnAngle = math.pi / 2;
|
spawnAngle = math.pi / 2;
|
||||||
break;
|
} else if (objId == MapObject.playerWest) {
|
||||||
case MapObject.playerWest:
|
|
||||||
spawnAngle = math.pi;
|
spawnAngle = math.pi;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
player = Player(
|
|
||||||
x: x + 0.5,
|
player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle);
|
||||||
y: y + 0.5,
|
|
||||||
angle: spawnAngle,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
Entity? newEntity = EntityRegistry.spawn(
|
Entity? newEntity = EntityRegistry.spawn(
|
||||||
objId,
|
objId,
|
||||||
@@ -147,31 +135,54 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
widget.data.sprites.length,
|
widget.data.sprites.length,
|
||||||
isSharewareMode: widget.data.version == GameVersion.shareware,
|
isSharewareMode: widget.data.version == GameVersion.shareware,
|
||||||
);
|
);
|
||||||
|
if (newEntity != null) entities.add(newEntity);
|
||||||
if (newEntity != null) {
|
|
||||||
entities.add(newEntity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7. Clear non-solid blocks from the collision grid
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
int id = currentLevel[y][x];
|
int id = currentLevel[y][x];
|
||||||
if ((id >= 1 && id <= 63) || (id >= 90 && id <= 101)) {
|
if (!((id >= 1 && id <= 63) || (id >= 90 && id <= 101))) {
|
||||||
// Leave walls and doors solid
|
|
||||||
} else {
|
|
||||||
currentLevel[y][x] = 0;
|
currentLevel[y][x] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_bumpPlayerIfStuck();
|
_bumpPlayerIfStuck();
|
||||||
_gameLoop = createTicker(_tick)..start();
|
debugPrint("Loaded Floor: ${_currentLevelIndex + 1} - ${activeLevel.name}");
|
||||||
_focusNode.requestFocus();
|
}
|
||||||
|
|
||||||
|
void _onLevelCompleted({bool isSecretExit = false}) {
|
||||||
|
Wolf3d.I.audio.stopMusic();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
final currentEpisode = widget.data.episodes[_currentEpisodeIndex];
|
||||||
|
|
||||||
|
if (isSecretExit) {
|
||||||
|
// Save the next normal map index so we can return to it later
|
||||||
|
_returnLevelIndex = _currentLevelIndex + 1;
|
||||||
|
_currentLevelIndex = 9; // Jump to the secret map
|
||||||
|
debugPrint("Found the Secret Exit!");
|
||||||
|
} else {
|
||||||
|
// Are we currently ON the secret map, and need to return?
|
||||||
|
if (_currentLevelIndex == 9 && _returnLevelIndex != null) {
|
||||||
|
_currentLevelIndex = _returnLevelIndex!;
|
||||||
|
_returnLevelIndex = null;
|
||||||
|
} else {
|
||||||
|
_currentLevelIndex++; // Normal progression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did we just beat the last map in the episode (Map 9) or the secret map (Map 10)?
|
||||||
|
if (_currentLevelIndex >= currentEpisode.levels.length ||
|
||||||
|
_currentLevelIndex > 9) {
|
||||||
|
debugPrint("Episode Completed! You win!");
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
_loadLevel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,11 +310,37 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inputManager.isInteracting) {
|
if (inputManager.isInteracting) {
|
||||||
doorManager.handleInteraction(
|
// 1. Calculate the tile exactly 1 block in front of the player
|
||||||
player.x,
|
int targetX = (player.x + math.cos(player.angle)).toInt();
|
||||||
player.y,
|
int targetY = (player.y + math.sin(player.angle)).toInt();
|
||||||
player.angle,
|
|
||||||
);
|
// Ensure we don't check outside the map bounds
|
||||||
|
if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) {
|
||||||
|
// 2. Check the WALL grid for the physical switch texture
|
||||||
|
int wallId = currentLevel[targetY][targetX];
|
||||||
|
if (wallId == MapObject.normalElevatorSwitch) {
|
||||||
|
// Player hit the switch!
|
||||||
|
_onLevelCompleted(isSecretExit: false);
|
||||||
|
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
||||||
|
} else if (wallId == MapObject.secretElevatorSwitch) {
|
||||||
|
_onLevelCompleted(isSecretExit: true);
|
||||||
|
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check the OBJECT grid for invisible floor triggers
|
||||||
|
// (Some custom maps use these instead of wall switches)
|
||||||
|
int objId = activeLevel.objectGrid[targetY][targetX];
|
||||||
|
if (objId == MapObject.normalExitTrigger) {
|
||||||
|
_onLevelCompleted(isSecretExit: false);
|
||||||
|
return (movement: movement, dAngle: dAngle);
|
||||||
|
} else if (objId == MapObject.secretExitTrigger) {
|
||||||
|
_onLevelCompleted(isSecretExit: true);
|
||||||
|
return (movement: movement, dAngle: dAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. If it wasn't an elevator, try opening a door or pushing a wall
|
||||||
|
doorManager.handleInteraction(player.x, player.y, player.angle);
|
||||||
pushwallManager.handleInteraction(
|
pushwallManager.handleInteraction(
|
||||||
player.x,
|
player.x,
|
||||||
player.y,
|
player.y,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_dart/features/screens/difficulty_screen.dart';
|
import 'package:wolf_dart/features/screens/difficulty_screen.dart';
|
||||||
import 'package:wolf_dart/wolf_3d.dart';
|
import 'package:wolf_dart/wolf_3d.dart';
|
||||||
|
|
||||||
@@ -10,15 +11,6 @@ class EpisodeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EpisodeScreenState extends State<EpisodeScreen> {
|
class _EpisodeScreenState extends State<EpisodeScreen> {
|
||||||
final List<String> _episodeNames = [
|
|
||||||
"Episode 1\nEscape from Wolfenstein",
|
|
||||||
"Episode 2\nOperation: Eisenfaust",
|
|
||||||
"Episode 3\nDie, Fuhrer, Die!",
|
|
||||||
"Episode 4\nA Dark Secret",
|
|
||||||
"Episode 5\nTrail of the Madman",
|
|
||||||
"Episode 6\nConfrontation",
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -36,7 +28,7 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final int numberOfEpisodes = Wolf3d.I.activeGame.numberOfEpisodes;
|
final List<Episode> episodes = Wolf3d.I.activeGame.episodes;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -56,8 +48,9 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
|
|||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: numberOfEpisodes,
|
itemCount: episodes.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
final Episode episode = episodes[index];
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
@@ -74,7 +67,7 @@ class _EpisodeScreenState extends State<EpisodeScreen> {
|
|||||||
),
|
),
|
||||||
onPressed: () => _selectEpisode(index),
|
onPressed: () => _selectEpisode(index),
|
||||||
child: Text(
|
child: Text(
|
||||||
_episodeNames[index],
|
episode.name,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 18),
|
style: const TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class Wolf3d {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience getters for the active game's assets
|
// Convenience getters for the active game's assets
|
||||||
List<WolfLevel> get levels => activeGame.levels;
|
List<WolfLevel> get levels => activeGame.episodes[activeEpisode].levels;
|
||||||
List<Sprite> get walls => activeGame.walls;
|
List<Sprite> get walls => activeGame.walls;
|
||||||
List<Sprite> get sprites => activeGame.sprites;
|
List<Sprite> get sprites => activeGame.sprites;
|
||||||
List<PcmSound> get sounds => activeGame.sounds;
|
List<PcmSound> get sounds => activeGame.sounds;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ abstract class WLParser {
|
|||||||
walls: parseWalls(vswap),
|
walls: parseWalls(vswap),
|
||||||
sprites: parseSprites(vswap),
|
sprites: parseSprites(vswap),
|
||||||
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
||||||
levels: parseMaps(mapHead, gameMaps, isShareware: isShareware),
|
episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware),
|
||||||
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
|
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
|
||||||
adLibSounds: audio.adLib,
|
adLibSounds: audio.adLib,
|
||||||
music: audio.music,
|
music: audio.music,
|
||||||
@@ -236,22 +236,39 @@ abstract class WLParser {
|
|||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Episode Names (From the original C Executable) ---
|
||||||
|
static const List<String> _sharewareEpisodeNames = [
|
||||||
|
"Episode 1\nEscape from Wolfenstein",
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<String> _retailEpisodeNames = [
|
||||||
|
"Episode 1\nEscape from Wolfenstein",
|
||||||
|
"Episode 2\nOperation: Eisenfaust",
|
||||||
|
"Episode 3\nDie, Fuhrer, Die!",
|
||||||
|
"Episode 4\nA Dark Secret",
|
||||||
|
"Episode 5\nTrail of the Madman",
|
||||||
|
"Episode 6\nConfrontation",
|
||||||
|
];
|
||||||
|
|
||||||
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
|
/// Parses MAPHEAD and GAMEMAPS to extract the raw level data.
|
||||||
static List<WolfLevel> parseMaps(
|
static List<Episode> parseEpisodes(
|
||||||
ByteData mapHead,
|
ByteData mapHead,
|
||||||
ByteData gameMaps, {
|
ByteData gameMaps, {
|
||||||
bool isShareware = true,
|
bool isShareware = true,
|
||||||
}) {
|
}) {
|
||||||
List<WolfLevel> levels = [];
|
List<WolfLevel> allLevels = [];
|
||||||
|
|
||||||
// 1. Select the correct map based on the version
|
|
||||||
final activeMusicMap = isShareware ? _sharewareMusicMap : _retailMusicMap;
|
|
||||||
|
|
||||||
int rlewTag = mapHead.getUint16(0, Endian.little);
|
int rlewTag = mapHead.getUint16(0, Endian.little);
|
||||||
|
|
||||||
|
// Select the correct music map based on the version
|
||||||
|
final activeMusicMap = isShareware ? _sharewareMusicMap : _retailMusicMap;
|
||||||
|
final episodeNames = isShareware
|
||||||
|
? _sharewareEpisodeNames
|
||||||
|
: _retailEpisodeNames;
|
||||||
|
|
||||||
|
// The game allows for up to 100 maps per file
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
int mapOffset = mapHead.getUint32(2 + (i * 4), Endian.little);
|
int mapOffset = mapHead.getUint32(2 + (i * 4), Endian.little);
|
||||||
if (mapOffset == 0) continue;
|
if (mapOffset == 0) continue; // Empty map slot
|
||||||
|
|
||||||
int plane0Offset = gameMaps.getUint32(mapOffset + 0, Endian.little);
|
int plane0Offset = gameMaps.getUint32(mapOffset + 0, Endian.little);
|
||||||
int plane1Offset = gameMaps.getUint32(mapOffset + 4, Endian.little);
|
int plane1Offset = gameMaps.getUint32(mapOffset + 4, Endian.little);
|
||||||
@@ -259,13 +276,15 @@ abstract class WLParser {
|
|||||||
int plane0Length = gameMaps.getUint16(mapOffset + 12, Endian.little);
|
int plane0Length = gameMaps.getUint16(mapOffset + 12, Endian.little);
|
||||||
int plane1Length = gameMaps.getUint16(mapOffset + 14, Endian.little);
|
int plane1Length = gameMaps.getUint16(mapOffset + 14, Endian.little);
|
||||||
|
|
||||||
|
// --- EXTRACT ACTUAL GAME DATA NAME ---
|
||||||
|
// The name is exactly 16 bytes long, starting at offset 22
|
||||||
List<int> nameBytes = [];
|
List<int> nameBytes = [];
|
||||||
for (int n = 0; n < 16; n++) {
|
for (int n = 0; n < 16; n++) {
|
||||||
int charCode = gameMaps.getUint8(mapOffset + 22 + n);
|
int charCode = gameMaps.getUint8(mapOffset + 22 + n);
|
||||||
if (charCode == 0) break;
|
if (charCode == 0) break; // Stop at the null-terminator
|
||||||
nameBytes.add(charCode);
|
nameBytes.add(charCode);
|
||||||
}
|
}
|
||||||
String name = ascii.decode(nameBytes);
|
String parsedName = ascii.decode(nameBytes);
|
||||||
|
|
||||||
// --- DECOMPRESS PLANES ---
|
// --- DECOMPRESS PLANES ---
|
||||||
final compressedWallData = gameMaps.buffer.asUint8List(
|
final compressedWallData = gameMaps.buffer.asUint8List(
|
||||||
@@ -282,7 +301,7 @@ abstract class WLParser {
|
|||||||
Uint16List carmackExpandedObjects = _expandCarmack(compressedObjectData);
|
Uint16List carmackExpandedObjects = _expandCarmack(compressedObjectData);
|
||||||
List<int> flatObjectGrid = _expandRlew(carmackExpandedObjects, rlewTag);
|
List<int> flatObjectGrid = _expandRlew(carmackExpandedObjects, rlewTag);
|
||||||
|
|
||||||
// --- BUILD GRIDS ---
|
// --- BUILD 64x64 GRIDS ---
|
||||||
List<List<int>> wallGrid = [];
|
List<List<int>> wallGrid = [];
|
||||||
List<List<int>> objectGrid = [];
|
List<List<int>> objectGrid = [];
|
||||||
|
|
||||||
@@ -297,17 +316,14 @@ abstract class WLParser {
|
|||||||
objectGrid.add(objectRow);
|
objectGrid.add(objectRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine music track index.
|
// --- ASSIGN MUSIC ---
|
||||||
// We use 'i' because it represents the absolute map slot (e.g. Map 12).
|
|
||||||
// If a map exists past the bounds of our lookup table (custom maps),
|
|
||||||
// we wrap around safely.
|
|
||||||
int trackIndex = (i < activeMusicMap.length)
|
int trackIndex = (i < activeMusicMap.length)
|
||||||
? activeMusicMap[i]
|
? activeMusicMap[i]
|
||||||
: activeMusicMap[i % activeMusicMap.length];
|
: activeMusicMap[i % activeMusicMap.length];
|
||||||
|
|
||||||
levels.add(
|
allLevels.add(
|
||||||
WolfLevel(
|
WolfLevel(
|
||||||
name: name,
|
name: parsedName,
|
||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: trackIndex,
|
musicIndex: trackIndex,
|
||||||
@@ -315,7 +331,35 @@ abstract class WLParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return levels;
|
// 2. Group the parsed levels into Episodes!
|
||||||
|
List<Episode> episodes = [];
|
||||||
|
|
||||||
|
// Calculate how many episodes we need (10 levels per episode)
|
||||||
|
int totalEpisodes = (allLevels.length / 10).ceil();
|
||||||
|
|
||||||
|
for (int i = 0; i < totalEpisodes; i++) {
|
||||||
|
int startIndex = i * 10;
|
||||||
|
int endIndex = startIndex + 10;
|
||||||
|
|
||||||
|
// Safety clamp for incomplete episodes at the end of the file
|
||||||
|
if (endIndex > allLevels.length) {
|
||||||
|
endIndex = allLevels.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we run out of hardcoded id Software names, generate a custom one!
|
||||||
|
String epName = (i < episodeNames.length)
|
||||||
|
? episodeNames[i]
|
||||||
|
: "Episode ${i + 1}\nCustom Maps";
|
||||||
|
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
name: epName,
|
||||||
|
levels: allLevels.sublist(startIndex, endIndex),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts AdLib sounds and IMF music tracks from the audio files.
|
/// Extracts AdLib sounds and IMF music tracks from the audio files.
|
||||||
|
|||||||
8
packages/wolf_3d_data_types/lib/src/episode.dart
Normal file
8
packages/wolf_3d_data_types/lib/src/episode.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
class Episode {
|
||||||
|
final String name;
|
||||||
|
final List<WolfLevel> levels;
|
||||||
|
|
||||||
|
const Episode({required this.name, required this.levels});
|
||||||
|
}
|
||||||
@@ -7,13 +7,8 @@ class WolfensteinData {
|
|||||||
final List<PcmSound> sounds;
|
final List<PcmSound> sounds;
|
||||||
final List<AdLibSound> adLibSounds;
|
final List<AdLibSound> adLibSounds;
|
||||||
final List<ImfMusic> music;
|
final List<ImfMusic> music;
|
||||||
final List<WolfLevel> levels;
|
|
||||||
final List<VgaImage> vgaImages;
|
final List<VgaImage> vgaImages;
|
||||||
|
final List<Episode> episodes;
|
||||||
// --- Derived Properties ---
|
|
||||||
/// Calculates the number of available episodes based on the loaded levels.
|
|
||||||
/// (Each episode consists of exactly 10 levels).
|
|
||||||
int get numberOfEpisodes => (levels.length / 10).floor().clamp(1, 6);
|
|
||||||
|
|
||||||
const WolfensteinData({
|
const WolfensteinData({
|
||||||
required this.version,
|
required this.version,
|
||||||
@@ -22,7 +17,7 @@ class WolfensteinData {
|
|||||||
required this.sounds,
|
required this.sounds,
|
||||||
required this.adLibSounds,
|
required this.adLibSounds,
|
||||||
required this.music,
|
required this.music,
|
||||||
required this.levels,
|
|
||||||
required this.vgaImages,
|
required this.vgaImages,
|
||||||
|
required this.episodes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
/// More dartdocs go here.
|
/// More dartdocs go here.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'src/episode.dart' show Episode;
|
||||||
export 'src/game_file.dart' show GameFile;
|
export 'src/game_file.dart' show GameFile;
|
||||||
export 'src/game_version.dart' show GameVersion;
|
export 'src/game_version.dart' show GameVersion;
|
||||||
export 'src/image.dart' show VgaImage;
|
export 'src/image.dart' show VgaImage;
|
||||||
|
|||||||
Reference in New Issue
Block a user