digitorum.ru

Как меня найти

Профиль

icq: 4415944

Wolfenstein 3D: Экспортируем карты уровней и объекты на них

C#, Games, Wolfenstein 3D

Wolfenstein 3D (Wolf 3D) — компьютерная игра, родоначальник жанра «шутер от первого лица». Wolfenstein 3D разработана компанией id Software и издана компанией Apogee Software 5 мая 1992 под DOS.

Решил я перепройти эту игру в очередной раз, но не просто так, а открыв все секреты... Но, естественно, бегать и прозванивать все стены мне не очень хочется .

Решение простое - пойти в гугл и найти карты (хардкорный про гейминг во всей красе ). Но не тут то было. Нормальных карт я не нашел, а ломать глаза над картинкой в 300x300 пикселей... Спасибо - не надо.

Вторая итерация простого решения - сделать карты самому... Для этого в гугле ищем как и где хранятся данные об уровнях...

Нам понадобятся 2 файла: GAMEMAPS.WL6 и MAPHEAD.WL6. WL6 - значит в файле хранятся данные о 6-ти актах (говорят есть еще WL1 и WL3).

Алгоритмы декодирования и декомпрессии расписывать, думаю, не стоит. Лучше сразу перейти к их реализации (DecodeMapDataFile и DecompressMapDataFile) на C# (люблю и почитаю этот язык, но кодировать на нем не умею ): 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;

namespace Wolfenstein3DMapGrabber
{
    class Wolfenstein3DMapGrabber
    {

        /*
         * Start
         */
        static void Main(string[] args)
        {
            string ProjectPath = System.IO.Directory.GetCurrentDirectory();
            Int32[] Offsets;

            if ((Offsets = Wolfenstein3DMapGrabber.ParseMapHeadFile(ProjectPath + "/Wolfenstein3DData/MAPHEAD.WL6")).Length > 0)
            {
                Console.WriteLine("MAPHEAD.WL6: Data received (Blocks count = " + Offsets.Length + ")");
                Wolfenstein3DMapGrabber.ParseMapDataFile(ProjectPath + "/Wolfenstein3DData/GAMEMAPS.WL6", Offsets);

            }
            Console.WriteLine("Something done! Press any key...");
            Console.ReadKey();
        }


        /*
         * Прочитать блок заданной длинны из стрима
         */
        private static byte[] ReadBytes(FileStream Stream, int Offset, int Length)
        {
            byte[] BuffBytes = new byte[Length];
            int BytesRead = 0;

            Stream.Seek(Offset, SeekOrigin.Begin);
            if ((BytesRead = Stream.Read(BuffBytes, 0, Length)) < Length)
            {
                Stream.Close();
                throw new Exception("FATAL: IO error (" + Length + " vs. " + BytesRead + ")");
            }
            return BuffBytes;
        }


        /*
         * Декодировать ("декармакизировать") данные по карте (unhuffman)
         * а-ля CAL_CarmackExpand в оригинале
         */
        private static ushort[] DecodeMapDataFile(byte[] Input, short NumberBytesAfterDecoding)
        {
            int WordCounter;
            int InCounter;
            int OutCounter;
            ushort Copy;
            ushort[] Output;
            byte Low;
            byte High;
            int Offset;
            
            WordCounter = NumberBytesAfterDecoding / 2;
            InCounter = 0;
            OutCounter = 0;
            Output = new ushort[WordCounter];

            do
            {
                Low = Input[InCounter++];
                High = Input[InCounter++];
                if (High == 0xA7)
                {
                    // If high byte of value is A7 then:
                    if (Low == 0)
                    {
                        // If low byte is 00 then:
                        //  - high byte of value is A7
                        //  - low  byte of value is the next byte
                        Output[OutCounter++] = (ushort)((High << 8) | Input[InCounter]);
                        ++InCounter;
                        --WordCounter;
                    }
                    else
                    {
                         // - the low  byte is a count (ie. # of integers we want to reuse).
                         // - the next byte is the # of integers we want to move back in order to reuse data we have already decoded.
                        Offset = Input[InCounter];
                        ++InCounter;
                        Copy = (ushort)(OutCounter - Offset);
                        WordCounter -= Low;
                        while (Low-- != 0)
                        {
                            Output[OutCounter++] = Output[Copy++];
                        }
                    }
                }
                else if (High == 0xA8)
                {
                    // If high byte of value is A8 then:
                    if (Low == 0)
                    {
                        // If low byte is 00 then:
                        //  - high byte of value is A8
                        //  - low  byte of value is the next byte
                        Output[OutCounter++] = (ushort)((High << 8) | Input[InCounter]);
                        ++InCounter;
                        --WordCounter;
                    }
                    else
                    {
                        // - the low  byte is a count (ie. # of integers we want to reuse).
                        // - the next integer (2 bytes) is the # of integers
                        //   we want to skip over, starting at the beginning of the buffer being used to hold data we have already decoded 
                        //   (the first integer in the buffer being the # of bytes after decompressing).
                        Offset = (ushort)((Input[InCounter + 1] << 8) | Input[InCounter]);
                        InCounter += 2;
                        Copy = (ushort)(0 + Offset - 1);
                        WordCounter -= Low;
                        while (Low-- != 0)
                        {
                            Output[OutCounter++] = Output[Copy++];
                        }
                    }
                }
                else
                {
                    // If high byte is not A7 or A8, then store the value as is.
                    Output[OutCounter++] = (ushort)((High << 8) | Low);
                    --WordCounter;
                }
            } while (WordCounter != 0);
            return Output;
        }


        /*
         * Распаковать данные (unRLEW)
         */
        private static ushort[] DecompressMapDataFile(ushort[] Data, short NumberOfBytesAfterDecompressing)
        {
            int WordsCount = NumberOfBytesAfterDecompressing / 2;
            ushort[] Output = new ushort[WordsCount];
            uint InCounter = 0;
            uint OutCounter = 0;
            ushort Number = 0;
            ushort Repeat;
            
            do
            {
                Number = Data[InCounter++];
                if (Number == 0xABCD)
                {
                    // If value == ABCD
                    //  - the next integer is # of repetitions
                    //  - the next integer is value to repeat
                    Repeat = Data[InCounter++];
                    Number = Data[InCounter++];
                    WordsCount -= Repeat;
                    while (Repeat-- > 0) 
                    {
                        Output[OutCounter++] = Number;
                    }
                }
                else 
                {
                    // If value is not ABCD then store the value as is.
                    Output[OutCounter++] = Number;
                    WordsCount--;
                }
            } while (WordsCount > 0);

            return Output;
        }


