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
-
Create a Project Directory:
bash
mkdir typescript-doom
cd typescript-doom -
Initialize a Node.js Project:
bash
npm init -y # Or: yarn init -y
This creates apackage.json
file, which will store your project’s metadata and dependencies. -
Install TypeScript:
bash
npm install --save-dev typescript # Or: yarn add --dev typescript
This installs the TypeScript compiler as a development dependency. -
Create a
tsconfig.json
File:
This file configures the TypeScript compiler. Create a file namedtsconfig.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 accidentalany
types, which can weaken TypeScript’s type safety.strictNullChecks
: Another essential option. It forces you to handlenull
andundefined
values explicitly, preventing common runtime errors.noImplicitThis
: Prevents implicitany
types for thethis
keyword.alwaysStrict
: Ensures your code is always parsed in strict mode.noUnusedLocals
andnoUnusedParameters
: 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 returnundefined
).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 thesrc
directory.exclude
: Specifies which files to exclude. We’re excluding thenode_modules
directory, which contains our installed packages.
-
Create a
src
Directory:
bash
mkdir src
This directory will contain your TypeScript source code. -
Create an
index.ts
File:
Inside thesrc
directory, create a file namedindex.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!"); -
Compile and Run:
Open your terminal and run:
bash
npx tsc
This command uses the TypeScript compiler (tsc
) to compile yourindex.ts
file into a JavaScript file (index.js
) in thedist
directory (as specified intsconfig.json
). Thenpx
command ensures you’re using the locally installed version oftsc
.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.
-
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.
-
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.
- Like most games, Doom operates on a game loop. This loop runs repeatedly (ideally at a consistent frame rate) and performs the following tasks:
-
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.
- Doom levels are represented using a 2D data structure. The key components are:
-
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.
-
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.
-
WAD File Parsing (Simplified):
Create a new file
src/wad.ts
:“`typescript
// src/wad.tsinterface 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 thefetch
API to retrieve the WAD file data. For simplicity, this example assumes you have the WAD file’s data as anArrayBuffer
. 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 theDataView
. It extracts theidentification
,numLumps
, andinfoTableOfs
. Note the use ofgetInt32(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 theDataView
. It iterates through the directory entries, extracting thefilePos
,size
, andname
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.
-
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 aWad
object as a dependency. This is a simple form of dependency injection, making theLevel
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 thevertices
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 usesreadLumpName
to extract texture names.loadSectors(sectorData: DataView)
: Parses the SECTORS lump. It also usesreadLumpName
.loadThings(thingData: DataView)
: Parses the THINGS lump.readLumpName(data: DataView, offset: number)
: A helper function to read null-terminated strings (lump names) from aDataView
.
- Interfaces (
-
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.tsimport { 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 therenderLevel
function.renderLevel(level: Level)
: This function will go through each line def and draw a basic representation on the canvas.
-
Putting It All Together (index.ts):
Modify
src/index.ts
to use theWad
,Level
, andRenderer
classes:“`typescript
// src/index.tsimport { 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 anawait
call becausewad.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.
- Creates a
createHTML()
Function: Creates anindex.html
file in thedist
directory.- Error Handling: Uses a
.catch
block to handle any errors during the loading or processing of the WAD.
-
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
- Make sure your