Created
October 5, 2025 15:30
-
-
Save corlaez/3e25d4f31a097e6777805ec3797b2e6e to your computer and use it in GitHub Desktop.
Juego 1 nivel. Disparos y game over.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <iostream> | |
| #include <vector> | |
| #include <string> | |
| #include <map> | |
| #include <cstdlib> // For system() (e.g., cls or clear) | |
| #include <ctime> // Explicitly include for time(0) | |
| #include <chrono> // For timing/sleep (optional) | |
| #include <thread> // For timing/sleep (optional) | |
| #include <algorithm> // For std::find_if, std::min | |
| #include <cmath> // For std::abs | |
| #include <cctype> // For std::tolower | |
| #include <cstdio> // For putchar/printf | |
| #include <queue> // For BFS pathfinding | |
| #include <tuple> // For BFS pathfinding map key/value | |
| #include <set> // For unique destruction in collision system | |
| // Note: conio.h is included conditionally for Windows input functions | |
| #ifdef _WIN32 | |
| #include <conio.h> | |
| #define NOMINMAX // Ensure min/max macros don't conflict with std::min/max | |
| #include <windows.h> // For console functions | |
| #endif | |
| // --- ECS CORE STRUCTURE --- | |
| // Component IDs - Simple integer handles for entities | |
| using EntityID = int; | |
| // Global ID counter | |
| EntityID nextEntityID = 0; | |
| // Global component pools (declarations needed before DestroyEntity and Systems) | |
| std::map<EntityID, struct PositionComponent> positions; | |
| std::map<EntityID, struct CharacterTagComponent> characterTags; | |
| std::map<EntityID, struct VelocityComponent> velocities; | |
| std::map<EntityID, struct MoveDelayComponent> moveDelays; // Delay pool | |
| // Global entity ID for the player for easy access in systems | |
| EntityID playerEntityID = -1; | |
| // Function to create a new entity ID | |
| EntityID CreateEntity() { | |
| return nextEntityID++; | |
| } | |
| // Helper function to remove components of an entity (simple cleanup) | |
| void DestroyEntity(EntityID id) { | |
| // Erase components from all pools they might belong to | |
| positions.erase(id); | |
| characterTags.erase(id); | |
| velocities.erase(id); | |
| moveDelays.erase(id); | |
| } | |
| // --- CONSTANTS AND ENUMS --- | |
| // Console Colors using ANSI Escape Codes | |
| #define ANSI_COLOR_RESET "\033[0m" | |
| // Standard 16-color blue, white, red, yellow, cyan | |
| #define ANSI_COLOR_BLUE "\033[34m" | |
| #define ANSI_COLOR_WHITE "\033[37m" | |
| #define ANSI_COLOR_RED "\033[31m" | |
| #define ANSI_COLOR_YELLOW "\033[33m" | |
| #define ANSI_COLOR_CYAN "\033[36m" | |
| // --- Two Tones of Green (using 256-color codes for better differentiation) --- | |
| // ID 40 is a slightly dull, regular green (good for walkable grass/TIERRA) | |
| #define ANSI_COLOR_LIGHT_GREEN "\033[38;5;40m" | |
| // ID 22 is a deep, forest/dark green (good for non-walkable leaves/HOJA_ARBOL) | |
| #define ANSI_COLOR_DARK_GREEN "\033[38;5;22m" | |
| // Map Dimensions (Updated to 80x35) | |
| const int MAP_WIDTH = 80; | |
| const int MAP_HEIGHT = 35; | |
| // Tile Definitions based on user input | |
| const int TIERRA = 3; // Walkable | |
| const int AGUA = 2; // Not-Walkable | |
| const int HOJA_ARBOL = 5; // Not-Walkable | |
| const int TRONCO = 6; // Not-Walkable | |
| // The solid block character for the map background | |
| const char SOLID_BLOCK_SYMBOL = (char)219; | |
| // Character Model (3x3 array) | |
| const std::string CHARACTER_MODEL[3] = { | |
| " O ", | |
| "/|\\", | |
| "/ \\" | |
| }; | |
| const int CHARACTER_WIDTH = 3; | |
| const int CHARACTER_HEIGHT = 3; | |
| // Projectile Model (1x1) | |
| const char PROJECTILE_SYMBOL = '*'; | |
| const int PROJECTILE_WIDTH = 1; | |
| const int PROJECTILE_HEIGHT = 1; | |
| // Define the game tick rate (33ms per frame = ~30 FPS) | |
| const std::chrono::milliseconds GAME_TICK_RATE = std::chrono::milliseconds(33); | |
| enum class GameState { | |
| PRE_GAME, | |
| RUNNING | |
| }; | |
| GameState currentState = GameState::PRE_GAME; | |
| // Game stats | |
| bool isGameOver = false; | |
| int score = 0; | |
| // STATIC MAP DATA (80x35) | |
| int mapa[][MAP_WIDTH] = | |
| { {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,3,2,3,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,6,6,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,3,3,3,3,3}, | |
| {3,3,3,3,6,6,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3}, | |
| {3,3,3,3,6,6,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,5,5,3}, | |
| {3,3,3,3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3}, | |
| {3,3,3,3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,5,5,3,3,3,3,3,3,3}, | |
| {3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3}, | |
| {3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3}, | |
| {3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3}, | |
| {3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3}, | |
| {3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3,3,3,3}, | |
| {3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,2,2,2,2,2,2,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}, | |
| {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3} | |
| }; | |
| // --- ANSI Escape Code Utilities --- | |
| const std::string CLEAR_SCREEN_HOME = "\033[H"; // Move cursor to home (1,1) | |
| const std::string CLEAR_TO_EOL = "\033[K"; // Clear from cursor to end of line | |
| // Function to move cursor to (row, col). ANSI is 1-based. | |
| std::string moveCursor(int row, int col) { | |
| // We add 1 to convert from 0-based indexing (C++ array) to 1-based indexing (ANSI) | |
| return "\033[" + std::to_string(row + 1) + ";" + std::to_string(col + 1) + "H"; | |
| } | |
| // --- COMPONENTS --- | |
| // 1. PositionComponent: Where the entity is located on the map | |
| struct PositionComponent { | |
| int x; | |
| int y; | |
| int prev_x; // Used for potential future optimization/tracking | |
| int prev_y; // Used for potential future optimization/tracking | |
| }; | |
| // 2. MapTileComponent: Data specific to map tiles | |
| struct MapTileComponent { | |
| bool isWalkable; | |
| std::string color; | |
| char symbol; | |
| }; | |
| // 3. CharacterTagComponent: Identifies the type of character | |
| enum class CharacterTag { | |
| PLAYER, | |
| ALLY, | |
| ENEMY, | |
| PROJECTILE | |
| }; | |
| struct CharacterTagComponent { | |
| CharacterTag tag; | |
| std::string color; | |
| }; | |
| // 4. VelocityComponent: For movement rate and direction (used by projectiles) | |
| struct VelocityComponent { | |
| int vx; // velocity X | |
| int vy; // velocity Y | |
| }; | |
| // 5. MoveDelayComponent: Used to slow down movement rate (for enemies/allies) | |
| struct MoveDelayComponent { | |
| int delayCounter; // Current tick count | |
| int delayMax; // Ticks required before moving again (e.g., 3 means move every 3rd update) | |
| }; | |
| // Represents a position on the map | |
| struct Point { | |
| int x, y; | |
| // Required for use in std::map as a key | |
| bool operator<(const Point& other) const { | |
| if (y != other.y) return y < other.y; | |
| return x < other.x; | |
| } | |
| // Required for use in comparison (like checking visited nodes) | |
| bool operator==(const Point& other) const { | |
| return x == other.x && y == other.y; | |
| } | |
| }; | |
| // --- SCREEN BUFFER (The Flicker Eliminator) --- | |
| // Stores the rendered content for a single cell, including color codes. | |
| struct ScreenCell { | |
| std::string content = ""; | |
| }; | |
| class ScreenBuffer { | |
| private: | |
| std::vector<ScreenCell> current_buffer; | |
| std::vector<ScreenCell> previous_buffer; | |
| public: | |
| ScreenBuffer() : | |
| current_buffer(MAP_WIDTH* MAP_HEIGHT), | |
| previous_buffer(MAP_WIDTH* MAP_HEIGHT) | |
| { | |
| // Initial full clear and setup of the terminal (done only once) | |
| std::cout << "\033[2J"; // Clear screen entirely | |
| std::cout << CLEAR_SCREEN_HOME << std::flush; | |
| // Initialize previous buffer with a sentinel value to force initial full map render | |
| force_repaint(); | |
| // Initialize previous buffer with spaces to force initial full map render | |
| /* for (auto& cell : previous_buffer) { | |
| cell.content = " "; | |
| }*/ | |
| } | |
| // Forces the differential render to draw the entire screen on the next call. | |
| // This is used for console resize/redraw events to fix graphical issues. | |
| void force_repaint() { | |
| for (auto& cell : previous_buffer) { | |
| // Use a unique sentinel value guaranteed not to be present in normal content | |
| cell.content = "FORCE_REDRAW_SENTINEL"; | |
| } | |
| } | |
| // Sets the final rendered content for a specific cell (includes color/reset codes) | |
| void set_cell(int x, int y, const std::string& content) { | |
| if (x >= 0 && x < MAP_WIDTH && y >= 0 && y < MAP_HEIGHT) { | |
| current_buffer[y * MAP_WIDTH + x].content = content; | |
| } | |
| } | |
| // The main function that draws ONLY the differences between frames. | |
| void differential_render() { | |
| bool needs_flush = false; | |
| for (int i = 0; i < current_buffer.size(); ++i) { | |
| // Check if the content (including color codes) has changed | |
| if (current_buffer[i].content != previous_buffer[i].content) { | |
| // Calculate 2D coordinates (y is row, x is col) | |
| int y = i / MAP_WIDTH; | |
| int x = i % MAP_WIDTH; | |
| // 1. Move cursor to the exact position of the change. | |
| std::cout << moveCursor(y, x); | |
| // 2. Print ONLY the new colored content. | |
| // We write directly to cout, bypassing the huge intermediate string build | |
| std::cout << current_buffer[i].content; | |
| // 3. Update the previous buffer state | |
| previous_buffer[i].content = current_buffer[i].content; | |
| needs_flush = true; | |
| } | |
| } | |
| // Return cursor to a safe position for the input prompt (MAP_HEIGHT + 1) | |
| // Clear the line just in case the previous line had leftover content from the map | |
| std::cout << moveCursor(MAP_HEIGHT, 0) << CLEAR_TO_EOL; | |
| // Only flush if we actually drew something | |
| if (needs_flush) { | |
| std::cout << std::flush; | |
| } | |
| } | |
| }; | |
| // --- GAME DATA STORAGE (The "World") --- | |
| // Map tiles are entities, but we store them in a 2D array for easy lookup | |
| EntityID mapEntities[MAP_HEIGHT][MAP_WIDTH]; | |
| std::map<EntityID, MapTileComponent> mapTiles; | |
| // --- UTILITIES --- | |
| /** | |
| * @brief Checks for AABB collision between two rectangular entities. | |
| * @param pos1 Position component of entity 1 (top-left corner). | |
| * @param w1 Width of entity 1. | |
| * @param h1 Height of entity 1. | |
| * @param pos2 Position component of entity 2 (top-left corner). | |
| * @param w2 Width of entity 2. | |
| * @param h2 Height of entity 2. | |
| * @return true if bounding boxes overlap, false otherwise. | |
| */ | |
| bool CheckCollision(const PositionComponent& pos1, int w1, int h1, | |
| const PositionComponent& pos2, int w2, int h2) { | |
| return (pos1.x < pos2.x + w2 && | |
| pos1.x + w1 > pos2.x && | |
| pos1.y < pos2.y + h2 && | |
| pos1.y + h1 > pos2.y); | |
| } | |
| // Utility function to calculate squared distance between two positions | |
| int GetSquaredDistance(const PositionComponent& p1, const PositionComponent& p2) { | |
| int dx = p1.x - p2.x; | |
| int dy = p1.y - p2.y; | |
| return dx * dx + dy * dy; | |
| } | |
| // --- SYSTEMS --- | |
| // System 1: MapInitializationSystem - Loads the static map data | |
| void MapInitializationSystem() { | |
| std::cout << "Initializing " << MAP_WIDTH << "x" << MAP_HEIGHT << " map from static data..." << std::endl; | |
| for (int y = 0; y < MAP_HEIGHT; ++y) { | |
| for (int x = 0; x < MAP_WIDTH; ++x) { | |
| EntityID tileID = CreateEntity(); | |
| mapEntities[y][x] = tileID; | |
| MapTileComponent tile; | |
| tile.symbol = SOLID_BLOCK_SYMBOL; // Always the solid block character | |
| // Determine properties based on the integer code in the static map array | |
| switch (mapa[y][x]) { | |
| case TIERRA: // 3 - Walkable Grass/Field | |
| tile.isWalkable = true; | |
| // Using the new LIGHT GREEN for walkable areas | |
| tile.color = ANSI_COLOR_LIGHT_GREEN; | |
| break; | |
| case AGUA: // 2 - Non-Walkable Water | |
| tile.isWalkable = false; | |
| tile.color = ANSI_COLOR_BLUE; | |
| break; | |
| case HOJA_ARBOL: // 5 - Non-Walkable Leaves/Bushes | |
| tile.isWalkable = false; | |
| // Using the new DARK GREEN for non-walkable areas | |
| tile.color = ANSI_COLOR_DARK_GREEN; | |
| break; | |
| case TRONCO: // 6 - Non-Walkable Trunk | |
| tile.isWalkable = false; | |
| // Using white for TRONCO (approximation for gray/brown) | |
| tile.color = ANSI_COLOR_WHITE; | |
| break; | |
| default: | |
| // Should not happen, but default to safe/walkable | |
| tile.isWalkable = true; | |
| tile.color = ANSI_COLOR_LIGHT_GREEN; | |
| break; | |
| } | |
| mapTiles[tileID] = tile; | |
| } | |
| } | |
| } | |
| // Function to create and initialize a projectile entity | |
| void CreateProjectileEntity(EntityID playerID) { | |
| if (positions.find(playerID) == positions.end()) return; | |
| const auto& playerPos = positions[playerID]; | |
| // Check if the player is still present | |
| if (playerID >= nextEntityID) return; | |
| EntityID projectileID = CreateEntity(); | |
| // Projectile originates on the left side of the player's 3x3 model. | |
| // Player X is top-left corner, so X-1 is one space left. | |
| // Y is center (playerPos.y + 1) | |
| int startX = playerPos.x - 1; | |
| int startY = playerPos.y + 1; | |
| // Basic boundary check for spawning | |
| // Projectile must spawn *on* the map (0 <= x < MAP_WIDTH) | |
| if (startX < 0 || startX >= MAP_WIDTH || startY < 0 || startY >= MAP_HEIGHT) { | |
| // Destroy the entity if it can't spawn in a valid location | |
| DestroyEntity(projectileID); | |
| return; | |
| } | |
| positions[projectileID] = { startX, startY }; | |
| // Projectile moves one step left per tick (vx = -1, vy = 0) | |
| velocities[projectileID] = { -1, 0 }; | |
| // Use a white color for the projectile | |
| characterTags[projectileID] = { CharacterTag::PROJECTILE, ANSI_COLOR_WHITE }; | |
| } | |
| // System 2: CharacterInitializationSystem | |
| void CharacterInitializationSystem() { | |
| std::cout << "Placing characters on the " << MAP_WIDTH << "x" << MAP_HEIGHT << " map..." << std::endl; | |
| // Player Entity (still one player) - Placed near the center/top-left of the main field | |
| EntityID playerID = CreateEntity(); | |
| positions[playerID] = { MAP_WIDTH / 3, MAP_HEIGHT / 4 }; | |
| characterTags[playerID] = { CharacterTag::PLAYER, ANSI_COLOR_CYAN }; | |
| playerEntityID = playerID; // Store player ID globally | |
| // 3 Allies on the right side | |
| for (int i = 0; i < 3; ++i) { | |
| EntityID allyID = CreateEntity(); | |
| // Spawn allies in the bottom-right half | |
| int spawnX = MAP_WIDTH * 3 / 4 + (rand() % 5) - (CHARACTER_WIDTH / 2); | |
| int spawnY = MAP_HEIGHT * 3 / 4 + (rand() % 5); | |
| // Ensure spawn is within bounds | |
| spawnX = std::min(spawnX, MAP_WIDTH - CHARACTER_WIDTH); | |
| spawnY = std::min(spawnY, MAP_HEIGHT - CHARACTER_HEIGHT); | |
| positions[allyID] = { spawnX, spawnY }; | |
| characterTags[allyID] = { CharacterTag::ALLY, ANSI_COLOR_YELLOW }; | |
| // Allies move randomly, so give them a movement delay (e.g., move once every 5 ticks) | |
| moveDelays[allyID] = { 0, 5 }; | |
| } | |
| // 3 Enemies on the left side | |
| for (int i = 0; i < 3; ++i) { | |
| EntityID enemyID = CreateEntity(); | |
| // Spawn enemies in the top-left quadrant, but away from the player | |
| int spawnX = MAP_WIDTH / 10 + (rand() % 5); | |
| int spawnY = MAP_HEIGHT / 10 + (i * 4) + (rand() % 2); | |
| // Ensure spawn is within bounds | |
| spawnX = std::min(spawnX, MAP_WIDTH - CHARACTER_WIDTH); | |
| spawnY = std::min(spawnY, MAP_HEIGHT - CHARACTER_HEIGHT); | |
| positions[enemyID] = { spawnX, spawnY }; | |
| characterTags[enemyID] = { CharacterTag::ENEMY, ANSI_COLOR_RED }; | |
| // Enemies move slowly towards the player (e.g., move once every 3 ticks) | |
| moveDelays[enemyID] = { 0, 3 }; | |
| } | |
| } | |
| // System 3: InputSystem (Handles player movement and actions) | |
| void MovementSystem(EntityID playerID, char input) { | |
| if (positions.find(playerID) == positions.end()) return; | |
| // Check for non-movement actions first (like shooting) | |
| if (input == ' ') { // Spacebar pressed | |
| CreateProjectileEntity(playerID); | |
| return; | |
| } | |
| int newX = positions[playerID].x; | |
| int newY = positions[playerID].y; | |
| // Calculate new position based on input | |
| switch (input) { | |
| case 'w': newY--; break; | |
| case 's': newY++; break; | |
| case 'a': newX--; break; | |
| case 'd': newX++; break; | |
| default: return; // No movement | |
| } | |
| // Boundary check (PREVENTS CHARACTER FROM VENTURING OFF THE CORNERS) | |
| // The top-left corner (X, Y) must stay within: | |
| // X: [0, MAP_WIDTH - CHARACTER_WIDTH] | |
| // Y: [0, MAP_HEIGHT - CHARACTER_HEIGHT] | |
| if (newX < 0 || newX > MAP_WIDTH - CHARACTER_WIDTH || | |
| newY < 0 || newY > MAP_HEIGHT - CHARACTER_HEIGHT) { | |
| return; | |
| } | |
| // Walkability check (Checking the 3x3 area for non-walkable tiles) | |
| for (int y_off = 0; y_off < CHARACTER_HEIGHT; ++y_off) { | |
| for (int x_off = 0; x_off < CHARACTER_WIDTH; ++x_off) { | |
| // Get the map tile entity ID at the new position | |
| EntityID tileID = mapEntities[newY + y_off][newX + x_off]; | |
| // Check its walkability component | |
| if (mapTiles.find(tileID) != mapTiles.end() && !mapTiles[tileID].isWalkable) { | |
| // If any tile in the 3x3 area is not walkable, block movement | |
| return; | |
| } | |
| } | |
| } | |
| // Apply movement if checks pass | |
| positions[playerID].x = newX; | |
| positions[playerID].y = newY; | |
| } | |
| // System 3.5: ProjectileSystem - Updates position and handles destruction | |
| void ProjectileSystem() { | |
| std::vector<EntityID> entitiesToDestroy; | |
| // Copy IDs to iterate safely while modifying the map | |
| std::vector<EntityID> activeVelocities; | |
| for (const auto& pair : velocities) { | |
| activeVelocities.push_back(pair.first); | |
| } | |
| for (EntityID id : activeVelocities) { | |
| // Ensure the entity still exists and has all required components | |
| if (velocities.count(id) == 0 || positions.count(id) == 0) continue; | |
| VelocityComponent& vel = velocities[id]; | |
| PositionComponent& pos = positions[id]; | |
| // Only run destruction logic for projectiles (using the tag as a filter) | |
| if (characterTags.count(id) && characterTags[id].tag == CharacterTag::PROJECTILE) { | |
| // Apply velocity | |
| pos.x += vel.vx; | |
| pos.y += vel.vy; | |
| // Check for map boundary destruction (goes off the left side) | |
| if (pos.x < 0) { | |
| entitiesToDestroy.push_back(id); | |
| continue; // Move to the next entity | |
| } | |
| // Check if the projectile is hitting a non-walkable tile (optional: hitting an enemy could go here too) | |
| // Get the map tile entity ID at the new projectile position (1x1) | |
| if (pos.y >= 0 && pos.y < MAP_HEIGHT && pos.x >= 0 && pos.x < MAP_WIDTH) { | |
| EntityID tileID = mapEntities[pos.y][pos.x]; | |
| if (mapTiles.count(tileID) && !mapTiles[tileID].isWalkable) { | |
| // Hit an obstacle, destroy projectile | |
| entitiesToDestroy.push_back(id); | |
| continue; | |
| } | |
| } | |
| else { | |
| // Out of bounds (if it didn't hit x < 0 boundary already) | |
| entitiesToDestroy.push_back(id); | |
| continue; | |
| } | |
| } | |
| } | |
| // Clean up destroyed entities | |
| for (EntityID id : entitiesToDestroy) { | |
| DestroyEntity(id); | |
| } | |
| } | |
| /** | |
| * @brief Checks if a 3x3 area starting at (startX, startY) is fully walkable. | |
| * @param startX Top-left X coordinate of the 3x3 area. | |
| * @param startY Top-left Y coordinate of the 3x3 area. | |
| * @return true if the area is within bounds and all 9 tiles are walkable, false otherwise. | |
| */ | |
| bool is_walkable_area(int startX, int startY) { | |
| // Check main boundaries for the 3x3 area | |
| if (startX < 0 || startX > MAP_WIDTH - CHARACTER_WIDTH || | |
| startY < 0 || startY > MAP_HEIGHT - CHARACTER_HEIGHT) { | |
| return false; | |
| } | |
| // Check individual tile walkability | |
| for (int y_off = 0; y_off < CHARACTER_HEIGHT; ++y_off) { | |
| for (int x_off = 0; x_off < CHARACTER_WIDTH; ++x_off) { | |
| int mapY = startY + y_off; | |
| int mapX = startX + x_off; | |
| EntityID tileID = mapEntities[mapY][mapX]; | |
| if (mapTiles.find(tileID) == mapTiles.end() || !mapTiles[tileID].isWalkable) { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * @brief Finds the next best movement (dx, dy) for a 3x3 entity using BFS. | |
| * @param startPos The entity's current position (top-left X, Y). | |
| * @param targetPos The target entity's position (top-left X, Y). | |
| * @return A Point representing the relative movement (dx, dy). Returns {0, 0} if no path is found. | |
| */ | |
| Point FindNextMoveBFS(const Point& startPos, const Point& targetPos) { | |
| // Using 4 cardinal directions for movement | |
| const Point directions[] = { | |
| {0, -1}, {0, 1}, {-1, 0}, {1, 0} // N, S, W, E | |
| // Optional: include diagonals for 8-way movement: | |
| // {-1, -1}, {1, -1}, {-1, 1}, {1, 1} | |
| }; | |
| // --- BFS Setup --- | |
| std::queue<Point> frontier; | |
| // Maps a Point to its immediate parent on the shortest path (for path reconstruction) | |
| std::map<Point, Point> came_from; | |
| frontier.push(startPos); | |
| came_from[startPos] = startPos; // Sentinel value indicating the start node | |
| Point found_target_step = { 0, 0 }; // Stores the step *before* the target | |
| // --- Core BFS Loop --- | |
| while (!frontier.empty()) { | |
| Point current = frontier.front(); | |
| frontier.pop(); | |
| // Safety break for excessive search (e.g., if target is unreachable) | |
| if (came_from.size() > 1000) break; | |
| // Check 4 cardinal neighbors | |
| for (const auto& dir : directions) { | |
| Point next = { current.x + dir.x, current.y + dir.y }; | |
| // 1. Boundary and Walkability Check (3x3 area) | |
| if (!is_walkable_area(next.x, next.y)) { | |
| continue; | |
| } | |
| // 2. Visited Check | |
| if (came_from.count(next)) { | |
| continue; | |
| } | |
| // 3. Goal Check | |
| // A path is found if the next position is the target position. | |
| // Since the target is 3x3, we accept if we reach its top-left corner. | |
| if (next == targetPos) { | |
| // Path found. Reconstruct the first step from the start. | |
| // Trace back from the target (current) until we hit the immediate neighbor of startPos | |
| Point trace_current = current; | |
| Point trace_parent = came_from[trace_current]; | |
| // Loop backward until the parent is the start position itself | |
| while (trace_parent.x != startPos.x || trace_parent.y != startPos.y) { | |
| trace_current = trace_parent; | |
| trace_parent = came_from[trace_current]; | |
| } | |
| // trace_current is now the first step from startPos | |
| found_target_step = trace_current; | |
| // The movement vector is the difference between the first step and the start | |
| return { found_target_step.x - startPos.x, found_target_step.y - startPos.y }; | |
| } | |
| // 4. Queue and Mark Visited | |
| frontier.push(next); | |
| came_from[next] = current; | |
| } | |
| } | |
| // If loop finishes without finding a path | |
| return { 0, 0 }; | |
| } | |
| // System 4: AISystem - Handles autonomous movement for enemies and allies | |
| void AISystem() { | |
| // Collect all dynamic entities to iterate safely | |
| std::vector<EntityID> dynamicEntities; | |
| for (const auto& pair : characterTags) { | |
| dynamicEntities.push_back(pair.first); | |
| } | |
| for (EntityID entityID : dynamicEntities) { | |
| // Only process entities that are still alive and have movement logic | |
| if (characterTags.count(entityID) == 0 || positions.count(entityID) == 0 || moveDelays.count(entityID) == 0) { | |
| continue; | |
| } | |
| const auto& tag = characterTags[entityID]; | |
| MoveDelayComponent& delay = moveDelays[entityID]; | |
| // Apply slowdown logic | |
| delay.delayCounter++; | |
| if (delay.delayCounter < delay.delayMax) continue; | |
| delay.delayCounter = 0; // Reset counter for movement | |
| int moveX = 0; | |
| int moveY = 0; | |
| const auto& currentPos = positions[entityID]; | |
| if (tag.tag == CharacterTag::ENEMY) { | |
| // Find closest target (Player or Ally) | |
| EntityID closestTargetID = -1; | |
| int minDistanceSq = 999999; | |
| PositionComponent targetPos = { 0, 0 }; | |
| for (const auto& targetPair : characterTags) { | |
| EntityID targetID = targetPair.first; | |
| const auto& targetTag = targetPair.second; | |
| if (positions.count(targetID) == 0) continue; | |
| // Target is Player or Ally | |
| if (targetTag.tag == CharacterTag::PLAYER || targetTag.tag == CharacterTag::ALLY) { | |
| const auto& potentialTargetPos = positions[targetID]; | |
| int distSq = GetSquaredDistance(currentPos, potentialTargetPos); | |
| if (distSq < minDistanceSq) { | |
| minDistanceSq = distSq; | |
| closestTargetID = targetID; | |
| targetPos = potentialTargetPos; | |
| } | |
| } | |
| } | |
| // --- NEW PATHFINDING LOGIC FOR ENEMIES --- | |
| if (closestTargetID != -1) { | |
| Point start = { currentPos.x, currentPos.y }; | |
| Point target = { targetPos.x, targetPos.y }; | |
| // Use BFS to get the next movement step | |
| Point nextMove = FindNextMoveBFS(start, target); | |
| moveX = nextMove.x; | |
| moveY = nextMove.y; | |
| // If BFS returns {0, 0}, it either means we are already next to the target | |
| // or the target is completely unreachable. | |
| // We let moveX/moveY remain 0 and the enemy won't move this turn. | |
| } | |
| } | |
| else if (tag.tag == CharacterTag::ALLY) { | |
| // Random movement: -1, 0, or 1 for X and Y | |
| moveX = (rand() % 3) - 1; // -1, 0, 1 | |
| moveY = (rand() % 3) - 1; // -1, 0, 1 | |
| } | |
| // --- Apply Movement and Collision Check for 3x3 Characters --- | |
| if (moveX != 0 || moveY != 0) { | |
| int newX = currentPos.x + moveX; | |
| int newY = currentPos.y + moveY; | |
| // AI characters (3x3) use the same full boundary check as the player | |
| bool boundary_ok = (newX >= 0 && newX <= MAP_WIDTH - CHARACTER_WIDTH && | |
| newY >= 0 && newY <= MAP_HEIGHT - CHARACTER_HEIGHT); | |
| if (tag.tag == CharacterTag::ALLY && newX < MAP_WIDTH / 2) { | |
| // Constraint: Allies must stay on the right side of the map (right of column 40) | |
| boundary_ok = newX >= MAP_WIDTH / 2; | |
| } | |
| if (boundary_ok) { | |
| bool walkable = true; | |
| // Check the 3x3 area for non-walkable tiles | |
| for (int y_off = 0; y_off < CHARACTER_HEIGHT; ++y_off) { | |
| for (int x_off = 0; x_off < CHARACTER_WIDTH; ++x_off) { | |
| EntityID tileID = mapEntities[newY + y_off][newX + x_off]; | |
| if (mapTiles.count(tileID) && !mapTiles[tileID].isWalkable) { | |
| walkable = false; | |
| break; | |
| } | |
| } | |
| if (!walkable) break; | |
| } | |
| if (walkable) { | |
| positions[entityID].x = newX; | |
| positions[entityID].y = newY; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // System 5: CollisionSystem | |
| void CollisionSystem() { | |
| if (isGameOver) return; | |
| std::set<EntityID> uniqueEntitiesToDestroy; | |
| bool playerWasHit = false; | |
| // --- 1. Projectile vs. Enemy --- | |
| for (const auto& projPair : characterTags) { | |
| EntityID projID = projPair.first; | |
| if (projPair.second.tag != CharacterTag::PROJECTILE) continue; | |
| if (positions.count(projID) == 0) continue; | |
| const auto& projPos = positions[projID]; | |
| for (const auto& enemyPair : characterTags) { | |
| EntityID enemyID = enemyPair.first; | |
| if (enemyPair.second.tag != CharacterTag::ENEMY) continue; | |
| if (positions.count(enemyID) == 0) continue; | |
| // Skip if enemy is already targeted for destruction by another projectile this tick | |
| if (uniqueEntitiesToDestroy.count(enemyID)) continue; | |
| const auto& enemyPos = positions[enemyID]; | |
| if (CheckCollision(projPos, PROJECTILE_WIDTH, PROJECTILE_HEIGHT, | |
| enemyPos, CHARACTER_WIDTH, CHARACTER_HEIGHT)) { | |
| // Projectile hits Enemy: Destroy both | |
| uniqueEntitiesToDestroy.insert(projID); | |
| uniqueEntitiesToDestroy.insert(enemyID); | |
| score += 10; | |
| // Break inner loop since this projectile is destroyed | |
| break; | |
| } | |
| } | |
| } | |
| // --- 2. Enemy vs. Player & 3. Enemy vs. Ally --- | |
| // Separate active enemies from other 3x3 characters (excluding those marked for destruction) | |
| std::vector<EntityID> activeEnemies; | |
| std::vector<EntityID> activeTargets; | |
| for (const auto& pair : characterTags) { | |
| if (uniqueEntitiesToDestroy.count(pair.first)) continue; // Skip already destroyed entities | |
| if (pair.second.tag == CharacterTag::ENEMY) { | |
| activeEnemies.push_back(pair.first); | |
| } | |
| else if (pair.second.tag == CharacterTag::PLAYER || pair.second.tag == CharacterTag::ALLY) { | |
| activeTargets.push_back(pair.first); | |
| } | |
| } | |
| // Check Enemy collisions against targets | |
| for (EntityID enemyID : activeEnemies) { | |
| if (positions.count(enemyID) == 0) continue; | |
| const auto& enemyPos = positions[enemyID]; | |
| for (EntityID targetID : activeTargets) { | |
| if (positions.count(targetID) == 0) continue; | |
| if (uniqueEntitiesToDestroy.count(targetID)) continue; | |
| const auto& targetPos = positions[targetID]; | |
| const auto& targetTag = characterTags[targetID].tag; | |
| if (CheckCollision(enemyPos, CHARACTER_WIDTH, CHARACTER_HEIGHT, | |
| targetPos, CHARACTER_WIDTH, CHARACTER_HEIGHT)) { | |
| // Enemy vs. Player: Game Over | |
| if (targetTag == CharacterTag::PLAYER) { | |
| playerWasHit = true; | |
| uniqueEntitiesToDestroy.insert(enemyID); | |
| uniqueEntitiesToDestroy.insert(targetID); | |
| break; // Stop processing targets and enemies | |
| } | |
| // Enemy vs. Ally | |
| else if (targetTag == CharacterTag::ALLY) { | |
| uniqueEntitiesToDestroy.insert(enemyID); | |
| uniqueEntitiesToDestroy.insert(targetID); | |
| break; // Move to next enemy | |
| } | |
| } | |
| } | |
| if (playerWasHit) break; | |
| } | |
| // Apply Game Over and destruction | |
| if (playerWasHit) { | |
| isGameOver = true; | |
| } | |
| // Apply destruction | |
| for (EntityID id : uniqueEntitiesToDestroy) { | |
| DestroyEntity(id); | |
| } | |
| } | |
| // System 6: RenderSystem (Now uses Differential Rendering) | |
| void RenderSystem(ScreenBuffer& buffer) { | |
| // We REMOVE the system("cls") / system("clear") calls here. | |
| // 1. Build the current Screen Buffer with MapTiles (Background) | |
| for (int y = 0; y < MAP_HEIGHT; ++y) { | |
| for (int x = 0; x < MAP_WIDTH; ++x) { | |
| EntityID tileID = mapEntities[y][x]; | |
| if (mapTiles.find(tileID) != mapTiles.end()) { | |
| const auto& tile = mapTiles[tileID]; | |
| // Store the colored symbol string in the buffer | |
| std::string colored_tile = tile.color + tile.symbol + ANSI_COLOR_RESET; | |
| buffer.set_cell(x, y, colored_tile); | |
| } | |
| } | |
| } | |
| // 2. Draw Characters and Projectiles on top of the buffer (Foreground) | |
| // Iterate over entities that have both a PositionComponent and a CharacterTagComponent | |
| for (const auto& pair : positions) { | |
| EntityID entityID = pair.first; | |
| if (characterTags.find(entityID) != characterTags.end()) { | |
| const auto& pos = pair.second; | |
| const auto& tag = characterTags[entityID]; | |
| int startX = pos.x; | |
| int startY = pos.y; | |
| // Determine size and model based on tag | |
| const int modelWidth = (tag.tag == CharacterTag::PROJECTILE) ? PROJECTILE_WIDTH : CHARACTER_WIDTH; | |
| const int modelHeight = (tag.tag == CharacterTag::PROJECTILE) ? PROJECTILE_HEIGHT : CHARACTER_HEIGHT; | |
| // Loop through the entity's model area | |
| for (int cy = 0; cy < modelHeight; ++cy) { | |
| for (int cx = 0; cx < modelWidth; ++cx) { | |
| char charToDraw; | |
| if (tag.tag == CharacterTag::PROJECTILE) { | |
| charToDraw = PROJECTILE_SYMBOL; | |
| } | |
| else { | |
| // Standard 3x3 character model | |
| charToDraw = CHARACTER_MODEL[cy][cx]; | |
| } | |
| // Check if the character model segment is not a space | |
| if (charToDraw != ' ') { | |
| int mapX = startX + cx; | |
| int mapY = startY + cy; | |
| // Check map boundaries | |
| if (mapX >= 0 && mapX < MAP_WIDTH && mapY >= 0 && mapY < MAP_HEIGHT) { | |
| // OVERWRITE the map tile in the buffer with the entity's colored symbol | |
| std::string colored_char = tag.color + charToDraw + ANSI_COLOR_RESET; | |
| buffer.set_cell(mapX, mapY, colored_char); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // 3. Differential Render: Print ONLY the cells that changed. | |
| buffer.differential_render(); | |
| // 3. Differential Render: Print ONLY the cells that changed, and display the UI message. | |
| std::string ui_message = ""; | |
| if (currentState == GameState::PRE_GAME) { | |
| ui_message = ANSI_COLOR_YELLOW; | |
| ui_message += "--- PRE-GAME SETUP --- | Press [SPACE] to start the game | Press [R] to repaint the map | Press [Q] to quit"; | |
| ui_message += ANSI_COLOR_RESET; | |
| } | |
| else { | |
| ui_message = ANSI_COLOR_CYAN; | |
| ui_message += "Map Size: " + std::to_string(MAP_WIDTH) + "x" + std::to_string(MAP_HEIGHT) + " | Move (W/A/S/D) | Shoot (SPACE) | Quit (Q)"; | |
| ui_message += ANSI_COLOR_RESET; | |
| } | |
| // 4. Print UI below the map without scrolling the map (using the cleared line from the buffer) | |
| //std::cout << "--------------------------------------------------------------------------------" << std::endl; | |
| //std::cout << "Map Size: " << MAP_WIDTH << "x" << MAP_HEIGHT << " | Move (W/A/S/D) | Shoot (SPACE) | Quit (Q)" << std::endl; | |
| //std::cout << ">> Waiting for input..." << std::flush; | |
| } | |
| // System 7: RenderHUDSystem - Draws score and messages | |
| void RenderHUDSystem() { | |
| // Row for HUD (below the map) | |
| int hudRow = MAP_HEIGHT + 1; | |
| // Game Over Message | |
| if (isGameOver) { | |
| std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL; | |
| std::cout << ANSI_COLOR_RED << "!!! GAME OVER !!! Enemy reached the player. Press 'Q' to restart." << ANSI_COLOR_RESET; | |
| hudRow++; | |
| } | |
| // Score Display | |
| std::string scoreDisplay = std::string(ANSI_COLOR_YELLOW) + "Score: " + std::to_string(score) + std::string(ANSI_COLOR_RESET); | |
| // Output status message to console | |
| std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL << scoreDisplay << ANSI_COLOR_RESET; | |
| hudRow++; | |
| // Controls Display | |
| if (!isGameOver) { | |
| std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL; | |
| std::cout << ANSI_COLOR_CYAN << "Controls: W/A/S/D to move, SPACE to shoot." << ANSI_COLOR_RESET; | |
| } | |
| hudRow++; | |
| // Final cursor position for input | |
| std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL << "> " << std::flush; | |
| } | |
| // Function to get non-blocking input | |
| char GetNonBlockingInput() { | |
| char input = '\0'; // Default to null char (no input) | |
| #ifdef _WIN32 | |
| // Check if a key is waiting in the buffer | |
| if (_kbhit()) { | |
| // Read the key without blocking | |
| input = _getch(); | |
| // Convert to lowercase for case-insensitive controls | |
| input = static_cast<char>(std::tolower(static_cast<unsigned char>(input))); | |
| } | |
| #endif | |
| // For non-Windows environments, input remains '\0', | |
| // ensuring the game continues to tick. | |
| return input; | |
| } | |
| /** | |
| * @brief Resizes the console window and buffer to fit the map and instructions. | |
| * This is primarily for running on the classic Windows console (cmd.exe) | |
| */ | |
| void resizeConsoleWindow() { | |
| // Target dimensions for map (80x35) + instructions/padding | |
| const int CONSOLE_WIDTH = MAP_WIDTH + 4; // 84 | |
| const int CONSOLE_HEIGHT = MAP_HEIGHT + 10; // Enough space for prompt | |
| // Minimum height is necessary to prevent scrolling which would cause flicker | |
| HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); | |
| if (hOut == INVALID_HANDLE_VALUE || hOut == NULL) return; | |
| // Get maximum console window dimensions available based on screen size | |
| COORD maxWindow = GetLargestConsoleWindowSize(hOut); | |
| // Ensure the desired size is not larger than the largest possible size | |
| int finalWidth = std::min(CONSOLE_WIDTH, (int)maxWindow.X); | |
| int finalHeight = std::min(CONSOLE_HEIGHT, (int)maxWindow.Y); | |
| // 1. Define the new screen buffer size | |
| COORD newSize = { (short)finalWidth, (short)finalHeight }; | |
| // Set the screen buffer size (must be done before setting the window size) | |
| if (!SetConsoleScreenBufferSize(hOut, newSize)) { | |
| return; | |
| } | |
| // 2. Define the new console window rectangle (size is always one unit less than buffer size) | |
| SMALL_RECT rect = { 0, 0, (short)(finalWidth - 1), (short)(finalHeight - 1) }; | |
| // Set the console window size | |
| SetConsoleWindowInfo(hOut, TRUE, &rect); | |
| } | |
| // Function to reset the game state | |
| void RestartGame() { | |
| // 1. Reset Global State | |
| isGameOver = false; | |
| score = 0; | |
| playerEntityID = -1; // Reset player ID | |
| // 2. Clear all dynamic component pools | |
| positions.clear(); | |
| characterTags.clear(); | |
| velocities.clear(); | |
| moveDelays.clear(); | |
| // 3. Re-initialize systems (Map is first to reset nextEntityID and occupy map IDs) | |
| MapInitializationSystem(); | |
| CharacterInitializationSystem(); | |
| // Clear the screen and force a full redraw | |
| std::cout << "\033[2J"; | |
| } | |
| // --- MAIN GAME LOOP --- | |
| void GameLoop() { | |
| // 1. Setup | |
| MapInitializationSystem(); | |
| ScreenBuffer screenBuffer; | |
| CharacterInitializationSystem(); | |
| while (currentState == GameState::PRE_GAME) { | |
| char input = GetNonBlockingInput(); | |
| if (input == 'q') { | |
| return; // Quit the application | |
| } | |
| else if (input == ' ' || input == 'w' || input == 'a' || input == 's' || input == 'd') { | |
| // Requirement met: The game begins on the first space press. | |
| currentState = GameState::RUNNING; | |
| screenBuffer.force_repaint(); // Force a clean redraw on state change | |
| // The main loop will start immediately, no break needed. | |
| } | |
| else if (input == 'r') { | |
| resizeConsoleWindow(); | |
| // Requirement met: Repaint the entire map (simulating resize fix). | |
| screenBuffer.force_repaint(); | |
| RenderSystem(screenBuffer); | |
| } | |
| // Wait a short time to prevent maxing out CPU while polling input | |
| std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| } | |
| auto lastTime = std::chrono::high_resolution_clock::now(); | |
| // 2. Main Loop | |
| while (currentState == GameState::RUNNING) { | |
| // A. Get Input (Non-Blocking) | |
| char input = GetNonBlockingInput(); | |
| // Check for quit first | |
| if (input == 'q') break; | |
| // B. Game Logic Update (This runs every tick, even if input is '\0') | |
| // Process movement and input actions (Player) | |
| MovementSystem(playerEntityID, input); | |
| // --- 2. Input Handling --- | |
| if (isGameOver) { | |
| if (input == 'q') { | |
| RestartGame(); | |
| continue; // Skip game logic until next tick | |
| } | |
| // Game over, only 'q' is processed, render HUD and wait | |
| } | |
| else { | |
| // Process Movement only if game is running | |
| MovementSystem(playerEntityID, input); | |
| } | |
| // Process AI movement (Enemies/Allies) | |
| //AISystem(); | |
| // Process projectile movement | |
| //ProjectileSystem(); | |
| // C. Draw the current state | |
| //RenderSystem(screenBuffer); | |
| // D. Fixed Time Step Delay (Ensures constant speed/tick rate) | |
| //std::this_thread::sleep_for(GAME_TICK_RATE); | |
| // --- 3. Update (Game Logic) --- | |
| auto currentTime = std::chrono::high_resolution_clock::now(); | |
| auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTime); | |
| if (elapsedTime >= GAME_TICK_RATE) { | |
| if (!isGameOver) { | |
| ProjectileSystem(); // Move projectiles, check wall/boundary | |
| AISystem(); // Move enemies/allies (handles map collisions) | |
| CollisionSystem(); // Handles Projectile-Enemy, Enemy-Ally, Enemy-Player collisions | |
| } | |
| // --- 4. Render --- | |
| RenderSystem(screenBuffer); | |
| RenderHUDSystem(); | |
| lastTime = currentTime; | |
| } | |
| // Small sleep to prevent CPU hogging if tick rate is high and no input occurs | |
| std::this_thread::sleep_for(std::chrono::milliseconds(1)); | |
| } | |
| } | |
| // --- WINDOWS ANSI ENABLE FIX AND MACRO CONFLICT FIX --- | |
| #ifdef _WIN32 | |
| // FIX 1: Define NOMINMAX to prevent Windows from defining min/max macros that conflict with C++ standard library functions/syntax. | |
| // NOTE: NOMINMAX is defined globally near the top of the file | |
| void enableVTMode() { | |
| HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); | |
| if (hOut == INVALID_HANDLE_VALUE) return; | |
| DWORD dwMode = 0; | |
| if (!GetConsoleMode(hOut, &dwMode)) return; | |
| dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; | |
| SetConsoleMode(hOut, dwMode); | |
| } | |
| #endif // _WIN32 | |
| int main() { | |
| // Seed the random number generator once | |
| srand(static_cast<unsigned int>(time(0))); | |
| // --- FIX: Enable Virtual Terminal Processing for ANSI 256 Colors on Windows --- | |
| #ifdef _WIN32 | |
| enableVTMode(); | |
| resizeConsoleWindow(); | |
| // Ensures 'echo off' is run once for cmd.exe environments | |
| // Note: this may be ignored in modern terminals like VS Code Terminal/PowerShell | |
| system("echo off"); | |
| #endif | |
| // Hide the cursor during rendering to prevent it from flickering on every cell update | |
| std::cout << "\033[?25l"; // ANSI code to hide cursor | |
| GameLoop(); | |
| // Restore the cursor before exiting | |
| std::cout << "\033[?25h"; // ANSI code to show cursor | |
| std::cout << ANSI_COLOR_RESET << "\n\nGame Over. Thanks for playing!" << std::endl; | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment