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 +

+ +

+ flat + Language C# + Target Framework + Build + Discord + +

+ +

+ You can play this game in your browser: +
+ + Play Now + +
+ 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

+ + + Go To Readme + + +
+
+
+			@Console.State
+		
+
+
+ + + + + + + + + + + + +
+
+ + + + + +@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 + 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",