        /*
         * Разбираем GAMEMAPS файл
         */
        private static bool ParseMapDataFile(string FileName, Int32[] Offsets)
        {
            int Length = 0;
            FileStream Stream;
            byte[] BuffBytes;
            Int32 DataShift = 0;
            Int32 DataLength = 0;
            short NumberBytesAfterDecoding;
            short NumberOfBytesAfterDecompressing;
            ushort[] Data;
            string ExportData;
            string LevelName;
            int[] Shifts;
            string FileNameSuffix;

            Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read);
            Length = (int)Stream.Length;
            Shifts = new int[] { 0, 1 };
            // забираем данные по заголовкам карт
            for (int i = 0; i < Offsets.Length; ++i)
            {
                if (Offsets[i] > 0)
                {
                    for (int k = 0; k < Shifts.Length; ++k)
                    {
                        if (Shifts[k] == 0)
                        {
                            FileNameSuffix = "-Map";
                        }
                        else 
                        {
                            FileNameSuffix = "-Objects";
                        }

                        /*
                        Level
                        Header  Data
                        Offset  Type  Description
                        ------  ----  -------------------------------
                        0000   long  File offset of Map_Block
                        0004   long  File offset of Object_Block
                        0008   long  File offset of Unknown_Block
                        000C   int   File size   of Map_Block
                        000E   int   File size   of Object_Block
                        0010   int   File size   of Unknown_Block
                        0012   int   Horizontal map size (# of squares)
                        0014   int   Vertical   map size (# of squares)
                        0016   char  Level name (ascii) (null terminated)
                        0026   char  "!ID!" (4 bytes)
                        */

                        DataShift = BitConverter.ToInt32(Wolfenstein3DMapGrabber.ReadBytes(Stream, Offsets[i] + Shifts[k] * 4, 4), 0);
                        DataLength = BitConverter.ToInt16(Wolfenstein3DMapGrabber.ReadBytes(Stream, Offsets[i] + 12 + Shifts[k] * 2, 2), 0);

                        /*
                        Block  Data
                        Offset Type Description
                        ------ ---- --------------------------------------------
                        0000  int  # of data bytes after decoding (count includes the two bytes at Block Offset 0002).
                        0002  int  # of data bytes after decompresssing.
                        0004  int  Data bytes. Values are stored as 2 byte integers (low byte first).
                        */

                        NumberBytesAfterDecoding = BitConverter.ToInt16(Wolfenstein3DMapGrabber.ReadBytes(Stream, DataShift, 2), 0);
                        NumberOfBytesAfterDecompressing = BitConverter.ToInt16(Wolfenstein3DMapGrabber.ReadBytes(Stream, DataShift + 2, 2), 0);
                        BuffBytes = Wolfenstein3DMapGrabber.ReadBytes(Stream, DataShift + 4, DataLength);
                        LevelName = System.Text.Encoding.ASCII.GetString(Wolfenstein3DMapGrabber.ReadBytes(Stream, Offsets[i] + 22, 16)).Replace("\0", "");

                        Console.WriteLine("GAMEMAPS.WL6:");
                        Console.WriteLine("     LEVEL " + i);
                        Console.WriteLine("     DATA OFFSET: " + Offsets[i]);
                        Console.WriteLine("     NAME: " + LevelName);
                        Console.WriteLine("     NUMBER OF BYTES AFTER DECODING: " + NumberBytesAfterDecoding);
                        Console.WriteLine("     NUMBER OF BYTES AFTER DECOMPRESSING: " + NumberOfBytesAfterDecompressing);
                        Console.WriteLine("     BUFFER LENGTH: " + BuffBytes.Length);

                        // Pass1 (decoding):
                        Data = Wolfenstein3DMapGrabber.DecodeMapDataFile(BuffBytes, NumberBytesAfterDecoding);
                        // Pass2 (decompressing):
                        Data = Wolfenstein3DMapGrabber.DecompressMapDataFile(Data, NumberOfBytesAfterDecompressing);
                        // Export decompressing data
                        ExportData = "";
                        for (int j = 0; j < Data.Length; j++)
                        {
                            if (ExportData != "")
                            {
                                ExportData += ",";
                            }
                            ExportData += Data[j].ToString();
                        }
                        System.IO.StreamWriter OutputFile = new System.IO.StreamWriter(@".\" + LevelName + FileNameSuffix + ".txt");
                        OutputFile.WriteLine(ExportData);
                        OutputFile.Close();
                    }
                }
            }
            Stream.Close();
            return false;
        }


