diff --git a/.github/workflows/First Person Shooter Build.yml b/.github/workflows/First Person Shooter Build.yml
new file mode 100644
index 00000000..f9a97bac
--- /dev/null
+++ b/.github/workflows/First Person Shooter Build.yml
@@ -0,0 +1,20 @@
+name: First Person Shooter Build
+on:
+ push:
+ paths:
+ - 'Projects/First Person Shooter/**'
+ - '!**.md'
+ pull_request:
+ paths:
+ - 'Projects/First Person Shooter/**'
+ - '!**.md'
+ workflow_dispatch:
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+ - run: dotnet build "Projects\First Person Shooter\First Person Shooter.csproj" --configuration Release
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3df29cc0..77b7a819 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -502,6 +502,16 @@
"console": "externalTerminal",
"stopAtEntry": false,
},
+ {
+ "name": "Shmup",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "Build Shmup",
+ "program": "${workspaceFolder}/Projects/Shmup/bin/Debug/Shmup.dll",
+ "cwd": "${workspaceFolder}/Projects/Shmup/bin/Debug",
+ "console": "externalTerminal",
+ "stopAtEntry": false,
+ },
{
"name": "Role Playing Game",
"type": "coreclr",
@@ -523,12 +533,12 @@
"stopAtEntry": false,
},
{
- "name": "Shmup",
+ "name": "First Person Shooter",
"type": "coreclr",
"request": "launch",
- "preLaunchTask": "Build Shmup",
- "program": "${workspaceFolder}/Projects/Shmup/bin/Debug/Shmup.dll",
- "cwd": "${workspaceFolder}/Projects/Shmup/bin/Debug",
+ "preLaunchTask": "Build First Person Shooter",
+ "program": "${workspaceFolder}/Projects/First Person Shooter/bin/Debug/First Person Shooter.dll",
+ "cwd": "${workspaceFolder}/Projects/First Person Shooter/bin/Debug",
"console": "externalTerminal",
"stopAtEntry": false,
},
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 22726416..0250913c 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -691,6 +691,19 @@
],
"problemMatcher": "$msCompile",
},
+ {
+ "label": "Build First Person Shooter",
+ "command": "dotnet",
+ "type": "process",
+ "args":
+ [
+ "build",
+ "${workspaceFolder}/Projects/First Person Shooter/First Person Shooter.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary",
+ ],
+ "problemMatcher": "$msCompile",
+ },
{
"label": "Build Solution",
"command": "dotnet",
diff --git a/Projects/First Person Shooter/First Person Shooter.csproj b/Projects/First Person Shooter/First Person Shooter.csproj
new file mode 100644
index 00000000..82273028
--- /dev/null
+++ b/Projects/First Person Shooter/First Person Shooter.csproj
@@ -0,0 +1,9 @@
+
+
+ Exe
+ net8.0
+ disable
+ enable
+ true
+
+
diff --git a/Projects/First Person Shooter/Program.cs b/Projects/First Person Shooter/Program.cs
new file mode 100644
index 00000000..cf509448
--- /dev/null
+++ b/Projects/First Person Shooter/Program.cs
@@ -0,0 +1,823 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+PlayAgain:
+bool closeRequested = false;
+bool screenLargeEnough = true;
+int screenWidth = 120;
+int screenHeight = 40;
+float fov = 3.14159f / 4.0f;
+float depth = 16.0f;
+float speed = 5.0f;
+float rotationSpeed = 0.28f;
+int score = 0;
+float fps = default;
+bool mapVisible = true;
+bool statsVisible = true;
+Weapon equippedWeapon = Weapon.Pistol;
+TimeSpan pistolShootAnimationTime = TimeSpan.FromSeconds(0.2f);
+TimeSpan shotgunShootAnimationTime = TimeSpan.FromSeconds(0.5f);
+TimeSpan gameTime = TimeSpan.FromSeconds(60);
+char[,] screen = new char[screenWidth, screenHeight];
+float[,] depthBuffer = new float[screenWidth, screenHeight];
+List<(float X, float Y)> enemies = new()
+{
+ (13.5f, 09.5f),
+};
+bool gameOver = false;
+bool backToMenu = false;
+
+string[] map =
+[
+ // (0,0) (+,0)
+ "███████████████████████████",
+ "█ ███ █",
+ "█ █ █ █",
+ "█ █ ██ █",
+ "█ █████ █ █",
+ "█ █",
+ "█ ███ █",
+ "█ ██ █",
+ "█ ███ █",
+ "█ █",
+ "█ ██████",
+ "█ ███ ^ █",
+ "█ █",
+ "███████████████████████████",
+ // (0,+) (+,+)
+];
+
+float playerA = default;
+float playerX = default;
+float playerY = default;
+for (int i = 0; i < map.Length; i++)
+{
+ for (int j = 0; j < map[i].Length; j++)
+ {
+ if (map[i][j] is '^' or '<' or '>' or 'v')
+ {
+ playerY = i + .5f;
+ playerX = j + .5f;
+ playerA = map[i][j] switch
+ {
+ '^' => 4.71f,
+ '>' => 0.00f,
+ '<' => 3.14f,
+ 'v' => 1.57f,
+ _ => throw new NotImplementedException(),
+ };
+ }
+ }
+}
+
+string[] enemySprite1 =
+[
+ "!!!!╭─────╮!!!!",
+ "!(O)│ ‾o‾ │(O)!",
+ "╭─╨─╯╔═══╗╰─╨─╮",
+ "│ ╭╮╔╝ ╚╗╭╮ │",
+ "╰─╯╔╝ ╚╗╰─╯",
+ "!!!╚╗ ╔╝!!!",
+ "!!╭╯╚╗ ╔╝╰╮!!",
+ "!!│ ╭╚═══╝╮ │!!",
+ "!!╰─╯!!!!!╰─╯!!",
+];
+
+string[] enemySprite2 =
+[
+ "!!!!╭───────╮!!!!",
+ "!(O)│ ‾o‾ │(O)!",
+ "╭─╨─╯ ╔═══╗ ╰─╨─╮",
+ "│ ╭─╮╔╝ ╚╗╭─╮ │",
+ "╰─╯!╔╝ ╚╗!╰─╯",
+ "!!!!║ ║!!!!",
+ "!!!!╚╗ ╔╝!!!!",
+ "!!!╭╯╚╗ ╔╝╰╮!!!",
+ "!!!│ ╭╚═══╝╮ │!!!",
+ "!!!╰─╯!!!!!╰─╯!!!",
+];
+
+string[] enemySprite3 =
+[
+ "!!╔═╗╭─────────╮╔═╗!!",
+ "!!║O║│ - - │║O║!!",
+ "!!╚╦╝│ O │╚╦╝!!",
+ "╭──╨─╯ ╔═════╗ ╰─╨──╮",
+ "│ ╭─╮╔╝ ╚╗╭─╮ │",
+ "│ │!╔╝ ╚╗!│ │",
+ "╰──╯!║ ║!╰──╯",
+ "!!!!!║ ║!!!!!",
+ "!!!!!╚╗ ╔╝!!!!!",
+ "!!!╭─╯╚╗ ╔╝╰─╮!!!",
+ "!!!│ ╭╚═════╝╮ │!!!",
+ "!!!│ │!!!!!!!│ │!!!",
+ "!!!╰──╯!!!!!!!╰──╯!!!",
+];
+
+string[] enemySprite4 =
+[
+ "!!╔═╗!╭──────────╮!╔═╗!!",
+ "!!║O║!│ - - │!║O║!!",
+ "!!╚╦╝!│ O │!╚╦╝!!",
+ "╭──╨──╯ ╔══════╗ ╰──╨──╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "╰───╯╔╝ ╚╗╰───╯",
+ "!!!!!║ ║!!!!!",
+ "!!!!!╚╗ ╔╝!!!!!",
+ "!!!╭──╚╗ ╔╝──╮!!!",
+ "!!!│ ╚╗ ╔╝ │!!!",
+ "!!!│ ╭╚══════╝╮ │!!!",
+ "!!!│ │!!!!!!!!│ │!!!",
+ "!!!╰───╯!!!!!!!!╰───╯!!!",
+];
+
+string[] enemySprite5 =
+[
+ "!╔═══╗╭────────────╮╔═══╗!",
+ "!║ O ║│ ── ── │║ O ║!",
+ "!╚═╦═╝│ O │╚═╦═╝!",
+ "╭──╨──╯ ╔════════╗ ╰──╨──╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "│ │╔╝ ╚╗│ │",
+ "╰───╯║ ║╰───╯",
+ "!!!!!║ ║!!!!!",
+ "!!!!!║ ║!!!!!",
+ "!!!!!╚╗ ╔╝!!!!!!",
+ "!!╭───╚╗ ╔╝───╮!!",
+ "!!│ ╚╗ ╔╝ │!!",
+ "!!│ ╭╚════════╝╮ │!!",
+ "!!│ │!!!!!!!!!!│ │!!",
+ "!!│ │!!!!!!!!!!│ │!!",
+ "!!╰────╯!!!!!!!!!!╰────╯!!",
+];
+
+string[] enemySprite6 =
+[
+ "!╔═══╗ ╭─────────────╮ ╔═══╗!",
+ "!║ O ║ │ ── ── │ ║ O ║!",
+ "!╚═╦═╝ │ O │ ╚═╦═╝!",
+ "╭──╨───╯ ╔═════════╗ ╰───╨──╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "│ │╔╝ ╚╗│ │",
+ "╰────╯║ ║╰────╯",
+ "!!!!!!║ ║!!!!!!",
+ "!!!!!!║ ║!!!!!!",
+ "!!!!!!║ ║!!!!!!",
+ "!!!!!!╚╗ ╔╝!!!!!!",
+ "!!╭────╚╗ ╔╝────╮!!",
+ "!!│ ╚╗ ╔╝ │!!",
+ "!!│ ╭╚═════════╝╮ │!!",
+ "!!│ │!!!!!!!!!!!│ │!!",
+ "!!│ │!!!!!!!!!!!│ │!!",
+ "!!╰─────╯!!!!!!!!!!!╰─────╯!!",
+];
+
+string[] enemySprite7 =
+[
+ "!!╔═══╗!╭───────────────╮!╔═══╗!!",
+ "!!║ O ║!│ ── ── │!║ O ║!!",
+ "!!╚═╦═╝!│ O │!╚═╦═╝!!",
+ "╭───╨───╯ ╔═══════════╗ ╰───╨───╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "│ │╔╝ ╚╗│ │",
+ "│ │║ ║│ │",
+ "╰─────╯║ ║╰─────╯",
+ "!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!╚╗ ╔╝!!!!!!!",
+ "!!!╭────╚╗ ╔╝────╮!!!",
+ "!!!│ ╚╗ ╔╝ │!!!",
+ "!!!│ ╭╚═══════════╝╮ │!!!",
+ "!!!│ │!!!!!!!!!!!!!│ │!!!",
+ "!!!│ │!!!!!!!!!!!!!│ │!!!",
+ "!!!│ │!!!!!!!!!!!!!│ │!!!",
+ "!!!╰─────╯!!!!!!!!!!!!!╰─────╯!!!",
+];
+
+string[] enemySprite8 =
+[
+ "!!!!!!!!!!╭───────────────────╮!!!!!!!!!!",
+ "!!╔═══╗!!!│ ── ── │!!!╔═══╗!!",
+ "!!║ O ║!!!│ O │!!!║ O ║!!",
+ "!!╚═╦═╝!!!│ │!!!╚═╦═╝!!",
+ "╭───╨─────╯ ╔═══════════════╗ ╰─────╨───╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭──╔╝ ╚╗──╮ │",
+ "│ │!╔╝ ╚╗!│ │",
+ "│ │╔╝ ╚╗│ │",
+ "│ │║ ║│ │",
+ "│ │║ ║│ │",
+ "╰──────╯║ ║╰──────╯",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!╚╗ ╔╝!!!!!!!",
+ "!!!!╭────╚╗ ╔╝────╮!!!",
+ "!!!!│ ╚╗ ╔╝ │!!!",
+ "!!!!│ ╚╗ ╔╝ │!!!",
+ "!!!!│ ╭╚═══════════════╝╮ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!╰──────╯!!!!!!!!!!!!!!!!!╰──────╯!!!",
+];
+
+string[] playerPistol =
+[
+ "!!!╔═╗!!!",
+ "!!!║ ║!!!",
+ "╭─╮║ ║!!!",
+ "│ │╠═╣╭─╮",
+ "│ ╰───╯ │",
+ "│ ───╯",
+ "│ ───╯",
+ "╰╮ ╭──╯!",
+];
+
+string[] playerPistolShoot =
+[
+ @"!!!\V/!!!",
+ @"!!!╔═╗!!!",
+ @"!!!║ ║!!!",
+ @"╭─╮║ ║!!!",
+ @"│ │╠═╣╭─╮",
+ @"│ ╰───╯ │",
+ @"│ ───╯",
+ @"│ ───╯",
+];
+
+string[] playerShotgun =
+[
+ "!!!!!╔═╦═╗!!",
+ "!!!!!║ ║ ║!!",
+ "!!!!!║ ║ ║!!",
+ "!!!!╭║ ║ ║╮!",
+ "!!!!|║ ║ ║╮!",
+ "!!!!|║ ║ ║╮!",
+ "!!!/ ║ ║ ║─╮",
+ "!!/ ╭─╮ ║ │",
+ "!/ /│ ├─╯ │",
+ "/ /!╰╮ ╭─╯",
+];
+
+string[] playerShotgunShoot =
+[
+ @"!!!!\\V|V//!",
+ @"!!!!\\V|V//!",
+ @"!!!!!╔═╦═╗!!",
+ @"!!!!!║ ║ ║!!",
+ @"!!!!!║ ║ ║!!",
+ @"!!!!╭║ ║ ║╮!",
+ @"!!!!|║ ║ ║╮!",
+ @"!!!!|║ ║ ║╮!",
+ @"!!!/ ║ ║ ║─╮",
+ @"!!/ ╭─╮ ║ │",
+ @"!/ /│ ├─╯ │",
+];
+
+int consoleWidth = Console.WindowWidth;
+int consoleHeight = Console.WindowHeight;
+Stopwatch gameTimeStopwatch;
+Stopwatch stopwatch = Stopwatch.StartNew();
+Stopwatch? stopwatchShoot = null;
+Console.OutputEncoding = Encoding.UTF8;
+Console.Clear();
+Console.WriteLine("""
+ First Person Shooter
+
+ This is a first person shooter target range. You have
+ 60 seconds to shoot as many targets as you can. Every
+ time you shoot a target a new one will spawn somewhere
+ in the arena. Good Luck!
+
+ Controls
+ - W, A, S, D: move/look
+ - Spacebar: shoot
+ - 1: equip pistol
+ - 2: equip shotgun
+ - M: toggle map
+ - Tab: toggle stats
+ - Escape: exit
+
+ Press any key to begin...
+ """);
+if (Console.ReadKey(true).Key is not ConsoleKey.Escape)
+{
+ gameTimeStopwatch = Stopwatch.StartNew();
+ Console.Clear();
+ stopwatch = Stopwatch.StartNew();
+ while (!closeRequested)
+ {
+ Update();
+ if (backToMenu)
+ {
+ backToMenu = false;
+ goto PlayAgain;
+ }
+ Render();
+ }
+}
+Console.Clear();
+Console.Write("First Person Shooter was closed.");
+
+void Update()
+{
+ if (gameTimeStopwatch.Elapsed > gameTime)
+ {
+ gameOver = true;
+ gameTimeStopwatch.Stop();
+ }
+
+ bool u = false;
+ bool d = false;
+ bool l = false;
+ bool r = false;
+
+ while (Console.KeyAvailable)
+ {
+ switch (Console.ReadKey(true).Key)
+ {
+ case ConsoleKey.Enter:
+ backToMenu = true;
+ break;
+ case ConsoleKey.Escape: closeRequested = true; return;
+ case ConsoleKey.M:
+ if (!gameOver)
+ {
+ mapVisible = !mapVisible;
+ }
+ break;
+ case ConsoleKey.Tab:
+ if (!gameOver)
+ {
+ statsVisible = !statsVisible;
+ }
+ break;
+ case ConsoleKey.D1 or ConsoleKey.NumPad1:
+ if (!gameOver && PlayerIsNotBusy())
+ {
+ equippedWeapon = Weapon.Pistol;
+ }
+ break;
+ case ConsoleKey.D2 or ConsoleKey.NumPad2:
+ if (!gameOver && PlayerIsNotBusy())
+ {
+ equippedWeapon = Weapon.Shotgun;
+ }
+ break;
+ case ConsoleKey.Spacebar:
+ if (!gameOver && PlayerIsNotBusy())
+ {
+ List<(float X, float Y)> defeatedEnemies = [];
+ bool spawnEnemy = false;
+ foreach (var enemy in enemies)
+ {
+ float angle = (float)Math.Atan2(enemy.Y - playerY, enemy.X - playerX);
+ if (angle < 0) angle += 2f * (float)Math.PI;
+ float distance = Vector2.Distance(new(playerX, playerY), new(enemy.X, enemy.Y));
+
+ float fovAngleA = playerA - fov / 2;
+ if (fovAngleA < 0) fovAngleA += 2 * (float)Math.PI;
+
+ float diff = angle < fovAngleA && fovAngleA - 2f * (float)Math.PI + fov > angle ? angle + 2f * (float)Math.PI - fovAngleA : angle - fovAngleA;
+ float ratio = diff / fov;
+ int enemyScreenX = (int)(screenWidth * ratio);
+
+ string[] enemySprite = distance switch
+ {
+ <= 01f => enemySprite8,
+ <= 02f => enemySprite7,
+ <= 03f => enemySprite6,
+ <= 04f => enemySprite5,
+ <= 05f => enemySprite4,
+ <= 06f => enemySprite3,
+ <= 07f => enemySprite2,
+ _ => enemySprite1
+ };
+
+ int halfEnemyWidth = enemySprite[0].Length / 2;
+ int enemyMinScreenX = enemyScreenX - halfEnemyWidth;
+ int enemyMaxScreenX = enemyScreenX + halfEnemyWidth;
+ int screenWidthMid = screenWidth / 2;
+
+ switch (equippedWeapon)
+ {
+ case Weapon.Pistol:
+ if (enemyMinScreenX <= screenWidthMid && screenWidthMid <= enemyMaxScreenX)
+ {
+ defeatedEnemies.Add(enemy);
+ spawnEnemy = true;
+ }
+ break;
+ case Weapon.Shotgun:
+ if (enemyMinScreenX <= screenWidthMid && screenWidthMid <= enemyMaxScreenX)
+ {
+ defeatedEnemies.Add(enemy);
+ spawnEnemy = true;
+ }
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+ foreach (var enemy in defeatedEnemies)
+ {
+ enemies.Remove(enemy);
+ score++;
+ }
+ if (spawnEnemy)
+ {
+ SpawnTarget();
+ }
+ stopwatchShoot = Stopwatch.StartNew();
+ }
+ break;
+ case ConsoleKey.W:
+ if (!gameOver)
+ {
+ u = true;
+ }
+ break;
+ case ConsoleKey.A:
+ if (!gameOver)
+ {
+ l = true;
+ }
+ break;
+ case ConsoleKey.S:
+ if (!gameOver)
+ {
+ d = true;
+ }
+ break;
+ case ConsoleKey.D:
+ if (!gameOver)
+ {
+ r = true;
+ }
+ break;
+ }
+ }
+
+ if (consoleWidth != Console.WindowWidth || consoleHeight != Console.WindowHeight)
+ {
+ Console.Clear();
+ consoleWidth = Console.WindowWidth;
+ consoleHeight = Console.WindowHeight;
+ }
+
+ screenLargeEnough = consoleWidth >= screenWidth && consoleHeight >= screenHeight;
+ if (!screenLargeEnough)
+ {
+ return;
+ }
+
+ float elapsedSeconds = (float)stopwatch.Elapsed.TotalSeconds;
+ fps = 1.0f / elapsedSeconds;
+ stopwatch.Restart();
+
+ if (OperatingSystem.IsWindows())
+ {
+ u = u || User32_dll.GetAsyncKeyState('W') is not 0 && !gameOver;
+ l = l || User32_dll.GetAsyncKeyState('A') is not 0 && !gameOver;
+ d = d || User32_dll.GetAsyncKeyState('S') is not 0 && !gameOver;
+ r = r || User32_dll.GetAsyncKeyState('D') is not 0 && !gameOver;
+ }
+
+ if (l && !r)
+ {
+ playerA -= (speed * rotationSpeed) * elapsedSeconds;
+ if (playerA < 0)
+ {
+ playerA %= (float)Math.PI * 2;
+ playerA += (float)Math.PI * 2;
+ }
+ }
+ if (r && !l)
+ {
+ playerA += (speed * rotationSpeed) * elapsedSeconds;
+ if (playerA > (float)Math.PI * 2)
+ {
+ playerA %= (float)Math.PI * 2;
+ }
+ }
+ if (u && !d)
+ {
+ playerX += (float)Math.Cos(playerA) * speed * elapsedSeconds;
+ playerY += (float)Math.Sin(playerA) * speed * elapsedSeconds;
+ if (map[(int)playerY][(int)playerX] is '█')
+ {
+ playerX -= (float)Math.Cos(playerA) * speed * elapsedSeconds;
+ playerY -= (float)Math.Sin(playerA) * speed * elapsedSeconds;
+ }
+ }
+ if (d && !u)
+ {
+ playerX -= (float)(Math.Cos(playerA) * speed * elapsedSeconds);
+ playerY -= (float)(Math.Sin(playerA) * speed * elapsedSeconds);
+ if (map[(int)playerY][(int)playerX] is '█')
+ {
+ playerX += (float)Math.Cos(playerA) * speed * elapsedSeconds;
+ playerY += (float)Math.Sin(playerA) * speed * elapsedSeconds;
+ }
+ }
+}
+
+void Render()
+{
+ if (!screenLargeEnough)
+ {
+ Console.CursorVisible = false;
+ Console.SetCursorPosition(0, 0);
+ Console.WriteLine($"Increase console size...");
+ Console.WriteLine($"Current Size: {consoleWidth}x{consoleHeight}");
+ Console.WriteLine($"Minimum Size: {screenWidth}x{screenHeight}");
+ return;
+ }
+
+ for (int y = 0; y < screenHeight; y++)
+ {
+ for (int x = 0; x < screenWidth; x++)
+ {
+ depthBuffer[x, y] = float.MaxValue;
+ }
+ }
+
+ for (int x = 0; x < screenWidth; x++)
+ {
+ float rayAngle = (playerA - fov / 2.0f) + (x / (float)screenWidth) * fov;
+
+ float stepSize = 0.1f;
+ float distanceToWall = 0.0f;
+
+ bool hitWall = false;
+ bool boundary = false;
+
+ float eyeX = (float)Math.Cos(rayAngle);
+ float eyeY = (float)Math.Sin(rayAngle);
+
+ while (!hitWall && distanceToWall < depth)
+ {
+ distanceToWall += stepSize;
+ int testX = (int)(playerX + eyeX * distanceToWall);
+ int testY = (int)(playerY + eyeY * distanceToWall);
+ if (testY < 0 || testY >= map.Length || testX < 0 || testX >= map[testY].Length)
+ {
+ hitWall = true;
+ distanceToWall = depth;
+ }
+ else
+ {
+ if (map[testY][testX] == '█')
+ {
+ hitWall = true;
+ List<(float, float)> p = new();
+ for (int tx = 0; tx < 2; tx++)
+ {
+ for (int ty = 0; ty < 2; ty++)
+ {
+ float vy = (float)testY + ty - playerY;
+ float vx = (float)testX + tx - playerX;
+ float d = (float)Math.Sqrt(vx * vx + vy * vy);
+ float dot = (eyeX * vx / d) + (eyeY * vy / d);
+ p.Add((d, dot));
+ }
+ }
+ p.Sort((a, b) => a.Item1.CompareTo(b.Item1));
+ float fBound = 0.005f;
+ if (Math.Acos(p[0].Item2) < fBound) boundary = true;
+ if (Math.Acos(p[1].Item2) < fBound) boundary = true;
+ if (Math.Acos(p[2].Item2) < fBound) boundary = true;
+ }
+ }
+ }
+ int ceiling = (int)((float)(screenHeight / 2.0f) - screenHeight / ((float)distanceToWall));
+ int floor = screenHeight - ceiling;
+
+ for (int y = 0; y < screenHeight; y++)
+ {
+ depthBuffer[x, y] = distanceToWall;
+
+ if (y <= ceiling)
+ {
+ screen[x, y] = ' ';
+ }
+ else if (y > ceiling && y <= floor)
+ {
+ screen[x, y] =
+ boundary ? ' ' :
+ distanceToWall < depth / 3.00f ? '█' :
+ distanceToWall < depth / 1.75f ? '■' :
+ distanceToWall < depth / 1.00f ? '▪' :
+ ' ';
+ }
+ else
+ {
+ float b = 1.0f - ((y - screenHeight / 2.0f) / (screenHeight / 2.0f));
+ screen[x, y] = b switch
+ {
+ < 0.20f => '●',
+ < 0.40f => '•',
+ < 0.60f => '·',
+ _ => ' ',
+ };
+ }
+ }
+ }
+
+ float fovAngleA = playerA - fov / 2;
+ float fovAngleB = playerA + fov / 2;
+ if (fovAngleA < 0) fovAngleA += 2 * (float)Math.PI;
+
+ foreach (var enemy in enemies)
+ {
+ float angle = (float)Math.Atan2(enemy.Y - playerY, enemy.X - playerX);
+ if (angle < 0) angle += 2f * (float)Math.PI;
+
+ float distance = Vector2.Distance(new(playerX, playerY), new(enemy.X, enemy.Y));
+
+ int ceiling = (int)((float)(screenHeight / 2.0f) - screenHeight / ((float)distance));
+ int floor = screenHeight - ceiling;
+
+ string[] enemySprite = distance switch
+ {
+ <= 01f => enemySprite8,
+ <= 02f => enemySprite7,
+ <= 03f => enemySprite6,
+ <= 04f => enemySprite5,
+ <= 05f => enemySprite4,
+ <= 06f => enemySprite3,
+ <= 07f => enemySprite2,
+ _ => enemySprite1
+ };
+
+ float diff = angle < fovAngleA && fovAngleA - 2f * (float)Math.PI + fov > angle ? angle + 2f * (float)Math.PI - fovAngleA : angle - fovAngleA;
+ float ratio = diff / fov;
+ int enemyScreenX = (int)(screenWidth * ratio);
+ int enemyScreenY = Math.Min(floor, screen.GetLength(1));
+
+ for (int y = 0; y < enemySprite.Length; y++)
+ {
+ for (int x = 0; x < enemySprite[y].Length; x++)
+ {
+ if (enemySprite[y][x] is not '!')
+ {
+ int screenX = x - enemySprite[y].Length / 2 + enemyScreenX;
+ int screenY = y - enemySprite.Length + enemyScreenY;
+ if (0 <= screenX && screenX <= screenWidth - 1 && 0 <= screenY && screenY <= screenHeight - 1 && depthBuffer[screenX, screenY] > distance)
+ {
+ screen[screenX, screenY] = enemySprite[y][x];
+ depthBuffer[screenX, screenY] = distance;
+ }
+ }
+ }
+ }
+ }
+
+ if (statsVisible)
+ {
+ string[] stats =
+ [
+ $"x={playerX:0.00}",
+ $"y={playerY:0.00}",
+ $"a={playerA:0.00}",
+ $"fps={fps:0.}",
+ $"score={score}",
+ $"time={(int)gameTimeStopwatch.Elapsed.TotalSeconds}/{(int)gameTime.TotalSeconds}",
+ ];
+ for (int i = 0; i < stats.Length; i++)
+ {
+ for (int j = 0; j < stats[i].Length; j++)
+ {
+ screen[screenWidth - stats[i].Length + j, i] = stats[i][j];
+ }
+ }
+ }
+
+ if (mapVisible)
+ {
+ for (int y = 0; y < map.Length; y++)
+ {
+ for (int x = 0; x < map[y].Length; x++)
+ {
+ screen[x, y] = map[y][x] is '^' or '<' or '>' or 'v' ? ' ' : map[y][x];
+ }
+ }
+ foreach (var enemy in enemies)
+ {
+ screen[(int)enemy.X, (int)enemy.Y] = 'X';
+ }
+ screen[(int)playerX, (int)playerY] = playerA switch
+ {
+ >= 0.785f and < 2.356f => 'v',
+ >= 2.356f and < 3.927f => '<',
+ >= 3.927f and < 5.498f => '^',
+ _ => '>',
+ };
+ }
+
+ string[] player =
+ equippedWeapon is Weapon.Pistol && stopwatchShoot is not null && stopwatchShoot.Elapsed < pistolShootAnimationTime ? playerPistolShoot :
+ equippedWeapon is Weapon.Shotgun && stopwatchShoot is not null && stopwatchShoot.Elapsed < shotgunShootAnimationTime ? playerShotgunShoot :
+ equippedWeapon is Weapon.Pistol ? playerPistol :
+ equippedWeapon is Weapon.Shotgun ? playerShotgun :
+ throw new NotImplementedException();
+ for (int y = 0; y < player.Length; y++)
+ {
+ for (int x = 0; x < player[y].Length; x++)
+ {
+ if (player[y][x] is not '!')
+ {
+ screen[x + screenWidth / 2 - player[y].Length / 2, screenHeight - player.Length + y] = player[y][x];
+ }
+ }
+ }
+
+ if (gameOver)
+ {
+ string[] gameOverMessage =
+ [
+ $" ",
+ $" GAME OVER! ",
+ $" Score: {score} ",
+ $" Press [enter] to return to menu... ",
+ $" ",
+ ];
+ int gameOverMessageY = screenHeight / 2 - gameOverMessage.Length / 2;
+ foreach (string line in gameOverMessage)
+ {
+ int gameOverMessageX = screenWidth / 2 - line.Length / 2;
+ foreach (char c in line)
+ {
+ screen[gameOverMessageX, gameOverMessageY] = c;
+ gameOverMessageX++;
+ }
+ gameOverMessageY++;
+ }
+ }
+
+ StringBuilder render = new();
+ for (int y = 0; y < screen.GetLength(1); y++)
+ {
+ for (int x = 0; x < screen.GetLength(0); x++)
+ {
+ render.Append(screen[x, y]);
+ }
+ if (y < screen.GetLength(1) - 1)
+ {
+ render.AppendLine();
+ }
+ }
+ Console.CursorVisible = false;
+ Console.SetCursorPosition(0, 0);
+ Console.Write(render);
+}
+
+void SpawnTarget()
+{
+ List<(float X, float Y)> possibleSpawnPoints = [];
+ for (int y = 0; y < map.Length; y++)
+ {
+ for (int x = 0; x < map[y].Length; x++)
+ {
+ if (map[y][x] is ' ')
+ {
+ possibleSpawnPoints.Add((x + .5f, y + .5f));
+ }
+ }
+ }
+ (float X, float Y) location = possibleSpawnPoints[Random.Shared.Next(possibleSpawnPoints.Count)];
+ enemies.Add(location);
+
+}
+
+bool PlayerIsNotBusy() =>
+ stopwatchShoot is null || stopwatchShoot.Elapsed > equippedWeapon switch
+ {
+ Weapon.Pistol => pistolShootAnimationTime,
+ Weapon.Shotgun => shotgunShootAnimationTime,
+ _ => throw new NotImplementedException(),
+ };
+
+partial class User32_dll
+{
+ [LibraryImport("user32.dll")]
+ internal static partial short GetAsyncKeyState(int vKey);
+}
+
+enum Weapon
+{
+ Pistol,
+ Shotgun,
+}
diff --git a/Projects/First Person Shooter/README.md b/Projects/First Person Shooter/README.md
new file mode 100644
index 00000000..bb3b0566
--- /dev/null
+++ b/Projects/First Person Shooter/README.md
@@ -0,0 +1,90 @@
+
+ First Person Shooter
+
+
+
+
+
+
+
+
+
+
+
+
+ You can play this game in your browser:
+
+
+
+
+
+ Hosted On GitHub Pages
+
+
+Play from the first person perspective and shoot some baddies. This is a target range where you have 60 seconds to shoot as many targets as you can. Every time you shoot a target, a new one will spawn somewhere in the arena. Good Luck!
+
+```
+███████████████████████████ x=15.62
+█ ███ █ y=10.35
+█ █ █ █ a=5.62
+█ █ ██ █ fps=2009
+█ █████ █ █ score=3
+█ █ time=26/60
+█ ███ █
+█ ██ X █
+█ ███ █
+█ █
+█ > ██████
+█ ███ ███
+█ ███████████████ ██████
+█████████████████████████████████████████ ██████████████■■■■ ■■■
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■■■■
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■╔═╗╭─────────╮╔═╗
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■║O║│ - - │║O║
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■╚╦╝│ O │╚╦╝ ▪▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■╭──╨─╯ ╔═════╗ ╰─╨──╮▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■│ ╭─╮╔╝ ╚╗╭─╮ │▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■│ │■╔╝ ╚╗▪│ │▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■╰──╯■║ ║▪╰──╯▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■■■║ ║▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■■■╚╗ ╔╝▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■╭─╯╚╗ ╔╝╰─╮▪ ▪▪▪▪▪▪▪▪▪▪▪ ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■│ ╭╚═════╝╮ │
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■│ │ │ │
+███████████████ █████████████████████████ ██████████████■■■■ ■■■■■■■■■■╰──╯ ╰──╯
+███████████████ █████████████████████████ ██████████████■■■■ ■■■
+███████████████ █████████████████████████ ██████·······································································
+·········██████ █████████████···························································································
+························································································································
+•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••╔═╗••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
+•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••║ ║••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
+••••••••••••••••••••••••••••••••••••••••••••••••••••••••╭─╮║ ║••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
+••••••••••••••••••••••••••••••••••••••••••••••••••••••••│ │╠═╣╭─╮•••••••••••••••••••••••••••••••••••••••••••••••••••••••
+●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●│ ╰───╯ │●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
+●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●│ ───╯●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
+●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●│ ───╯●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
+●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●╰╮ ╭──╯●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
+```
+
+## Input
+
+- `W`, `A`, `S`, `D`: move/look
+- `Spacebar`: shoot
+- `1`: equip pistol
+- `2`: equip shotgun
+- `M`: toggle map
+- `Tab`: toggle stats
+- `Escape`: exit
+
+## Credit
+
+This game was originally forked from Javidx9's (aka "One Lone Coder") implementation here:
+https://github.com/OneLoneCoder/CommandLineFPS/blob/master/CommandLineFPS.cpp
+
+## Downloads
+
+[win-x64](https://github.com/dotnet/dotnet-console-games/raw/binaries/win-x64/First%20Person%20Shooter.exe)
+
+~linux-x64~ (not supported)
+
+~osx-x64~ (not supported)
diff --git a/Projects/Website/Games/First Person Shooter/First Person Shooter.cs b/Projects/Website/Games/First Person Shooter/First Person Shooter.cs
new file mode 100644
index 00000000..651815a0
--- /dev/null
+++ b/Projects/Website/Games/First Person Shooter/First Person Shooter.cs
@@ -0,0 +1,844 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Website.Games.First_Person_Shooter;
+
+public class First_Person_Shooter
+{
+ public readonly BlazorConsole Console = new();
+
+ internal static bool ui_w_down = false;
+ internal static bool ui_a_down = false;
+ internal static bool ui_s_down = false;
+ internal static bool ui_d_down = false;
+
+ internal static bool w_down = false;
+ internal static bool a_down = false;
+ internal static bool s_down = false;
+ internal static bool d_down = false;
+
+ public async Task Run()
+ {
+
+ PlayAgain:
+ bool closeRequested = false;
+ bool screenLargeEnough = true;
+ int screenWidth = 120;
+ int screenHeight = 40;
+ float fov = 3.14159f / 4.0f;
+ float depth = 16.0f;
+ float speed = 5.0f;
+ float rotationSpeed = 0.28f;
+ int score = 0;
+ float fps = default;
+ bool mapVisible = true;
+ bool statsVisible = true;
+ Weapon equippedWeapon = Weapon.Pistol;
+ TimeSpan pistolShootAnimationTime = TimeSpan.FromSeconds(0.2f);
+ TimeSpan shotgunShootAnimationTime = TimeSpan.FromSeconds(0.5f);
+ TimeSpan gameTime = TimeSpan.FromSeconds(60);
+ char[,] screen = new char[screenWidth, screenHeight];
+ float[,] depthBuffer = new float[screenWidth, screenHeight];
+ List<(float X, float Y)> enemies = new()
+ {
+ (13.5f, 09.5f),
+ };
+ bool gameOver = false;
+ bool backToMenu = false;
+
+ string[] map =
+ [
+ // (0,0) (+,0)
+ "███████████████████████████",
+ "█ ███ █",
+ "█ █ █ █",
+ "█ █ ██ █",
+ "█ █████ █ █",
+ "█ █",
+ "█ ███ █",
+ "█ ██ █",
+ "█ ███ █",
+ "█ █",
+ "█ ██████",
+ "█ ███ ^ █",
+ "█ █",
+ "███████████████████████████",
+ // (0,+) (+,+)
+ ];
+
+ float playerA = default;
+ float playerX = default;
+ float playerY = default;
+ for (int i = 0; i < map.Length; i++)
+ {
+ for (int j = 0; j < map[i].Length; j++)
+ {
+ if (map[i][j] is '^' or '<' or '>' or 'v')
+ {
+ playerY = i + .5f;
+ playerX = j + .5f;
+ playerA = map[i][j] switch
+ {
+ '^' => 4.71f,
+ '>' => 0.00f,
+ '<' => 3.14f,
+ 'v' => 1.57f,
+ _ => throw new NotImplementedException(),
+ };
+ }
+ }
+ }
+
+ string[] enemySprite1 =
+ [
+ "!!!!╭─────╮!!!!",
+ "!(O)│ ‾o‾ │(O)!",
+ "╭─╨─╯╔═══╗╰─╨─╮",
+ "│ ╭╮╔╝ ╚╗╭╮ │",
+ "╰─╯╔╝ ╚╗╰─╯",
+ "!!!╚╗ ╔╝!!!",
+ "!!╭╯╚╗ ╔╝╰╮!!",
+ "!!│ ╭╚═══╝╮ │!!",
+ "!!╰─╯!!!!!╰─╯!!",
+ ];
+
+ string[] enemySprite2 =
+ [
+ "!!!!╭───────╮!!!!",
+ "!(O)│ ‾o‾ │(O)!",
+ "╭─╨─╯ ╔═══╗ ╰─╨─╮",
+ "│ ╭─╮╔╝ ╚╗╭─╮ │",
+ "╰─╯!╔╝ ╚╗!╰─╯",
+ "!!!!║ ║!!!!",
+ "!!!!╚╗ ╔╝!!!!",
+ "!!!╭╯╚╗ ╔╝╰╮!!!",
+ "!!!│ ╭╚═══╝╮ │!!!",
+ "!!!╰─╯!!!!!╰─╯!!!",
+ ];
+
+ string[] enemySprite3 =
+ [
+ "!!╔═╗╭─────────╮╔═╗!!",
+ "!!║O║│ - - │║O║!!",
+ "!!╚╦╝│ O │╚╦╝!!",
+ "╭──╨─╯ ╔═════╗ ╰─╨──╮",
+ "│ ╭─╮╔╝ ╚╗╭─╮ │",
+ "│ │!╔╝ ╚╗!│ │",
+ "╰──╯!║ ║!╰──╯",
+ "!!!!!║ ║!!!!!",
+ "!!!!!╚╗ ╔╝!!!!!",
+ "!!!╭─╯╚╗ ╔╝╰─╮!!!",
+ "!!!│ ╭╚═════╝╮ │!!!",
+ "!!!│ │!!!!!!!│ │!!!",
+ "!!!╰──╯!!!!!!!╰──╯!!!",
+ ];
+
+ string[] enemySprite4 =
+ [
+ "!!╔═╗!╭──────────╮!╔═╗!!",
+ "!!║O║!│ - - │!║O║!!",
+ "!!╚╦╝!│ O │!╚╦╝!!",
+ "╭──╨──╯ ╔══════╗ ╰──╨──╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "╰───╯╔╝ ╚╗╰───╯",
+ "!!!!!║ ║!!!!!",
+ "!!!!!╚╗ ╔╝!!!!!",
+ "!!!╭──╚╗ ╔╝──╮!!!",
+ "!!!│ ╚╗ ╔╝ │!!!",
+ "!!!│ ╭╚══════╝╮ │!!!",
+ "!!!│ │!!!!!!!!│ │!!!",
+ "!!!╰───╯!!!!!!!!╰───╯!!!",
+ ];
+
+ string[] enemySprite5 =
+ [
+ "!╔═══╗╭────────────╮╔═══╗!",
+ "!║ O ║│ ── ── │║ O ║!",
+ "!╚═╦═╝│ O │╚═╦═╝!",
+ "╭──╨──╯ ╔════════╗ ╰──╨──╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "│ │╔╝ ╚╗│ │",
+ "╰───╯║ ║╰───╯",
+ "!!!!!║ ║!!!!!",
+ "!!!!!║ ║!!!!!",
+ "!!!!!╚╗ ╔╝!!!!!!",
+ "!!╭───╚╗ ╔╝───╮!!",
+ "!!│ ╚╗ ╔╝ │!!",
+ "!!│ ╭╚════════╝╮ │!!",
+ "!!│ │!!!!!!!!!!│ │!!",
+ "!!│ │!!!!!!!!!!│ │!!",
+ "!!╰────╯!!!!!!!!!!╰────╯!!",
+ ];
+
+ string[] enemySprite6 =
+ [
+ "!╔═══╗ ╭─────────────╮ ╔═══╗!",
+ "!║ O ║ │ ── ── │ ║ O ║!",
+ "!╚═╦═╝ │ O │ ╚═╦═╝!",
+ "╭──╨───╯ ╔═════════╗ ╰───╨──╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "│ │╔╝ ╚╗│ │",
+ "╰────╯║ ║╰────╯",
+ "!!!!!!║ ║!!!!!!",
+ "!!!!!!║ ║!!!!!!",
+ "!!!!!!║ ║!!!!!!",
+ "!!!!!!╚╗ ╔╝!!!!!!",
+ "!!╭────╚╗ ╔╝────╮!!",
+ "!!│ ╚╗ ╔╝ │!!",
+ "!!│ ╭╚═════════╝╮ │!!",
+ "!!│ │!!!!!!!!!!!│ │!!",
+ "!!│ │!!!!!!!!!!!│ │!!",
+ "!!╰─────╯!!!!!!!!!!!╰─────╯!!",
+ ];
+
+ string[] enemySprite7 =
+ [
+ "!!╔═══╗!╭───────────────╮!╔═══╗!!",
+ "!!║ O ║!│ ── ── │!║ O ║!!",
+ "!!╚═╦═╝!│ O │!╚═╦═╝!!",
+ "╭───╨───╯ ╔═══════════╗ ╰───╨───╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭─╔╝ ╚╗─╮ │",
+ "│ │╔╝ ╚╗│ │",
+ "│ │║ ║│ │",
+ "╰─────╯║ ║╰─────╯",
+ "!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!╚╗ ╔╝!!!!!!!",
+ "!!!╭────╚╗ ╔╝────╮!!!",
+ "!!!│ ╚╗ ╔╝ │!!!",
+ "!!!│ ╭╚═══════════╝╮ │!!!",
+ "!!!│ │!!!!!!!!!!!!!│ │!!!",
+ "!!!│ │!!!!!!!!!!!!!│ │!!!",
+ "!!!│ │!!!!!!!!!!!!!│ │!!!",
+ "!!!╰─────╯!!!!!!!!!!!!!╰─────╯!!!",
+ ];
+
+ string[] enemySprite8 =
+ [
+ "!!!!!!!!!!╭───────────────────╮!!!!!!!!!!",
+ "!!╔═══╗!!!│ ── ── │!!!╔═══╗!!",
+ "!!║ O ║!!!│ O │!!!║ O ║!!",
+ "!!╚═╦═╝!!!│ │!!!╚═╦═╝!!",
+ "╭───╨─────╯ ╔═══════════════╗ ╰─────╨───╮",
+ "│ ╔╝ ╚╗ │",
+ "│ ╭──╔╝ ╚╗──╮ │",
+ "│ │!╔╝ ╚╗!│ │",
+ "│ │╔╝ ╚╗│ │",
+ "│ │║ ║│ │",
+ "│ │║ ║│ │",
+ "╰──────╯║ ║╰──────╯",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!║ ║!!!!!!!",
+ "!!!!!!!!╚╗ ╔╝!!!!!!!",
+ "!!!!╭────╚╗ ╔╝────╮!!!",
+ "!!!!│ ╚╗ ╔╝ │!!!",
+ "!!!!│ ╚╗ ╔╝ │!!!",
+ "!!!!│ ╭╚═══════════════╝╮ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!│ │!!!!!!!!!!!!!!!!!│ │!!!",
+ "!!!!╰──────╯!!!!!!!!!!!!!!!!!╰──────╯!!!",
+ ];
+
+ string[] playerPistol =
+ [
+ "!!!╔═╗!!!",
+ "!!!║ ║!!!",
+ "╭─╮║ ║!!!",
+ "│ │╠═╣╭─╮",
+ "│ ╰───╯ │",
+ "│ ───╯",
+ "│ ───╯",
+ "╰╮ ╭──╯!",
+ ];
+
+ string[] playerPistolShoot =
+ [
+ @"!!!\V/!!!",
+ @"!!!╔═╗!!!",
+ @"!!!║ ║!!!",
+ @"╭─╮║ ║!!!",
+ @"│ │╠═╣╭─╮",
+ @"│ ╰───╯ │",
+ @"│ ───╯",
+ @"│ ───╯",
+ ];
+
+ string[] playerShotgun =
+ [
+ "!!!!!╔═╦═╗!!",
+ "!!!!!║ ║ ║!!",
+ "!!!!!║ ║ ║!!",
+ "!!!!╭║ ║ ║╮!",
+ "!!!!|║ ║ ║╮!",
+ "!!!!|║ ║ ║╮!",
+ "!!!/ ║ ║ ║─╮",
+ "!!/ ╭─╮ ║ │",
+ "!/ /│ ├─╯ │",
+ "/ /!╰╮ ╭─╯",
+ ];
+
+ string[] playerShotgunShoot =
+ [
+ @"!!!!\\V|V//!",
+ @"!!!!\\V|V//!",
+ @"!!!!!╔═╦═╗!!",
+ @"!!!!!║ ║ ║!!",
+ @"!!!!!║ ║ ║!!",
+ @"!!!!╭║ ║ ║╮!",
+ @"!!!!|║ ║ ║╮!",
+ @"!!!!|║ ║ ║╮!",
+ @"!!!/ ║ ║ ║─╮",
+ @"!!/ ╭─╮ ║ │",
+ @"!/ /│ ├─╯ │",
+ ];
+
+ int consoleWidth = Console.WindowWidth;
+ int consoleHeight = Console.WindowHeight;
+ Stopwatch gameTimeStopwatch;
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ Stopwatch? stopwatchShoot = null;
+ Console.OutputEncoding = Encoding.UTF8;
+ await Console.Clear();
+ await Console.WriteLine("""
+ First Person Shooter
+
+ This is a first person shooter target range. You have
+ 60 seconds to shoot as many targets as you can. Every
+ time you shoot a target a new one will spawn somewhere
+ in the arena. Good Luck!
+
+ Controls
+ - W, A, S, D: move/look
+ - Spacebar: shoot
+ - 1: equip pistol
+ - 2: equip shotgun
+ - M: toggle map
+ - Tab: toggle stats
+ - Escape: exit
+
+ Press any key to begin...
+ """);
+ if ((await Console.ReadKey(true)).Key is not ConsoleKey.Escape)
+ {
+ gameTimeStopwatch = Stopwatch.StartNew();
+ await Console.Clear();
+ stopwatch = Stopwatch.StartNew();
+ while (!closeRequested)
+ {
+ await Update();
+ if (backToMenu)
+ {
+ backToMenu = false;
+ goto PlayAgain;
+ }
+ await Render();
+ }
+ }
+ await Console.Clear();
+ await Console.Write("First Person Shooter was closed.");
+ await Console.Refresh();
+
+ async Task Update()
+ {
+ if (gameTimeStopwatch.Elapsed > gameTime)
+ {
+ gameOver = true;
+ gameTimeStopwatch.Stop();
+ }
+
+ bool u = false;
+ bool d = false;
+ bool l = false;
+ bool r = false;
+
+ while (await Console.KeyAvailable())
+ {
+ switch ((await Console.ReadKey(true)).Key)
+ {
+ case ConsoleKey.Enter:
+ backToMenu = true;
+ break;
+ case ConsoleKey.Escape: closeRequested = true; return;
+ case ConsoleKey.M:
+ if (!gameOver)
+ {
+ mapVisible = !mapVisible;
+ }
+ break;
+ case ConsoleKey.Tab:
+ if (!gameOver)
+ {
+ statsVisible = !statsVisible;
+ }
+ break;
+ case ConsoleKey.D1 or ConsoleKey.NumPad1:
+ if (!gameOver && PlayerIsNotBusy())
+ {
+ equippedWeapon = Weapon.Pistol;
+ }
+ break;
+ case ConsoleKey.D2 or ConsoleKey.NumPad2:
+ if (!gameOver && PlayerIsNotBusy())
+ {
+ equippedWeapon = Weapon.Shotgun;
+ }
+ break;
+ case ConsoleKey.Spacebar:
+ if (!gameOver && PlayerIsNotBusy())
+ {
+ List<(float X, float Y)> defeatedEnemies = [];
+ bool spawnEnemy = false;
+ foreach (var enemy in enemies)
+ {
+ float angle = (float)Math.Atan2(enemy.Y - playerY, enemy.X - playerX);
+ if (angle < 0) angle += 2f * (float)Math.PI;
+ float distance = Vector2.Distance(new(playerX, playerY), new(enemy.X, enemy.Y));
+
+ float fovAngleA = playerA - fov / 2;
+ if (fovAngleA < 0) fovAngleA += 2 * (float)Math.PI;
+
+ float diff = angle < fovAngleA && fovAngleA - 2f * (float)Math.PI + fov > angle ? angle + 2f * (float)Math.PI - fovAngleA : angle - fovAngleA;
+ float ratio = diff / fov;
+ int enemyScreenX = (int)(screenWidth * ratio);
+
+ string[] enemySprite = distance switch
+ {
+ <= 01f => enemySprite8,
+ <= 02f => enemySprite7,
+ <= 03f => enemySprite6,
+ <= 04f => enemySprite5,
+ <= 05f => enemySprite4,
+ <= 06f => enemySprite3,
+ <= 07f => enemySprite2,
+ _ => enemySprite1
+ };
+
+ int halfEnemyWidth = enemySprite[0].Length / 2;
+ int enemyMinScreenX = enemyScreenX - halfEnemyWidth;
+ int enemyMaxScreenX = enemyScreenX + halfEnemyWidth;
+ int screenWidthMid = screenWidth / 2;
+
+ switch (equippedWeapon)
+ {
+ case Weapon.Pistol:
+ if (enemyMinScreenX <= screenWidthMid && screenWidthMid <= enemyMaxScreenX)
+ {
+ defeatedEnemies.Add(enemy);
+ spawnEnemy = true;
+ }
+ break;
+ case Weapon.Shotgun:
+ if (enemyMinScreenX <= screenWidthMid && screenWidthMid <= enemyMaxScreenX)
+ {
+ defeatedEnemies.Add(enemy);
+ spawnEnemy = true;
+ }
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+ foreach (var enemy in defeatedEnemies)
+ {
+ enemies.Remove(enemy);
+ score++;
+ }
+ if (spawnEnemy)
+ {
+ SpawnTarget();
+ }
+ stopwatchShoot = Stopwatch.StartNew();
+ }
+ break;
+ case ConsoleKey.W:
+ if (!gameOver)
+ {
+ u = true;
+ }
+ break;
+ case ConsoleKey.A:
+ if (!gameOver)
+ {
+ l = true;
+ }
+ break;
+ case ConsoleKey.S:
+ if (!gameOver)
+ {
+ d = true;
+ }
+ break;
+ case ConsoleKey.D:
+ if (!gameOver)
+ {
+ r = true;
+ }
+ break;
+ }
+ }
+
+ if (consoleWidth != Console.WindowWidth || consoleHeight != Console.WindowHeight)
+ {
+ await Console.Clear();
+ consoleWidth = Console.WindowWidth;
+ consoleHeight = Console.WindowHeight;
+ }
+
+ screenLargeEnough = consoleWidth >= screenWidth && consoleHeight >= screenHeight;
+ if (!screenLargeEnough)
+ {
+ return;
+ }
+
+ float elapsedSeconds = (float)stopwatch.Elapsed.TotalSeconds;
+ fps = 1.0f / elapsedSeconds;
+ stopwatch.Restart();
+
+ //if (OperatingSystem.IsWindows())
+ //{
+ // u = u || User32_dll.GetAsyncKeyState('W') is not 0 && !gameOver;
+ // l = l || User32_dll.GetAsyncKeyState('A') is not 0 && !gameOver;
+ // d = d || User32_dll.GetAsyncKeyState('S') is not 0 && !gameOver;
+ // r = r || User32_dll.GetAsyncKeyState('D') is not 0 && !gameOver;
+ //}
+
+ u = (u || ui_w_down || w_down) && !gameOver;
+ l = (l || ui_a_down || a_down) && !gameOver;
+ d = (d || ui_s_down || s_down) && !gameOver;
+ r = (r || ui_d_down || d_down) && !gameOver;
+
+ if (l && !r)
+ {
+ playerA -= (speed * rotationSpeed) * elapsedSeconds;
+ if (playerA < 0)
+ {
+ playerA %= (float)Math.PI * 2;
+ playerA += (float)Math.PI * 2;
+ }
+ }
+ if (r && !l)
+ {
+ playerA += (speed * rotationSpeed) * elapsedSeconds;
+ if (playerA > (float)Math.PI * 2)
+ {
+ playerA %= (float)Math.PI * 2;
+ }
+ }
+ if (u && !d)
+ {
+ playerX += (float)Math.Cos(playerA) * speed * elapsedSeconds;
+ playerY += (float)Math.Sin(playerA) * speed * elapsedSeconds;
+ if (map[(int)playerY][(int)playerX] is '█')
+ {
+ playerX -= (float)Math.Cos(playerA) * speed * elapsedSeconds;
+ playerY -= (float)Math.Sin(playerA) * speed * elapsedSeconds;
+ }
+ }
+ if (d && !u)
+ {
+ playerX -= (float)(Math.Cos(playerA) * speed * elapsedSeconds);
+ playerY -= (float)(Math.Sin(playerA) * speed * elapsedSeconds);
+ if (map[(int)playerY][(int)playerX] is '█')
+ {
+ playerX += (float)Math.Cos(playerA) * speed * elapsedSeconds;
+ playerY += (float)Math.Sin(playerA) * speed * elapsedSeconds;
+ }
+ }
+ }
+
+ async Task Render()
+ {
+ if (!screenLargeEnough)
+ {
+ Console.CursorVisible = false;
+ await Console.SetCursorPosition(0, 0);
+ await Console.WriteLine($"Increase console size...");
+ await Console.WriteLine($"Current Size: {consoleWidth}x{consoleHeight}");
+ await Console.WriteLine($"Minimum Size: {screenWidth}x{screenHeight}");
+ return;
+ }
+
+ for (int y = 0; y < screenHeight; y++)
+ {
+ for (int x = 0; x < screenWidth; x++)
+ {
+ depthBuffer[x, y] = float.MaxValue;
+ }
+ }
+
+ for (int x = 0; x < screenWidth; x++)
+ {
+ float rayAngle = (playerA - fov / 2.0f) + (x / (float)screenWidth) * fov;
+
+ float stepSize = 0.1f;
+ float distanceToWall = 0.0f;
+
+ bool hitWall = false;
+ bool boundary = false;
+
+ float eyeX = (float)Math.Cos(rayAngle);
+ float eyeY = (float)Math.Sin(rayAngle);
+
+ while (!hitWall && distanceToWall < depth)
+ {
+ distanceToWall += stepSize;
+ int testX = (int)(playerX + eyeX * distanceToWall);
+ int testY = (int)(playerY + eyeY * distanceToWall);
+ if (testY < 0 || testY >= map.Length || testX < 0 || testX >= map[testY].Length)
+ {
+ hitWall = true;
+ distanceToWall = depth;
+ }
+ else
+ {
+ if (map[testY][testX] == '█')
+ {
+ hitWall = true;
+ List<(float, float)> p = new();
+ for (int tx = 0; tx < 2; tx++)
+ {
+ for (int ty = 0; ty < 2; ty++)
+ {
+ float vy = (float)testY + ty - playerY;
+ float vx = (float)testX + tx - playerX;
+ float d = (float)Math.Sqrt(vx * vx + vy * vy);
+ float dot = (eyeX * vx / d) + (eyeY * vy / d);
+ p.Add((d, dot));
+ }
+ }
+ p.Sort((a, b) => a.Item1.CompareTo(b.Item1));
+ float fBound = 0.005f;
+ if (Math.Acos(p[0].Item2) < fBound) boundary = true;
+ if (Math.Acos(p[1].Item2) < fBound) boundary = true;
+ if (Math.Acos(p[2].Item2) < fBound) boundary = true;
+ }
+ }
+ }
+ int ceiling = (int)((float)(screenHeight / 2.0f) - screenHeight / ((float)distanceToWall));
+ int floor = screenHeight - ceiling;
+
+ for (int y = 0; y < screenHeight; y++)
+ {
+ depthBuffer[x, y] = distanceToWall;
+
+ if (y <= ceiling)
+ {
+ screen[x, y] = ' ';
+ }
+ else if (y > ceiling && y <= floor)
+ {
+ screen[x, y] =
+ boundary ? ' ' :
+ distanceToWall < depth / 3.00f ? '█' :
+ distanceToWall < depth / 1.75f ? '■' :
+ distanceToWall < depth / 1.00f ? '▪' :
+ ' ';
+ }
+ else
+ {
+ float b = 1.0f - ((y - screenHeight / 2.0f) / (screenHeight / 2.0f));
+ screen[x, y] = b switch
+ {
+ < 0.20f => '●',
+ < 0.40f => '•',
+ < 0.60f => '·',
+ _ => ' ',
+ };
+ }
+ }
+ }
+
+ float fovAngleA = playerA - fov / 2;
+ float fovAngleB = playerA + fov / 2;
+ if (fovAngleA < 0) fovAngleA += 2 * (float)Math.PI;
+
+ foreach (var enemy in enemies)
+ {
+ float angle = (float)Math.Atan2(enemy.Y - playerY, enemy.X - playerX);
+ if (angle < 0) angle += 2f * (float)Math.PI;
+
+ float distance = Vector2.Distance(new(playerX, playerY), new(enemy.X, enemy.Y));
+
+ int ceiling = (int)((float)(screenHeight / 2.0f) - screenHeight / ((float)distance));
+ int floor = screenHeight - ceiling;
+
+ string[] enemySprite = distance switch
+ {
+ <= 01f => enemySprite8,
+ <= 02f => enemySprite7,
+ <= 03f => enemySprite6,
+ <= 04f => enemySprite5,
+ <= 05f => enemySprite4,
+ <= 06f => enemySprite3,
+ <= 07f => enemySprite2,
+ _ => enemySprite1
+ };
+
+ float diff = angle < fovAngleA && fovAngleA - 2f * (float)Math.PI + fov > angle ? angle + 2f * (float)Math.PI - fovAngleA : angle - fovAngleA;
+ float ratio = diff / fov;
+ int enemyScreenX = (int)(screenWidth * ratio);
+ int enemyScreenY = Math.Min(floor, screen.GetLength(1));
+
+ for (int y = 0; y < enemySprite.Length; y++)
+ {
+ for (int x = 0; x < enemySprite[y].Length; x++)
+ {
+ if (enemySprite[y][x] is not '!')
+ {
+ int screenX = x - enemySprite[y].Length / 2 + enemyScreenX;
+ int screenY = y - enemySprite.Length + enemyScreenY;
+ if (0 <= screenX && screenX <= screenWidth - 1 && 0 <= screenY && screenY <= screenHeight - 1 && depthBuffer[screenX, screenY] > distance)
+ {
+ screen[screenX, screenY] = enemySprite[y][x];
+ depthBuffer[screenX, screenY] = distance;
+ }
+ }
+ }
+ }
+ }
+
+ if (statsVisible)
+ {
+ string[] stats =
+ [
+ $"x={playerX:0.00}",
+ $"y={playerY:0.00}",
+ $"a={playerA:0.00}",
+ $"fps={fps:0.}",
+ $"score={score}",
+ $"time={(int)gameTimeStopwatch.Elapsed.TotalSeconds}/{(int)gameTime.TotalSeconds}",
+ ];
+ for (int i = 0; i < stats.Length; i++)
+ {
+ for (int j = 0; j < stats[i].Length; j++)
+ {
+ screen[screenWidth - stats[i].Length + j, i] = stats[i][j];
+ }
+ }
+ }
+
+ if (mapVisible)
+ {
+ for (int y = 0; y < map.Length; y++)
+ {
+ for (int x = 0; x < map[y].Length; x++)
+ {
+ screen[x, y] = map[y][x] is '^' or '<' or '>' or 'v' ? ' ' : map[y][x];
+ }
+ }
+ foreach (var enemy in enemies)
+ {
+ screen[(int)enemy.X, (int)enemy.Y] = 'X';
+ }
+ screen[(int)playerX, (int)playerY] = playerA switch
+ {
+ >= 0.785f and < 2.356f => 'v',
+ >= 2.356f and < 3.927f => '<',
+ >= 3.927f and < 5.498f => '^',
+ _ => '>',
+ };
+ }
+
+ string[] player =
+ equippedWeapon is Weapon.Pistol && stopwatchShoot is not null && stopwatchShoot.Elapsed < pistolShootAnimationTime ? playerPistolShoot :
+ equippedWeapon is Weapon.Shotgun && stopwatchShoot is not null && stopwatchShoot.Elapsed < shotgunShootAnimationTime ? playerShotgunShoot :
+ equippedWeapon is Weapon.Pistol ? playerPistol :
+ equippedWeapon is Weapon.Shotgun ? playerShotgun :
+ throw new NotImplementedException();
+ for (int y = 0; y < player.Length; y++)
+ {
+ for (int x = 0; x < player[y].Length; x++)
+ {
+ if (player[y][x] is not '!')
+ {
+ screen[x + screenWidth / 2 - player[y].Length / 2, screenHeight - player.Length + y] = player[y][x];
+ }
+ }
+ }
+
+ if (gameOver)
+ {
+ string[] gameOverMessage =
+ [
+ $" ",
+ $" GAME OVER! ",
+ $" Score: {score} ",
+ $" Press [enter] to return to menu... ",
+ $" ",
+ ];
+ int gameOverMessageY = screenHeight / 2 - gameOverMessage.Length / 2;
+ foreach (string line in gameOverMessage)
+ {
+ int gameOverMessageX = screenWidth / 2 - line.Length / 2;
+ foreach (char c in line)
+ {
+ screen[gameOverMessageX, gameOverMessageY] = c;
+ gameOverMessageX++;
+ }
+ gameOverMessageY++;
+ }
+ }
+
+ StringBuilder render = new();
+ for (int y = 0; y < screen.GetLength(1); y++)
+ {
+ for (int x = 0; x < screen.GetLength(0); x++)
+ {
+ render.Append(screen[x, y]);
+ }
+ if (y < screen.GetLength(1) - 1)
+ {
+ render.AppendLine();
+ }
+ }
+ Console.CursorVisible = false;
+ await Console.SetCursorPosition(0, 0);
+ await Console.Write(render);
+ }
+
+ void SpawnTarget()
+ {
+ List<(float X, float Y)> possibleSpawnPoints = [];
+ for (int y = 0; y < map.Length; y++)
+ {
+ for (int x = 0; x < map[y].Length; x++)
+ {
+ if (map[y][x] is ' ')
+ {
+ possibleSpawnPoints.Add((x + .5f, y + .5f));
+ }
+ }
+ }
+ (float X, float Y) location = possibleSpawnPoints[Random.Shared.Next(possibleSpawnPoints.Count)];
+ enemies.Add(location);
+
+ }
+
+ bool PlayerIsNotBusy() =>
+ stopwatchShoot is null || stopwatchShoot.Elapsed > equippedWeapon switch
+ {
+ Weapon.Pistol => pistolShootAnimationTime,
+ Weapon.Shotgun => shotgunShootAnimationTime,
+ _ => throw new NotImplementedException(),
+ };
+ }
+
+ enum Weapon
+ {
+ Pistol,
+ Shotgun,
+ }
+}
diff --git a/Projects/Website/Games/Reversi/Reversi.cs b/Projects/Website/Games/Reversi/Reversi.cs
index a7de957d..685630ae 100644
--- a/Projects/Website/Games/Reversi/Reversi.cs
+++ b/Projects/Website/Games/Reversi/Reversi.cs
@@ -206,6 +206,7 @@ Your opponent played a piece.
await Console.Clear();
await Console.WriteLine("Reversi was closed.");
Console.CursorVisible = true;
+ await Console.Refresh();
void InitializeBoard()
{
diff --git a/Projects/Website/Pages/First Person Shooter.razor b/Projects/Website/Pages/First Person Shooter.razor
new file mode 100644
index 00000000..08d90c7f
--- /dev/null
+++ b/Projects/Website/Pages/First Person Shooter.razor
@@ -0,0 +1,81 @@
+@using System
+
+@page "/First Person Shooter"
+
+First Person Shooter
+
+First Person Shooter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⌨ Keyboard input is supported if you click on the game.
+
+
+
+ ↻ You can restart the game by refreshing the page.
+
+
+@code
+{
+ Games.First_Person_Shooter.First_Person_Shooter Game;
+ BlazorConsole Console;
+
+ public First_Person_Shooter()
+ {
+ Game = new();
+ Console = Game.Console;
+ Console.WindowWidth = 121;
+ Console.WindowHeight = 41;
+ Console.TriggerRefresh = StateHasChanged;
+ }
+
+ public void OnKeyDown(KeyboardEventArgs e)
+ {
+ Console.OnKeyDown(e);
+ switch (e.Key)
+ {
+ case "w": Games.First_Person_Shooter.First_Person_Shooter.w_down = true; break;
+ case "a": Games.First_Person_Shooter.First_Person_Shooter.a_down = true; break;
+ case "s": Games.First_Person_Shooter.First_Person_Shooter.s_down = true; break;
+ case "d": Games.First_Person_Shooter.First_Person_Shooter.d_down = true; break;
+ }
+ }
+
+ public void OnKeyUp(KeyboardEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case "w": Games.First_Person_Shooter.First_Person_Shooter.w_down = false; break;
+ case "a": Games.First_Person_Shooter.First_Person_Shooter.a_down = false; break;
+ case "s": Games.First_Person_Shooter.First_Person_Shooter.s_down = false; break;
+ case "d": Games.First_Person_Shooter.First_Person_Shooter.d_down = false; break;
+ }
+ }
+
+ protected override void OnInitialized() => InvokeAsync(Game.Run);
+}
diff --git a/Projects/Website/Shared/NavMenu.razor b/Projects/Website/Shared/NavMenu.razor
index 357c01ad..fd3f3cb0 100644
--- a/Projects/Website/Shared/NavMenu.razor
+++ b/Projects/Website/Shared/NavMenu.razor
@@ -253,6 +253,11 @@
Tetris
+
+
+ Shmup
+
+
Role Playing Game
@@ -264,8 +269,8 @@
-
- Shmup
+
+ First Person Shooter
diff --git a/README.md b/README.md
index da096fbe..15d6e085 100644
--- a/README.md
+++ b/README.md
@@ -74,9 +74,10 @@
|[PacMan](Projects/PacMan)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/PacMan) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/PacMan%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)|
|[Gravity](Projects/Gravity)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Gravity) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Gravity%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)|
|[Tetris](Projects/Tetris)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Tetris) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Tetris%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_[Community Contribution](https://github.com/dotnet/dotnet-console-games/pull/89)_|
+|[Shmup](Projects/Shmup)|5|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Shmup) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Shmup%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Work In Progress_
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Only Supported On Windows OS_|
|[Role Playing Game](Projects/Role%20Playing%20Game)|6|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Role%20Playing%20Game) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Role%20Playing%20Game%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)|
|[Console Monsters](Projects/Console%20Monsters)|7|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Console%20Monsters) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Console%20Monsters%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
*_Community Collaboration_
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Work In Progress_|
-|[Shmup](Projects/Shmup)|?|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/Shmup) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/Shmup%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Work In Progress_
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Only Supported On Windows OS_|
+|[First Person Shooter](Projects/First%20Person%20Shooter)|8|[![Play Now](.github/resources/play-badge.svg)](https://dotnet.github.io/dotnet-console-games/First%20Person%20Shooter) [![Status](https://github.com/dotnet/dotnet-console-games/workflows/First%20Person%20Shooter%20Build/badge.svg)](https://github.com/dotnet/dotnet-console-games/actions)
[![Warning](https://raw.githubusercontent.com/dotnet/dotnet-console-games/main/.github/resources/warning-icon.svg)](#) _Only Supported On Windows OS_|
\*_**Weight**: A relative rating for how advanced the source code is._
diff --git a/dotnet-console-games.sln b/dotnet-console-games.sln
index 2d33927d..19180f80 100644
--- a/dotnet-console-games.sln
+++ b/dotnet-console-games.sln
@@ -111,6 +111,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lights Out", "Projects\Ligh
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reversi", "Projects\Reversi\Reversi.csproj", "{8D5244E1-C54E-4909-A9DE-0DE7E683D911}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "First Person Shooter", "Projects\First Person Shooter\First Person Shooter.csproj", "{5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -333,6 +335,10 @@ Global
{8D5244E1-C54E-4909-A9DE-0DE7E683D911}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D5244E1-C54E-4909-A9DE-0DE7E683D911}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D5244E1-C54E-4909-A9DE-0DE7E683D911}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5A18DEF8-A8C3-4B5B-B127-9BA0A0767287}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/dotnet-console-games.slnf b/dotnet-console-games.slnf
index 725d1db1..6c8623c5 100644
--- a/dotnet-console-games.slnf
+++ b/dotnet-console-games.slnf
@@ -17,6 +17,7 @@
"Projects\\Drive\\Drive.csproj",
"Projects\\Duck Hunt\\Duck Hunt.csproj",
"Projects\\Fighter\\Fighter.csproj",
+ "Projects\\First Person Shooter\\First Person Shooter.csproj",
"Projects\\Flappy Bird\\Flappy Bird.csproj",
"Projects\\Flash Cards\\Flash Cards.csproj",
"Projects\\Guess A Number\\Guess A Number.csproj",