TypeScript Doom: Your First Steps

Okay, here’s a very detailed article (approximately 5000 words) on “TypeScript Doom: Your First Steps,” covering the setup, core concepts, and initial implementation stages. I’ve structured it in a way that assumes the reader has some basic JavaScript and programming experience but is new to TypeScript and this particular project.

TypeScript Doom: Your First Steps

This article guides you through the exciting (and slightly intimidating) world of porting the classic game Doom to TypeScript. We’ll be focusing on the very first steps: setting up your development environment, understanding the core architectural concepts, and implementing the foundational pieces that will form the bedrock of your project. This is not a complete guide to building a fully functional Doom engine; instead, it’s a robust introduction designed to get you started and give you the confidence to continue exploring.

Why TypeScript Doom?

Before we dive in, let’s consider the “why.” Why would anyone undertake such a project? There are several excellent reasons:

  • Learning TypeScript in Depth: A project of this scale forces you to grapple with nearly every aspect of TypeScript, from basic types and interfaces to advanced concepts like generics, modules, and decorators (if you choose to use them).
  • Understanding Game Engine Architecture: Doom, despite its age, has a well-designed (for its time) engine. Dissecting it and reimplementing it in a modern language teaches you valuable principles of game development, including rendering, collision detection, AI, and resource management.
  • A Challenging and Rewarding Project: Successfully porting even a portion of Doom is a significant accomplishment. It’s a project that will test your skills and push you to learn and grow as a developer.
  • Modernizing a Classic: Bringing Doom to the browser with TypeScript makes it accessible to a wider audience and opens up possibilities for modifications and extensions.
  • Learning to deal with Legacy Code

Prerequisites

Before you begin, you’ll need the following:

  • Basic JavaScript Knowledge: You should be comfortable with JavaScript fundamentals like variables, functions, objects, arrays, loops, and conditional statements.
  • Node.js and npm (or yarn): These are essential for managing your project’s dependencies and running your build tools. Download the latest LTS version of Node.js from https://nodejs.org/. npm is included with Node.js. Yarn is an alternative package manager, and you can install it if you prefer.
  • A Text Editor or IDE: Choose your favorite code editor. Popular choices include Visual Studio Code (recommended, with excellent TypeScript support), Sublime Text, Atom, or WebStorm.
  • Git (optional but highly recommended): Git is a version control system that will help you track your changes and collaborate with others (if you choose to). You can download Git from https://git-scm.com/.
  • The original Doom WAD File This contains the games assets, and while it is copyrighted material, it can often be found bundled with various versions and ports of the original doom game.

Step 1: Project Setup

  1. Create a Project Directory:
    bash
    mkdir typescript-doom
    cd typescript-doom

  2. Initialize a Node.js Project:
    bash
    npm init -y # Or: yarn init -y

    This creates a package.json file, which will store your project’s metadata and dependencies.

  3. Install TypeScript:
    bash
    npm install --save-dev typescript # Or: yarn add --dev typescript

    This installs the TypeScript compiler as a development dependency.

  4. Create a tsconfig.json File:
    This file configures the TypeScript compiler. Create a file named tsconfig.json in your project root with the following content:

    json
    {
    "compilerOptions": {
    "target": "es2017", // Target JavaScript version (ES2017 is a good balance of compatibility and features)
    "module": "commonjs", // Module system (CommonJS is suitable for Node.js)
    "outDir": "./dist", // Output directory for compiled JavaScript files
    "sourceMap": true, // Generate source maps for debugging
    "strict": true, // Enable strict type checking
    "esModuleInterop": true, // Allows importing CommonJS modules as ES modules
    "skipLibCheck": true, // Skip type checking of declaration files (*.d.ts)
    "forceConsistentCasingInFileNames": true, // Enforce consistent file casing
    "noImplicitAny": true, // Error on implicit 'any' types (highly recommended)
    "strictNullChecks": true, // Enable strict null checks (highly recommended)
    "noImplicitThis": true, // Error on 'this' expressions with implicit 'any' type
    "alwaysStrict": true, // Parse in strict mode and emit "use strict" for each source file
    "noUnusedLocals": true, // Report errors on unused local variables
    "noUnusedParameters": true, // Report errors on unused function parameters
    "noImplicitReturns": true, // Report error when not all code paths in function return a value
    "noFallthroughCasesInSwitch": true // Report errors for fallthrough cases in switch statement
    },
    "include": [
    "./src/**/*" // Include all files in the 'src' directory
    ],
    "exclude": [
    "node_modules" // Exclude the 'node_modules' directory
    ]
    }

    Explanation of tsconfig.json Options:

    • target: Specifies the ECMAScript target version. es2017 provides a good balance of modern features and browser compatibility.
    • module: Defines the module system used. commonjs is the standard for Node.js projects.
    • outDir: Sets the output directory for the compiled JavaScript files.
    • sourceMap: Enables the generation of source maps, which are crucial for debugging TypeScript code in your browser.
    • strict: Enables a suite of strict type-checking options, making your code more robust and less prone to errors.
    • esModuleInterop: Improves compatibility when working with CommonJS modules.
    • skipLibCheck: Speeds up compilation by skipping type checking of declaration files.
    • forceConsistentCasingInFileNames: Helps prevent issues on case-sensitive file systems.
    • noImplicitAny: This is a crucial option. It forces you to explicitly type everything, preventing accidental any types, which can weaken TypeScript’s type safety.
    • strictNullChecks: Another essential option. It forces you to handle null and undefined values explicitly, preventing common runtime errors.
    • noImplicitThis: Prevents implicit any types for the this keyword.
    • alwaysStrict: Ensures your code is always parsed in strict mode.
    • noUnusedLocals and noUnusedParameters: Help keep your code clean by identifying unused variables and parameters.
    • noImplicitReturns: Ensures that all code paths in a function return a value (or explicitly return undefined).
    • noFallthroughCasesInSwitch: Makes sure that if you have a case in a switch, it has a break or return, and doesn’t fall to the next case unintentionally.
    • include: Specifies which files to include in the compilation. We’re including everything in the src directory.
    • exclude: Specifies which files to exclude. We’re excluding the node_modules directory, which contains our installed packages.
  5. Create a src Directory:
    bash
    mkdir src

    This directory will contain your TypeScript source code.

  6. Create an index.ts File:
    Inside the src directory, create a file named index.ts. This will be the entry point of your application. For now, let’s just add a simple “Hello, Doom!” message:

    typescript
    // src/index.ts
    console.log("Hello, Doom!");

  7. Compile and Run:
    Open your terminal and run:
    bash
    npx tsc

    This command uses the TypeScript compiler (tsc) to compile your index.ts file into a JavaScript file (index.js) in the dist directory (as specified in tsconfig.json). The npx command ensures you’re using the locally installed version of tsc.

    Now, run the compiled JavaScript:
    bash
    node dist/index.js

    You should see “Hello, Doom!” printed in your console.

Step 2: Understanding the Core Concepts (Doom’s Architecture)

Before we write more code, it’s essential to grasp the fundamental architectural components of the original Doom engine. This knowledge will guide our implementation in TypeScript. We’ll be simplifying some aspects for this initial phase, but the core ideas remain the same.

  1. WAD Files (Where’s All the Data?):

    • Doom’s data (levels, textures, sprites, sounds, etc.) is stored in WAD files. These are essentially archives containing various lumps of data.
    • A WAD file has a specific structure: a header, a directory (which is a list of lumps and their locations within the file), and the lump data itself.
    • Lumps: Each piece of data within the WAD file is called a lump. Lumps have names (up to 8 characters) and are identified by their position in the directory.
    • Key Lump Types:
      • MAP lumps: Contain level data (linedefs, sidedefs, sectors, things, etc.).
      • TEXTURE lumps: Define composite textures built from patches.
      • PNAMES lump: A list of patch names used in textures.
      • PATCH lumps: Individual image data for parts of textures.
      • SPRITE lumps: Image data for sprites (enemies, items, decorations).
      • SOUND lumps: Audio data.
  2. Game Loop:

    • Like most games, Doom operates on a game loop. This loop runs repeatedly (ideally at a consistent frame rate) and performs the following tasks:
      • Input Handling: Process player input (keyboard, mouse).
      • Game Logic: Update game state (player position, enemy AI, projectiles, etc.).
      • Rendering: Draw the current game state to the screen.
  3. Level Representation:

    • Doom levels are represented using a 2D data structure. The key components are:
      • Vertices: Points in 2D space (x, y coordinates).
      • Linedefs: Lines connecting two vertices. They define the walls of the level.
      • Sidedefs: Each linedef has one or two sidedefs (one for each side of the wall). Sidedefs define the textures and properties of the wall.
      • Sectors: Closed areas bounded by linedefs. They represent the “floor” and “ceiling” of a room.
      • Things: Objects in the level, such as the player, enemies, items, and decorations. They have a position, type, and other properties.
  4. Rendering (Simplified):

    • Doom uses a technique called Binary Space Partitioning (BSP) to efficiently determine which parts of the level are visible to the player. We will not implement the BSP at this time.
    • Wall Rendering: Walls are rendered by drawing vertical strips (columns) of texture data.
    • Floor and Ceiling Rendering: Doom uses a clever trick to render floors and ceilings by essentially “stretching” textures based on the player’s perspective.
    • Sprites These are images that are rendered using their own image data, and are not stretched like walls.
  5. Collision Detection:

    • Doom uses a relatively simple collision detection system based on bounding boxes. It checks if the player’s bounding box intersects with the bounding boxes of walls and other objects.

Step 3: Implementing Foundational Components

Now, let’s start implementing some of the core components in TypeScript. We’ll focus on creating data structures to represent the WAD file, level data, and basic rendering.

  1. WAD File Parsing (Simplified):

    Create a new file src/wad.ts:

    “`typescript
    // src/wad.ts

    interface WadHeader {
    identification: string; // Should be “IWAD” or “PWAD”
    numLumps: number;
    infoTableOfs: number; // Offset to the directory
    }

    interface WadLump {
    filePos: number; // Offset to the lump’s data
    size: number;
    name: string;
    }

    export class Wad {
    private header: WadHeader | null = null;
    private lumps: WadLump[] = [];
    private data: DataView | null = null;

    async load(wadFilePath: string): Promise {
    // In a real implementation, you’d fetch the WAD file (e.g., using fetch API in the browser).
    // For this example, we’ll assume you have the WAD file data as an ArrayBuffer.
    // We use a File Reader in Node to get the ArrayBuffer
    const fileBuffer = await this.readFileAsArrayBuffer(wadFilePath);
    this.data = new DataView(fileBuffer);

    this.header = this.readHeader();
    this.lumps = this.readDirectory();
    

    }

    private readFileAsArrayBuffer(filePath: string): Promise {
    return new Promise((resolve, reject) => {
    const fs = require(‘fs’); // Import the ‘fs’ module for file system operations
    fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => {
    if (err) {
    reject(err);
    } else {
    resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
    }
    });
    });
    }

    private readHeader(): WadHeader {
    if (!this.data) throw new Error(“Wad Data Not loaded”);

      const identification = String.fromCharCode(
          ...new Uint8Array(this.data.buffer.slice(0, 4))
      );
      const numLumps = this.data.getInt32(4, true); // Little-endian
      const infoTableOfs = this.data.getInt32(8, true);
    
      return { identification, numLumps, infoTableOfs };
    

    }
    private readDirectory(): WadLump[] {
    if (!this.data || !this.header) throw new Error(“Wad Data or Header not loaded”);

    const lumps: WadLump[] = [];
    let offset = this.header.infoTableOfs;
    
    for (let i = 0; i < this.header.numLumps; i++) {
      const filePos = this.data.getInt32(offset, true);
      const size = this.data.getInt32(offset + 4, true);
      const nameBytes = new Uint8Array(this.data.buffer.slice(offset + 8, offset + 16));
      // Find the null terminator to get the actual name length
      let nameLength = 0;
      while (nameLength < 8 && nameBytes[nameLength] !== 0) {
        nameLength++;
      }
      const name = String.fromCharCode(...nameBytes.slice(0, nameLength));
    
      lumps.push({ filePos, size, name });
      offset += 16; // Each directory entry is 16 bytes
    }
    
    return lumps;
    

    }

    getLump(name: string): WadLump | undefined {
    return this.lumps.find((lump) => lump.name === name);
    }
    getLumpData(lump: WadLump): DataView {
    if(!this.data) throw new Error(“Data is Null”);
    return new DataView(this.data.buffer, lump.filePos, lump.size);
    }

    printWADHeader() {
    if(this.header) {
    console.log(WAD Header:
    Identification: ${this.header.identification}
    Number of Lumps: ${this.header.numLumps}
    Offset to Directory: ${this.header.infoTableOfs}
    );
    }
    else
    {
    console.error(“Header is NULL”);
    }
    }

    printLumpNames() {
    console.log(“Lump Names:”);
    this.lumps.forEach((lump) => {
    console.log(lump.name);
    });
    }
    }
    “`

    Explanation:

    • WadHeader Interface: Defines the structure of the WAD file header.
    • WadLump Interface: Defines the structure of a single lump entry in the WAD directory.
    • Wad Class:
      • load(wadFilePath: string): This method is asynchronous (async). It’s designed to load the WAD file. In a browser environment, you’d likely use the fetch API to retrieve the WAD file data. For simplicity, this example assumes you have the WAD file’s data as an ArrayBuffer. This function uses the Node file system library to load a file.
      • readFileAsArrayBuffer: Loads a file and returns a promise.
      • readHeader(): Reads the header information from the DataView. It extracts the identification, numLumps, and infoTableOfs. Note the use of getInt32(offset, true) to read 32-bit integers in little-endian format (which is how Doom stores data).
      • readDirectory(): Reads the directory (list of lumps) from the DataView. It iterates through the directory entries, extracting the filePos, size, and name for each lump.
      • getLump(name: string): Finds and returns the information for a given lump.
      • getLumpData(lump: WadLump): Returns the lump’s data.
      • printWADHeader(): Prints header data to console.
      • printLumpNames(): Prints all lump names to console.
  2. Level Data Structures:

    Create a new file src/level.ts:

    “`typescript
    // src/level.ts
    import { Wad, WadLump } from ‘./wad’;

    interface Vertex {
    x: number;
    y: number;
    }

    interface Linedef {
    startVertex: number; // Index into the vertices array
    endVertex: number;
    flags: number;
    specialType: number;
    sectorTag: number;
    frontSidedef: number; // Index into the sidedefs array (-1 if none)
    backSidedef: number; // Index into the sidedefs array (-1 if none)
    }

    interface Sidedef {
    xOffset: number;
    yOffset: number;
    upperTexture: string; // Texture names
    lowerTexture: string;
    middleTexture: string;
    sector: number; // Index into the sectors array
    }
    interface Sector {
    floorHeight: number;
    ceilingHeight: number;
    floorTexture: string;
    ceilingTexture: string;
    lightLevel: number;
    specialType: number;
    tag: number;
    }

    interface Thing {
    x: number;
    y: number;
    angle: number;
    type: number;
    flags: number;
    }
    export class Level {
    vertices: Vertex[] = [];
    linedefs: Linedef[] = [];
    sidedefs: Sidedef[] = [];
    sectors: Sector[] = [];
    things: Thing[] = [];

    constructor(private wad: Wad) {}
    loadMapData(mapName: string): void {
    const mapLumpIndex = this.wad.getLump(mapName);
    if (!mapLumpIndex) {
    throw new Error(Map lump ${mapName} not found.);
    }

    //   const lumpNames = ["THINGS", "LINEDEFS", "SIDEDEFS", "VERTEXES", "SEGS",
    //    "SSECTORS", "NODES", "SECTORS", "REJECT", "BLOCKMAP"];
    
      const lumpNames = ["THINGS", "LINEDEFS", "SIDEDEFS", "VERTEXES", "SECTORS"];
      const lumpData: { [key: string]: DataView } = {};
    
      let currentIndex = this.wad.getLump(mapName)!.filePos;
    
      for (const lumpName of lumpNames) {
          const lump = this.wad.getLump(lumpName);
          if (lump) {
            lumpData[lumpName] = this.wad.getLumpData(lump);
          }
          else {
            console.warn(`Lump ${lumpName} Not Found`);
          }
      }
        this.loadVertices(lumpData["VERTEXES"]);
        this.loadLinedefs(lumpData["LINEDEFS"]);
        this.loadSidedefs(lumpData["SIDEDEFS"]);
        this.loadSectors(lumpData["SECTORS"]);
        this.loadThings(lumpData["THINGS"]);
    }
    

    private loadVertices(vertexData: DataView): void {

    const numVertices = vertexData.byteLength / 4; // Each vertex is 4 bytes (2 shorts)
    
    for (let i = 0; i < numVertices; i++) {
      const x = vertexData.getInt16(i * 4, true);
      const y = vertexData.getInt16(i * 4 + 2, true);
      this.vertices.push({ x, y });
    }
    

    }
    private loadLinedefs(linedefData: DataView): void {

    const numLinedefs = linedefData.byteLength / 14; // Each linedef is 14 bytes
    
    for (let i = 0; i < numLinedefs; i++) {
      const startVertex = linedefData.getInt16(i * 14, true);
      const endVertex = linedefData.getInt16(i * 14 + 2, true);
      const flags = linedefData.getInt16(i * 14 + 4, true);
      const specialType = linedefData.getInt16(i * 14 + 6, true);
      const sectorTag = linedefData.getInt16(i * 14 + 8, true);
      const frontSidedef = linedefData.getInt16(i * 14 + 10, true);
      const backSidedef = linedefData.getInt16(i * 14 + 12, true);
    
      this.linedefs.push({
        startVertex,
        endVertex,
        flags,
        specialType,
        sectorTag,
        frontSidedef,
        backSidedef,
      });
    }
    

    }
    private loadSidedefs(sidedefData: DataView): void {

    const numSidedefs = sidedefData.byteLength / 30; // Each sidedef is 30 bytes
    
    for (let i = 0; i < numSidedefs; i++) {
      const xOffset = sidedefData.getInt16(i * 30, true);
      const yOffset = sidedefData.getInt16(i * 30 + 2, true);
      const upperTexture = this.readLumpName(sidedefData, i * 30 + 4);
      const lowerTexture = this.readLumpName(sidedefData, i * 30 + 12);
      const middleTexture = this.readLumpName(sidedefData, i * 30 + 20);
      const sector = sidedefData.getInt16(i * 30 + 28, true);
    
      this.sidedefs.push({
        xOffset,
        yOffset,
        upperTexture,
        lowerTexture,
        middleTexture,
        sector,
      });
    }
    

    }

    private loadSectors(sectorData: DataView): void {

    const numSectors = sectorData.byteLength / 26; // Each sector is 26 bytes
    
    for (let i = 0; i < numSectors; i++) {
      const floorHeight = sectorData.getInt16(i * 26, true);
      const ceilingHeight = sectorData.getInt16(i * 26 + 2, true);
      const floorTexture = this.readLumpName(sectorData, i * 26 + 4);
      const ceilingTexture = this.readLumpName(sectorData, i * 26 + 12);
      const lightLevel = sectorData.getInt16(i * 26 + 20, true);
      const specialType = sectorData.getInt16(i * 26 + 22, true);
      const tag = sectorData.getInt16(i * 26 + 24, true);
    
      this.sectors.push({
        floorHeight,
        ceilingHeight,
        floorTexture,
        ceilingTexture,
        lightLevel,
        specialType,
        tag,
      });
    }
    

    }
    private loadThings(thingData: DataView): void {

    const numThings = thingData.byteLength / 10; // Each thing is 10 bytes
    
    for (let i = 0; i < numThings; i++) {
      const x = thingData.getInt16(i * 10, true);
      const y = thingData.getInt16(i * 10 + 2, true);
      const angle = thingData.getInt16(i * 10 + 4, true);
      const type = thingData.getInt16(i * 10 + 6, true);
      const flags = thingData.getInt16(i * 10 + 8, true);
    
      this.things.push({ x, y, angle, type, flags });
    }
    

    }

    private readLumpName(data: DataView, offset: number): string {
    const nameBytes = new Uint8Array(data.buffer, data.byteOffset + offset, 8);
    let nameLength = 0;
    while (nameLength < 8 && nameBytes[nameLength] !== 0) {
    nameLength++;
    }
    return String.fromCharCode(…nameBytes.slice(0, nameLength));
    }
    }

    “`

    Explanation:

    • Interfaces (Vertex, Linedef, Sidedef, Sector, Thing): These interfaces define the structure of the different level components, mirroring the data structures in the Doom WAD file.
    • Level Class:
      • vertices, linedefs, sidedefs, sectors, things: Arrays to store the level data.
      • constructor(private wad: Wad): Takes a Wad object as a dependency. This is a simple form of dependency injection, making the Level class more testable and reusable.
      • loadMapData(mapName: string): Loads the level data from the WAD file. It finds the map lump (e.g., “E1M1”) and then retrieves the related lumps (VERTEXES, LINEDEFS, SIDEDEFS, SECTORS, THINGS). It calls helper functions to parse each lump type.
      • loadVertices(vertexData: DataView): Parses the VERTEXES lump and populates the vertices array. It reads pairs of 16-bit integers (x, y coordinates).
      • loadLinedefs(linedefData: DataView): Parses the LINEDEFS lump.
      • loadSidedefs(sidedefData: DataView): Parses the SIDEDEFS lump. It uses readLumpName to extract texture names.
      • loadSectors(sectorData: DataView): Parses the SECTORS lump. It also uses readLumpName.
      • loadThings(thingData: DataView): Parses the THINGS lump.
      • readLumpName(data: DataView, offset: number): A helper function to read null-terminated strings (lump names) from a DataView.
  3. Basic Rendering (Conceptual):

    We won’t implement full rendering in this “first steps” guide, but let’s outline the conceptual approach and create a placeholder file.

    Create src/renderer.ts:

    “`typescript
    // src/renderer.ts

    import { Level } from ‘./level’;

    export class Renderer {
    private canvas: HTMLCanvasElement;
    private context: CanvasRenderingContext2D;

    constructor(canvasId: string) {
    const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
    if (!canvas) {
    throw new Error(Canvas element with ID "${canvasId}" not found.);
    }
    this.canvas = canvas;

    const context = this.canvas.getContext('2d');
    if (!context) {
      throw new Error('Could not get 2D rendering context.');
    }
    this.context = context;
    

    }

    render(level: Level): void {
    // Placeholder for rendering logic.
    // In a real implementation, you would:
    // 1. Clear the canvas.
    // 2. Implement a BSP traversal (or a simplified visibility check).
    // 3. Draw visible walls, floors, ceilings, and sprites.
    this.context.fillStyle = ‘gray’;
    this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
    console.log(“Rendering Placeholder”);

    this.renderLevel(level);
    

    }

    private renderLevel(level: Level) : void {
    const scale = 4; // Scaling factor for visualization

      // Draw linedefs
      this.context.strokeStyle = 'white';
      this.context.lineWidth = 2;
    
      for (const linedef of level.linedefs) {
          const startVertex = level.vertices[linedef.startVertex];
          const endVertex = level.vertices[linedef.endVertex];
    
          this.context.beginPath();
          this.context.moveTo(startVertex.x / scale, startVertex.y / scale);
          this.context.lineTo(endVertex.x / scale, endVertex.y / scale);
          this.context.stroke();
      }
    

    }
    }
    “`

    Explanation:

    • Renderer Class:
      • constructor(canvasId: string): Takes the ID of a canvas element as input. It gets the canvas and its 2D rendering context.
      • render(level: Level): This is the main rendering function. For now, it just fills the canvas with gray. This is where the core rendering logic would go. It calls the renderLevel function.
      • renderLevel(level: Level): This function will go through each line def and draw a basic representation on the canvas.
  4. Putting It All Together (index.ts):

    Modify src/index.ts to use the Wad, Level, and Renderer classes:

    “`typescript
    // src/index.ts

    import { Wad } from ‘./wad’;
    import { Level } from ‘./level’;
    import { Renderer } from ‘./renderer’;

    async function main() {
    const wad = new Wad();
    // Replace ‘path/to/your/doom.wad’ with the actual path to your WAD file.
    await wad.load(‘./doom.wad’);
    wad.printWADHeader();
    wad.printLumpNames();
    const level = new Level(wad); // Pass the Wad object to the Level
    level.loadMapData(‘E1M1’); // Load a specific map (e.g., E1M1)

    // Create an HTML file
    createHTML();

    const renderer = new Renderer(‘gameCanvas’);
    renderer.render(level);

    }
    function createHTML() {
    const htmlContent = <!DOCTYPE html>
    <html>
    <head>
    <title>TypeScript Doom</title>
    <style>
    body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
    background-color: black;
    }
    canvas {
    border: 2px solid white;
    }
    </style>
    </head>
    <body>
    <canvas id="gameCanvas" width="640" height="480"></canvas>
    <script src="index.js"></script>
    </body>
    </html>
    ;

    const fs = require(‘fs’);
    fs.writeFileSync(‘./dist/index.html’, htmlContent);
    }

    main().catch((error) => {
    console.error(error);
    });
    “`

    Explanation:

    • main() Function:
      • Creates a Wad instance.
      • Calls wad.load() to load the WAD file (replace 'path/to/your/doom.wad' with the correct path). This is an await call because wad.load() is asynchronous.
      • Creates a Level, and passes the wad as a dependency.
      • Calls level.loadMapData('E1M1') to load the level data for E1M1.
      • Calls the createHTML() function to generate our HTML file.
      • Creates a Renderer instance, targeting a canvas element with the ID “gameCanvas”.
      • Calls renderer.render(level) to initiate the (placeholder) rendering.
    • createHTML() Function: Creates an index.html file in the dist directory.
    • Error Handling: Uses a .catch block to handle any errors during the loading or processing of the WAD.
  5. Running the Code:

    • Make sure your doom.wad file is in the root of your project.
    • Compile using npx tsc
    • Run by opening ./dist/index.html in your browser.

    You should see a gray canvas with your level’s lines drawn in white. This

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top