        /*
         * Разобрать MAPHEAD файл
         */
        private static Int32[] ParseMapHeadFile(string FileName)
        {
            int Length = 0;
            int Iteration = 0;
            int DataLength = 0;
            int BlockLengthInBytes = 4;
            FileStream Stream;
            Int32[] Offsets;
            byte[] BuffBytes;

            try
            {
                if (!File.Exists(FileName))
                {
                    throw new Exception("MAPHEAD.WL6: File not exists");
                }
                Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read);
                Length = (int)Stream.Length;
                BuffBytes = Wolfenstein3DMapGrabber.ReadBytes(Stream, 0, 2);
                // первые 2 байта - ключ для декомпрессии (см. DecompressMapDataFile)
                if (BuffBytes[0] != 0xCD || BuffBytes[1] != 0xAB)
                {
                    throw new Exception("MAPHEAD.WL6: Wrong file format");
                }
                DataLength = Length - 2;
                if (DataLength <= 0 || DataLength % BlockLengthInBytes != 0)
                {
                    throw new Exception("MAPHEAD.WL6: Wrong file format");
                }
                Offsets = new Int32[DataLength / BlockLengthInBytes];
                Length = 2;
                // файл содержит в себе набор int'ов (смещений в файле GAMEMAPS)
                while ((BuffBytes = Wolfenstein3DMapGrabber.ReadBytes(Stream, Length, BlockLengthInBytes)).Length > 0)
                {
                    Length += BuffBytes.Length;
                    Offsets[Iteration] = BitConverter.ToInt32(BuffBytes, 0);
                    Iteration++;
                    if (Iteration == Offsets.Length)
                    {
                        Stream.Close();
                        return Offsets;
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            return new Int32[0];
        }

    }
}

 

На выходе получаем набор файликов (по 2 на уровень: *-Map.txt и *-Objects.txt). Они содержат (64 * 64) чисел. Как вы догадались - уровень представляет из себя матрицу 64x64 клетки.

На гитхабе можно посмотреть листинг этих кодов: https://github.....

Чуть более понятнный листинг ниже.

[MAP VALUES]

01	Wall: Grey stone cube
02	Wall: Grey stone cube
03	Wall: Grey stone cube with flag
04	Wall: Grey stone cube with picture
05	Wall: Blue stone cube with cell door
06	Wall: Grey stone cube with bird and archway
07	Wall: Blue stone cube with cell door and skeleton
08	Wall: Blue stone cube
09	Wall: Blue stone cube
0a	Wall: Wood cube with picture of bird
0b	Wall: Wood cube with picture
0c	Wall: Wood cube
0d	Eleva: Elevator door (no red door handle) (ie. from prvs level)
0e	Wall: Steel cube (N/S="Verbotem", E/W="Achtung")
0f	Wall: Steel cube
10	Exit: Landscape view (N/S=sky & green land, E/W=dark & stars?)
11	Wall: Red brick cube
12	Wall: Red brick cube with green wreath
13	Wall: Purple and green cube
14	Wall: Red brick cube with tapestry of bird
15	Wall: Inside elevator (N/S=hand rail,  E/W=controls in down position)
16	Wall: Inside elevator (N/S=blank wall, E/W=controls in up position)
17	Wall: Wood cube with green branches over a cross
18	Wall: (v1.1+) Grey stone cube with green moss/slime
19	Wall: Pink and green cube
1a	Wall: (v1.1+) Grey stone cube with green moss/slime
1b	Wall: Grey stone cube
1c	Wall: Grey stone cube (N/S="Verbotem", E/W="Achtung")
1d	Wall: (Eps.2+) Brown cave
1e	Wall: (Eps.2+) Brown cave with blood
1f	Wall: (Eps.2+) Brown cave with blood
20	Wall: (Eps.2+) Brown cave with blood
21	Wall: (Eps.2+) Stained glass window of Hitler
22	Wall: (Eps.2+) Blue brick wall with skulls
23	Wall: (Eps.2+) Grey brick wall
24	Wall: (Eps.2+) Blue brick wall with swastikas
25	Wall: (Eps.2+) Grey brick wall with hole
26	Wall: (Eps.2+) Red/grey/brown wall
27	Wall: (Eps.2+) Grey brick wall with crack
28	Wall: (Eps.2+) Blue brick wall
29	Wall: (Eps.2+) Blue stone wall with verboten sign
2a	Wall: (Eps.2+) Brown tiles
2b	Wall: (Eps.2+) Grey brick wall with map
2c	Wall: (Eps.2+) Orange stone wall
2d	Wall: (Eps.2+) Orange stone wall
2e	Wall: (Eps.2+) Brown tiles
2f	Wall: (Eps.2+) Brown tiles with banner
30	Wall: (Eps.2+) Orange panel on wood wall
31	Wall: (Eps.2+) Grey brick wall with Hitler
40	Wall: Grey stone cube
41	Wall: Grey stone cube
42	Wall: Grey stone cube
43	Wall: Grey stone cube with flag
44	Wall: Grey stone cube with picture
45	Wall: Blue stone cube with cell door
46	Wall: Grey stone cube with bird and archway
47	Wall: Blue stone cube with cell door and skeleton
48	Wall: Blue stone cube
49	Wall: Blue stone cube
4a	Wall: Wood cube with picture of bird
4b	Wall: Wood cube with picture
4c	Wall: Wood cube
4d	Eleva: Elevator door (no red door handle)
4e	Wall: Steel cube (N/S="Verbotem", E/W="Achtung")
4f	Wall: Steel cube
50	Exit: Exit (N/S=sky & green land, E/W=dark & stars?)
51	Wall: Red brick cube
52	Wall: Red brick cube with green wreath
53	Wall: Pink and green cube
54	Wall: Red brick cube with tapestry of bird
55	Wall: Inside elevator (N/S=hand rail,  E/W=controls in down position)
56	Wall: Inside elevator (N/S=blank wall, E/W=controls in up position)
57	Wall: Wood cube with green branches over a cross
59	Wall: Pink and green cube
5a	VDoor: Steel door (east/west doorway) (vertical on map)
5b	HDoor: Steel door (north/south doorway) (horizontal on map)
5c	Lock: Locked version of 5a (need gold key to open)
5d	Lock: Locked version of 5b (need gold key to open)
5e	Lock: Locked version of 5a (need silver key to open)
5f	Lock: Locked version of 5b (need silver key to open)
60	Lock: Locked version of 5a (can't open)
61	Lock: Locked version of 5b (can't open)
62	Lock: Locked version of 5a (can't open)
63	Lock: Locked version of 5b (can't open)
64	Eleva: Elevator door with a grey stone cube on north and south side
65	Eleva: Elevator door with a grey stone cube on east  and west  side
6b	Floor: Floor
6c	Floor: Floor
6d	Floor: Floor
6e	Floor: Floor
6f	Floor: Floor
70	Floor: Floor
71	Floor: Floor
72	Floor: Floor
73	Floor: Floor
74	Floor: Floor
75	Floor: Floor
76	Floor: Floor
77	Floor: Floor
78	Floor: Floor
79	Floor: Floor
7a	Floor: Floor
7b	Floor: Floor
7c	Floor: Floor
7d	Floor: Floor
7e	Floor: Floor
7f	Floor: Floor
80	Floor: Floor
81	Floor: Floor
82	Floor: Floor
83	Floor: Floor
84	Floor: Floor
85	Floor: Floor
86	Floor: Floor
87	Floor: Floor
88	Floor: Floor
89	Floor: Floor
8a	Floor: Floor
8b	Floor: Floor
8c	Floor: Floor
8d	Floor: Floor
8e	Floor: Floor
8f	Floor: Floor

[OBJECT VALUES]

00	Nothi: Nothing
13	Start: Starting location, facing north
14	Start: Starting location, facing east
15	Start: Starting location, facing south
16	Start: Starting location, facing west
17	Objec: Puddle of water
18	Objec: Green barrel
19	Objec: Table and two chairs
1a	Objec: Floor lamp
1b	Objec: Chandelier
1c	Objec: Skeleton handing from hook in ceiling
1d	Food: Bowl of dog food
1e	Objec: Stone pillar
1f	Objec: Potted tree
20	Objec: Skeleton lying on ground
21	Objec: Sink
22	Objec: Potted plant
23	Objec: Blue vase
24	Objec: Round table
25	Objec: Ceiling light
26	Objec: 5 Pots and pans hanging from wood beam attached to ceiling
27	Objec: Suit of armour
28	Objec: Hanging cage
29	Objec: Hanging cage with skeleton inside
2a	Objec: Pile of bones
2b	GKey: Gold key
2c	SKey: Silver key
2d	Objec: Cot
2e	Objec: Bucket
2f	Food: Plate of food
30	Aid: First aid kit
31	Ammo: Ammo
32	Weap3: Weapon 3
33	Weap4: Weapon 4
34	Treas: Jewelled cross
35	Treas: Gold chalice
36	Treas: Jewelled box
37	Treas: Crown
38	Mirro: Blue mirror with face
39	Objec: Bones and blood
3a	Objec: Wood barrel
3b	Objec: Stone well with water
3c	Objec: Stone well no water
3d	Objec: Blood
3e	Objec: Flag pole and flag
3f	Objec: (v1.1)  Floating sign: "CALL APOGEE. SAY "SNAPPITY"  (v1.11) Floating sign: "CALL APOGEE. SAY "AARDWOLF"
40	Objec: (v1.1+) Broken glass on floor (or crushed bones?)
41	Objec: (v1.1+) Broken glass on floor (or crushed bones?)
42	Objec: (v1.1+) Broken glass on floor (or crushed bones?)
43	Objec: (v1.1+) Pot, pan, and ladle hanging from ceiling
44	Objec: (v1.1+) Iron wood-burning stove
45	Objec: (v1.1+) Rack of poles (spears ?)
46	Objec: (v1.1+) Green vines hanging from ceiling
49	?????: Unknown purpose (found in episode 1, on level 3)
5a	Face: Makes enemy face east
5b	Face: Makes enemy face north east
5c	Face: Makes enemy face north
5d	Face: Makes enemy face north west
5e	Face: Makes enemy face west
5f	Face: Makes enemy face south west
60	Face: Makes enemy face south
61	Face: Makes enemy face south east
62	Secre: Block is a secret passage
63	End: Ends game
6c	Guard: Tan soldier (skill level 1 & 2), standing still facing east
6d	Guard: Tan soldier (skill level 1 & 2), standing still facing north
6e	Guard: Tan soldier (skill level 1 & 2), standing still facing west
6f	Guard: Tan soldier (skill level 1 & 2), standing still facing south
70	Guard: Tan soldier (skill level 1 & 2), walking east
71	Guard: Tan soldier (skill level 1 & 2), walking north
72	Guard: Tan soldier (skill level 1 & 2), walking west
73	Guard: Tan soldier (skill level 1 & 2), walking south
74	WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing east
75	WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing north
76	WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing west
77	WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing south
7c	Objec: Dead tan soldier
7e	SS: Blue soldier (skill level 1 & 2), standing still, facing east
7f	SS: Blue soldier (skill level 1 & 2), standing still, facing north
80	SS: Blue soldier (skill level 1 & 2), standing still, facing west
81	SS: Blue soldier (skill level 1 & 2), standing still, facing south
82	SS: Blue soldier (skill level 1 & 2), walking east
83	SS: Blue soldier (skill level 1 & 2), walking north
84	SS: Blue soldier (skill level 1 & 2), walking west
85	SS: Blue soldier (skill level 1 & 2), walking south
8a	Dog: Dog (skill level 1 & 2), running east
8b	Dog: Dog (skill level 1 & 2), running north
8c	Dog: Dog (skill level 1 & 2), running west
8d	Dog: Dog (skill level 1 & 2), running south
90	Guar3: Tan soldier (skill level 3), standing still, facing east
91	Guar3: Tan soldier (skill level 3), standing still, facing north
92	Guar3: Tan soldier (skill level 3), standing still, facing west
93	Guar3: Tan soldier (skill level 3), standing still, facing south
94	Guar3: Tan soldier (skill level 3), walking east
95	Guar3: Tan soldier (skill level 3), walking north
96	Guar3: Tan soldier (skill level 3), walking west
97	Guar3: Tan soldier (skill level 3), walking south
98	WGua3: (Eps.2+) White guard (skill level 3), standing, facing east
99	WGua3: (Eps.2+) White guard (skill level 3), standing, facing north
9a	WGua3: (Eps.2+) White guard (skill level 3), standing, facing west
9b	WGua3: (Eps.2+) White guard (skill level 3), standing, facing south
a0	Pries: (Eps.2+) Black priest
a2	SS3: Blue soldier (skill level 3), standing still, facing east
a3	SS3: Blue soldier (skill level 3), standing still, facing north
a4	SS3: Blue soldier (skill level 3), standing still, facing west
a5	SS3: Blue soldier (skill level 3), standing still, facing south
a6	SS3: Blue soldier (skill level 3), walking east
a7	SS3: Blue soldier (skill level 3), walking north
a8	SS3: Blue soldier (skill level 3), walking west
a9	SS3: Blue soldier (skill level 3), walking south
ae	Dog3: Dog (skill level 3), running east
af	Dog3: Dog (skill level 3), running north
b0	Dog3: Dog (skill level 3), running west
b1	Dog3: Dog (skill level 3), running south
b2	Hitlr: (Eps.2+) Hitler
b4	Guar4: Tan soldier (skill level 4), standing still, facing east
b5	Guar4: Tan soldier (skill level 4), standing still, facing north
b6	Guar4: Tan soldier (skill level 4), standing still, facing west
b7	Guar4: Tan soldier (skill level 4), standing still, facing south
b8	Guar4: Tan soldier (skill level 4), walking east
b9	Guar4: Tan soldier (skill level 4), walking north
ba	Guar4: Tan soldier (skill level 4), walking west
bb	Guar4: Tan soldier (skill level 4), walking south
bc	WGua4: (Eps.2+) White guard (skill level 4), standing, facing east 
bd	WGua4: (Eps.2+) White guard (skill level 4), standing, facing north
be	WGua4: (Eps.2+) White guard (skill level 4), standing, facing west 
bf	WGua4: (Eps.2+) White guard (skill level 4), standing, facing south
c4	Schab: (Eps.2+) Dr. Schabbs
c6	SS4: Blue soldier (skill level 4), standing still, facing east
c7	SS4: Blue soldier (skill level 4), standing still, facing north
c8	SS4: Blue soldier (skill level 4), standing still, facing west
c9	SS4: Blue soldier (skill level 4), standing still, facing south
ca	SS4: Blue soldier (skill level 4), walking east
cb	SS4: Blue soldier (skill level 4), walking north
cc	SS4: Blue soldier (skill level 4), walking west
cd	SS4: Blue soldier (skill level 4), walking south
d2	Dog4: Dog (skill level 4), running east
d3	Dog4: Dog (skill level 4), running north
d4	Dog4: Dog (skill level 4), running west
d5	Dog4: Dog (skill level 4), running south
d6	Boss: Final boss (blue) ("Hans")
d8	Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing east
d9	Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing north
da	Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing west
db	Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing south
ea	Zomb3: (Eps.2+) Zombie (skill level 3), standing facing east 
eb	Zomb3: (Eps.2+) Zombie (skill level 3), standing facing north
ec	Zomb3: (Eps.2+) Zombie (skill level 3), standing facing west 
ed	Zomb3: (Eps.2+) Zombie (skill level 3), standing facing south
fc	Zomb4: (Eps.2+) Zombie (skill level 4), standing facing east 
fd	Zomb4: (Eps.2+) Zombie (skill level 4), standing facing north
fe	Zomb4: (Eps.2+) Zombie (skill level 4), standing facing west 
ff	Zomb4: (Eps.2+) Zombie (skill level 4), standing facing south

 

Возможно будет интересно: