迷宫1

· · 个人记录

#define _WIN32_WINNT 0x0600 // This defines the minimum Windows version for API availability (0x0600 for Windows Vista)
#include <bits/stdc++.h>
#include <windows.h> // For Windows specific console functions and GetAsyncKeyState, PlaySound
#include <cmath>
#include <fstream>   // For file input/output (map loading)
#include <functional> // For std::function (in generateChunk)
#include <random>    // For std::mt19937 (in generateChunk)

// For PlaySound - remember to link with winmm.lib (in Visual Studio: Project -> Properties -> Linker -> Input -> Additional Dependencies, add winmm.lib)
// Or compile with: g++ your_game.cpp -o your_game -lwinmm

using namespace std;

// --- Constants and Enums ---
// Console size set by user
static int SCREEN_WIDTH;
static int SCREEN_HEIGHT;
static CHAR_INFO* screen = nullptr;
static HANDLE hConsole;
static SMALL_RECT writeRegion;
static COORD bufCoord = {0,0};
const double PI = 3.141592653589793;

// Game related constants
constexpr int CHUNK_SIZE = 20; // Size of each maze chunk
const double PLAYER_HEIGHT = 0.5; // Player's height from the floor (0 is floor level)
const double GRAVITY = 0.08;      // Acceleration due to gravity
const double JUMP_FORCE = 0.2;    // Initial upward velocity for jump
const double MOVE_SPEED = 0.1;    // Player movement speed
const double MOUSE_SENSITIVITY = 0.003; // Mouse camera sensitivity
const double KEY_SENSITIVITY = 0.05;    // Keyboard camera sensitivity
const double PITCH_SENSITIVITY = 0.03;  // Keyboard pitch sensitivity
const double MAX_PITCH_ANGLE = PI / 2.0 - 0.1; // Limit pitch to prevent flipping

// Block types (using enum for better readability)
enum BlockType {
    EMPTY = 0,
    WALL = 1,
    TREASURE = 2,
    EXIT = 3,
    MONSTER = 4, // New: Simple monster block
    TRAP = 5     // New: Simple trap block
};

// Console Colors (Standard Windows Console Colors)
enum ConsoleColor {
    BLACK = 0,
    DARK_BLUE = 1,
    DARK_GREEN = 2,
    DARK_CYAN = 3,
    DARK_RED = 4,
    DARK_MAGENTA = 5,
    DARK_YELLOW = 6,
    GREY = 7,
    DARK_GREY = 8,
    BLUE = 9,
    GREEN = 10,
    CYAN = 11,
    RED = 12,
    MAGENTA = 13,
    YELLOW = 14,
    WHITE = 15
};

struct Chunk {
    int map[CHUNK_SIZE][CHUNK_SIZE];
};
using ChunkCoord = pair<int,int>;

// Fast hash pair for unordered_map
struct pair_hash {
    size_t operator()(const ChunkCoord& p) const {
        return std::hash<int>()(p.first) ^ (std::hash<int>()(p.second)<<1);
    }
};

unordered_map<ChunkCoord,Chunk,pair_hash> chunkPool;

// Get chunk coordinates and local offset within chunk
inline ChunkCoord getChunkPos(int x, int y) {
    int cx = (x >= 0) ? (x/CHUNK_SIZE) : ((x+1-CHUNK_SIZE)/CHUNK_SIZE);
    int cy = (y >= 0) ? (y/CHUNK_SIZE) : ((y+1-CHUNK_SIZE)/CHUNK_SIZE);
    return ChunkCoord(cx, cy);
}
inline void getLocalPos(int x, int y, int& lx, int& ly) {
    lx = x % CHUNK_SIZE; if(lx<0) lx+=CHUNK_SIZE;
    ly = y % CHUNK_SIZE; if(ly<0) ly+=CHUNK_SIZE;
}

// Player and Particle struct
struct Particle { double x,y,z,vx,vy,vz; int life; char symbol; WORD color; };
struct Player {
    double x=1.5,y=1.5,z=PLAYER_HEIGHT,angle=0,pitch=0,vz=0;
    bool onGround=true;
    int health=100, treasures=0;
    double cosA,sinA,cosP,sinP; // Cos/Sin of angle (yaw) and pitch
    inline void updateTrig(){ cosA=cos(angle); sinA=sin(angle); cosP=cos(pitch); sinP=sin(pitch); }
} player;
static vector<Particle> particles;

// --- New: Monster Structure and List ---
struct Monster {
    double x, y; // Precise float coordinates (center of the tile)
    double moveCooldown; // Timer for movement
    int id; // For unique identification, if needed
};
static vector<Monster> monsters; // Global list of active monsters

// Monster constants
const double MONSTER_MOVE_DELAY = 0.5; // Monster moves every 0.5 seconds
const double MONSTER_DETECTION_RANGE = 40.0; // Monsters detect player within 40 units (Euclidean distance)

static BlockType selectedBlockType = WALL; // New: Variable to hold currently selected block type for placing

// Particle constants
const double PARTICLE_GRAVITY = 0.08;
const double PARTICLE_FRICTION = 0.98;

bool showMiniMap=false, mouseMode=false;
bool backroomsMode=false; // Backrooms mode switch
POINT lastMousePos;
bool gameOver = false; // Game over flag
long long startTime; // For game timer
int gameEndReason = 0; // 0: manual exit, 1: win, 2: lose

// === NEW: Damage Flash Variables ===
static double damageFlashTimer = 0.0;    // Remaining flash screen time (seconds)
const double DAMAGE_FLASH_DURATION = 0.2; // Total flash duration (seconds)
// ===================================

// Map mode declaration
enum class MapMode { Infinite, Existing };
static MapMode mapMode = MapMode::Infinite; // Default to infinite

// Map access wrapper (forward declaration for use in tryMoveMonster)
int getWorldAt(int x, int y);
void setWorldAt(int x, int y, int value);
void playSound(const char* filename); // Forward declaration

// --- New: Minimap scaling variables ---
static int minimap_radius = 7; // Default to show 7 tiles in each direction (15x15 total)
const int MIN_MINIMAP_RADIUS = 3; // Smallest minimap (7x7 total)
const int MAX_MINIMAP_RADIUS = 200; // Largest minimap (41x41 total)
// --- END NEW ---

// --- NEW: Minimap Flip Variable ---
static bool minimap_flipped = false; // False = normal, True = flipped horizontally
// --- END NEW ---

// --- New: Helper function for monster movement and digging ---
// Returns true if the monster successfully moved (or dug to move)
bool tryMoveMonster(Monster& m, int old_mx, int old_my, int new_mx, int new_my) {
    // Check if the target block is occupied by *another* monster
    for (const auto& other_m : monsters) {
        if (&other_m == &m) continue; // Skip self
        if (static_cast<int>(other_m.x) == new_mx && static_cast<int>(other_m.y) == new_my) { // Monster x/y are center, new_mx/my are floor
            return false; // Target cell occupied by another monster, cannot move here
        }
    }

    int target_block_type = getWorldAt(new_mx, new_my);

    if (target_block_type == WALL || target_block_type == TREASURE || target_block_type == EXIT || target_block_type == TRAP) {
        // It's an obstacle, monster will dig.
        // Dig 3x3 area around the target (new_mx, new_my)
        for (int dy_dig = -1; dy_dig <= 1; ++dy_dig) {
            for (int dx_dig = -1; dx_dig <= 1; ++dx_dig) {
                int dig_x = new_mx + dx_dig;
                int dig_y = new_my + dy_dig;
                // Only clear non-empty blocks.
                // Important: When a monster digs, it should not dig other monsters from map.
                if (getWorldAt(dig_x, dig_y) != EMPTY && getWorldAt(dig_x, dig_y) != MONSTER) {
                    setWorldAt(dig_x, dig_y, EMPTY);
                }
            }
        }
        // After digging, the target tile (new_mx, new_my) is now empty (or was already, if digging around it).
        // So, the monster can now move there.
        setWorldAt(old_mx, old_my, EMPTY); // Clear monster's old map spot
        m.x = new_mx + 0.5; // Update monster's precise position to center of new tile
        m.y = new_my + 0.5;
        setWorldAt(static_cast<int>(m.x),static_cast<int>(m.y), MONSTER); // Set monster's new map spot
        // playSound("dig.wav"); // Monster digging sound
        return true;
    } else if (target_block_type == EMPTY) {
        // It's an empty space, just move.
        setWorldAt(old_mx, old_my, EMPTY); // Clear monster's old map spot
        m.x = new_mx + 0.5; // Update monster's precise position
        m.y = new_my + 0.5;
        setWorldAt(static_cast<int>(m.x),static_cast<int>(m.y), MONSTER); // Set monster's new map spot
        return true;
    }
    return false; // Cannot move to this type of block (e.g., player occupies it, or other unhandled types)
}
// --- End New Helper Function ---

// Ensure connectivity between chunks during maze generation
void generateChunk(const ChunkCoord& coord) {
    Chunk& chunk = chunkPool[coord];
    // Fill with walls
    for(int y=0; y<CHUNK_SIZE; ++y)
    for(int x=0; x<CHUNK_SIZE; ++x)
        chunk.map[y][x] = WALL;

    // Random engine, seeded by chunk coordinates for consistent generation
    uint64_t seed = coord.first * 73856093ull ^ coord.second * 19349663ull;
    mt19937 rng(seed);

    // DFS for maze path generation (single odd-point grid, not digging outer boundary)
    function<void(int,int)> dfs = [&](int x,int y){
        chunk.map[y][x] = EMPTY;
        // Shuffle directions
        vector<int> dir = {0,1,2,3};
        shuffle(dir.begin(), dir.end(), rng);
        static const int dx[] = {1,-1,0,0}, dy[] = {0,0,1,-1};
        for(int d:dir) {
            int nx = x + dx[d]*2, ny = y + dy[d]*2;
            if(nx > 0 && nx < CHUNK_SIZE-1 && ny > 0 && ny < CHUNK_SIZE-1 && chunk.map[ny][nx] == WALL) {
                chunk.map[y+dy[d]][x+dx[d]] = EMPTY; // Dig path
                dfs(nx, ny);
            }
        }
    };
    dfs(1, 1); // Start DFS from (1,1)

    // Place dynamic elements (Treasures, Exits, Monsters, Traps)
    auto place_element = [&](int type, int chance_inv, int min_dist_border) {
        if (rng() % chance_inv == 0) {
            int tx = rng() % (CHUNK_SIZE - 2 * min_dist_border) + min_dist_border;
            int ty = rng() % (CHUNK_SIZE - 2 * min_dist_border) + min_dist_border;
            // Only place if it's an empty spot and not too close to player spawn (1.5, 1.5)
            // Or if type is Monster, avoid spawning too close to player initially
            if (chunk.map[ty][tx] == EMPTY) {
                // Approximate distance check from player spawn (0,0 chunk, 1.5, 1.5)
                int world_x = coord.first * CHUNK_SIZE + tx;
                int world_y = coord.second * CHUNK_SIZE + ty;
                if (type == MONSTER && sqrt(pow(world_x - player.x, 2) + pow(world_y - player.y, 2)) < MONSTER_DETECTION_RANGE + 5) {
                    return; // Avoid spawning monsters too close to player at start
                }

                chunk.map[ty][tx] = type;
                // --- New: If it's a monster, add it to the global monsters list ---
                if (type == MONSTER) {
                    monsters.push_back({
                        (double)coord.first * CHUNK_SIZE + tx + 0.5, // Center monster in tile
                        (double)coord.second * CHUNK_SIZE + ty + 0.5,
                        MONSTER_MOVE_DELAY, // Initial cooldown
                        (int)monsters.size() // Simple ID
                    });
                }
                // --- End New ---
            }
        }
    };

    place_element(TREASURE, 10, 2); // 1/10 chance, min 2 units from border
    place_element(EXIT, 30, 3);    // 1/30 chance, min 3 units from border
    place_element(MONSTER, 40, 2); // 1/40 chance, min 2 units from border
    place_element(TRAP, 25, 2);    // 1/25 chance, min 2 units from border

    // Ensure connectivity with neighboring chunks
    // Left side: If left neighbor exists and has a path on its right, open path here
    ChunkCoord leftC = {coord.first - 1, coord.second};
    if (chunkPool.count(leftC)) {
        Chunk& lc = chunkPool[leftC];
        for (int y = 3; y < CHUNK_SIZE - 3; y += 2) { // Iterate over potential path points
            if (lc.map[y][CHUNK_SIZE - 2] == EMPTY) { // If left chunk's right edge path is open
                chunk.map[y][0] = EMPTY; // Open this chunk's left edge
                chunk.map[y][1] = EMPTY; // And the next cell into the chunk
            }
        }
    }
    // Top side: Same for top neighbor
    ChunkCoord upC = {coord.first, coord.second - 1};
    if (chunkPool.count(upC)) {
        Chunk& uc = chunkPool[upC];
        for (int x = 3; x < CHUNK_SIZE - 3; x += 2) {
            if (uc.map[CHUNK_SIZE - 2][x] == EMPTY) {
                chunk.map[0][x] = EMPTY;
                chunk.map[1][x] = EMPTY;
            }
        }
    }
    // Open paths to the right/bottom: These points will be considered by future neighboring chunks
    for (int y = 3; y < CHUNK_SIZE - 3; y += 2) {
        if (chunk.map[y][CHUNK_SIZE - 3] == EMPTY) { // If internal path reaches third-to-last column on right
            chunk.map[y][CHUNK_SIZE - 2] = EMPTY; // Open up to second-to-last column
        }
    }
    for (int x = 3; x < CHUNK_SIZE - 3; x += 2) {
        if (chunk.map[CHUNK_SIZE - 3][x] == EMPTY) { // If internal path reaches third-to-last row on bottom
            chunk.map[CHUNK_SIZE - 2][x] = EMPTY; // Open up to second-to-last row
        }
    }
}

// Map access wrapper
int getWorldAt(int x, int y) {
    ChunkCoord c = getChunkPos(x, y);
    int lx, ly;
    getLocalPos(x, y, lx, ly);

    if (chunkPool.count(c)) {
        return chunkPool[c].map[ly][lx];
    } else {
        if (mapMode == MapMode::Infinite) {
            // If chunk doesn't exist and in infinite mode, generate it
            generateChunk(c);
            return chunkPool[c].map[ly][lx]; // Return the newly generated block type
        } else { // MapMode::Existing
            // If in existing map mode and chunk not loaded, it's outside the defined map. Treat as wall.
            return WALL;
        }
    }
}

// Set world block type
void setWorldAt(int x, int y, int value) {
    ChunkCoord c = getChunkPos(x, y);
    int lx, ly;
    getLocalPos(x, y, lx, ly);

    // Only modify if the chunk exists in the pool (either pre-loaded or already generated)
    // In Existing mode, this prevents creating new chunks outside the loaded map.
    // In Infinite mode, getWorldAt would usually generate the chunk before setWorldAt is called for new areas.
    if(chunkPool.count(c)) {
        chunkPool[c].map[ly][lx] = value;
    }
}

// Load existing map from file
void loadExistingMap(const string& filename) {
    ifstream fin(filename);
    if (!fin) {
        cerr << "错误:无法打开地图文件: " << filename << endl;
        mapMode = MapMode::Infinite; // Fallback to infinite generation
        cout << "已切换到无限自动生成模式。\n";
        return;
    }

    // Clear existing dynamic elements from previous runs/infinite mode
    chunkPool.clear();
    monsters.clear();

    int chunkX, chunkY;
    int loaded_chunks = 0;
    while (fin >> chunkX >> chunkY) {
        ChunkCoord coord = {chunkX, chunkY};
        Chunk c;
        for (int y = 0; y < CHUNK_SIZE; ++y) {
            for (int x = 0; x < CHUNK_SIZE; ++x) {
                int v_int;
                if (!(fin >> v_int)) {
                    cerr << "错误:地图文件格式不正确,在读取区块 (" << chunkX << "," << chunkY << ") 的块数据时出错。\n";
                    fin.close();
                    mapMode = MapMode::Infinite; // Fallback
                    cout << "已切换到无限自动生成模式。\n";
                    return;
                }
                BlockType v = static_cast<BlockType>(v_int);
                c.map[y][x] = v_int; // Store the integer value directly

                // If it's a monster, add to dynamic list
                if (v == MONSTER) {
                    monsters.push_back({
                        (double)coord.first * CHUNK_SIZE + x + 0.5, // Center monster in tile
                        (double)coord.second * CHUNK_SIZE + y + 0.5,
                        MONSTER_MOVE_DELAY, // Initial cooldown
                        (int)monsters.size() // Simple ID
                    });
                }
            }
        }
        chunkPool[coord] = c;
        loaded_chunks++;
    }
    fin.close();
    cout << "已加载 " << loaded_chunks << " 个区块。\n";

    // MODIFICATION START: Set player position to the first available empty spot in any loaded chunk
    bool found_spawn_location = false;
    for (const auto& pair : chunkPool) {
        ChunkCoord current_chunk_coord = pair.first;
        const Chunk& current_chunk = pair.second;

        int spawn_x_offset = current_chunk_coord.first * CHUNK_SIZE;
        int spawn_y_offset = current_chunk_coord.second * CHUNK_SIZE;

        for (int dy = 0; dy < CHUNK_SIZE; ++dy) {
            for (int dx = 0; dx < CHUNK_SIZE; ++dx) {
                if (current_chunk.map[dy][dx] == EMPTY) {
                    player.x = spawn_x_offset + dx + 0.5; // Center in the tile
                    player.y = spawn_y_offset + dy + 0.5;
                    found_spawn_location = true;
                    // Exit inner loops
                    goto end_spawn_search; // Use goto to break out of nested loops
                }
            }
        }
    }

end_spawn_search:; // Label for goto

    if (!found_spawn_location) {
        cout << "警告:在所有加载的区块中未找到空地作为玩家出生点,玩家可能出生在墙内。\n";
        // Player will remain at initial {1.5, 1.5} if no empty spot found.
    }
    // MODIFICATION END

    if (chunkPool.empty()) {
        cout << "警告:没有加载任何区块,请检查地图文件。\n";
        mapMode = MapMode::Infinite; // Fallback if no chunks loaded
        cout << "已切换到无限自动生成模式。\n";
    }
}

inline bool key(int vk){ return (GetAsyncKeyState(vk) & 0x8000) != 0; }
inline void draw(int x,int y,char c,WORD col){
    if(y>=0&&y<SCREEN_HEIGHT&&x>=0&&x<SCREEN_WIDTH) {
        CHAR_INFO cell_info;
        cell_info.Char.AsciiChar = c; 
        cell_info.Attributes = col;    
        screen[y*SCREEN_WIDTH + x] = cell_info; 
    }
}
// Setup console and buffer
void initConsole(){
    hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    cout << "Enter console width height (e.g., 120 40): "; cin >> SCREEN_WIDTH >> SCREEN_HEIGHT;
    COORD bufSize = {(SHORT)SCREEN_WIDTH,(SHORT)SCREEN_HEIGHT};
    SetConsoleScreenBufferSize(hConsole,bufSize);
    SMALL_RECT win={0,0,(SHORT)(SCREEN_WIDTH-1),(SHORT)(SCREEN_HEIGHT-1)};
    SetConsoleWindowInfo(hConsole,TRUE,&win);
    writeRegion=win;
    screen = (CHAR_INFO*)malloc(sizeof(CHAR_INFO)*SCREEN_WIDTH*SCREEN_HEIGHT);
    CONSOLE_CURSOR_INFO cci; GetConsoleCursorInfo(hConsole,&cci); cci.bVisible=false; SetConsoleCursorInfo(hConsole,&cci);

    // Attempt to center mouse for initial setup
    HWND hwnd=GetConsoleWindow(); RECT r; GetWindowRect(hwnd,&r);
    int cx=(r.left+r.right)/2, cy=(r.top+r.bottom)/2;
    SetCursorPos(cx,cy);
    lastMousePos={cx,cy};
}

// Function to play a sound (blocking, for simple effects)
void playSound(const char* filename) {
    // PlaySoundA(filename, NULL, SND_FILENAME | SND_ASYNC); // SND_ASYNC for non-blocking
    // Commented out as sound files are not provided and cause linker errors without winmm.lib.
    // Uncomment and link with winmm.lib if you have the WAV files.
}

// Fast render
void renderFrame(){
    // precompute
    double FOV=PI/3; // Field of View
    double invW=1.0/SCREEN_WIDTH;
    player.updateTrig();

    // Adjust the "horizon" based on pitch and player's Z-position
    int verticalOffset = int(player.pitch * (SCREEN_HEIGHT / PI / 2.0));
    verticalOffset += int((player.z - PLAYER_HEIGHT) * SCREEN_HEIGHT); // Adjust based on player Z
    int halfH_shifted = (SCREEN_HEIGHT>>1) + verticalOffset;

    // Sky / Ceiling
    WORD skyBaseColor = backroomsMode ? YELLOW : BLUE;
    for(int y=0;y<halfH_shifted;y++){
        for(int x=0;x<SCREEN_WIDTH;x++){
            screen[y*SCREEN_WIDTH+x]={{' '},skyBaseColor};

            // Backrooms mode: simulate ceiling lights
            if(backroomsMode){
                int light_grid_x = 5; // Horizontal grid count
                int light_grid_y = 3; // Vertical grid count

                int cell_width = SCREEN_WIDTH / light_grid_x;
                int ceiling_height = max(1, halfH_shifted);
                int cell_height = ceiling_height / light_grid_y;
                if (cell_height == 0) cell_height = 1;

                int current_cell_x_idx = x / cell_width;
                int current_cell_y_idx = y / cell_height;

                int light_center_x = current_cell_x_idx * cell_width + cell_width / 2;
                int light_center_y = current_cell_y_idx * cell_height + cell_height / 2;

                if(abs(x - light_center_x) < cell_width / 6 &&
                   abs(y - light_center_y) < cell_height / 6){
                    screen[y*SCREEN_WIDTH+x]={{' '},WHITE}; // White as light
                }
            }
        }
    }

    // Raycast columns
    for(int x=0;x<SCREEN_WIDTH;x++){
        double camX=2*x*invW-1;
        double rx=player.cosA - player.sinA*camX;
        double ry=player.sinA + player.cosA*camX;

        int mx=static_cast<int>(player.x), my=static_cast<int>(player.y);

        double dDX=fabs(1/rx);
        double dDY=fabs(1/ry);

        double sDX=(rx<0?(player.x-mx)*dDX:(mx+1-player.x)*dDX);
        double sDY=(ry<0?(player.y-my)*dDY:(my+1-player.y)*dDY);

        int stepX=(rx<0?-1:1);
        int stepY=(ry<0?-1:1);
        int side;
        int hitType=WALL; // Default to wall

        // DDA algorithm
        while(true){
            if(sDX<sDY){sDX+=dDX; mx+=stepX; side=0;} else {sDY+=dDY; my+=stepY; side=1;}
            hitType = getWorldAt(mx,my);
            if(hitType) break; // Hit a wall or object
        }

        double perp = side? (my-player.y+(1-stepY)/2)/ry : (mx-player.x+(1-stepX)/2)/rx;
        if(perp<0.05) perp=0.05;

        // Calculate wall height based on distance and player's Z position
        // The wall is assumed to be 1 unit high, from z=0 to z=1
        int wall_top_y = int(halfH_shifted - (1.0 - player.z) * SCREEN_HEIGHT / perp);
        int wall_bottom_y = int(halfH_shifted - (0.0 - player.z) * SCREEN_HEIGHT / perp);

        // Adjust draw start and end Y-coordinates
        int dS = max(0, wall_top_y);
        int dE = min(SCREEN_HEIGHT, wall_bottom_y);

        // Choose wall color based on backroomsMode and side
        WORD baseCol;
        if (backroomsMode) {
            baseCol = YELLOW; // Backrooms: walls are yellow
        } else {
            baseCol = (side ? DARK_GREY : GREY); // Normal: Darker for side, lighter for front
        }

        for(int y=dS;y<dE;y++){
            WORD col=baseCol;
            char ch = ' ';
            switch (hitType) {
                case WALL:      col = baseCol;                   ch = (perp<2.5?'#':(perp<5?'%':(perp<7.5?'*':'.'))); break;
                case TREASURE:  col = YELLOW | FOREGROUND_INTENSITY; ch = '$'; break; // Bright yellow for treasure
                case EXIT:      col = GREEN | FOREGROUND_INTENSITY;  ch = 'E'; break; // Bright green for exit
                case MONSTER:   col = RED | FOREGROUND_INTENSITY;    ch = 'M'; break; // Bright red for monster
                case TRAP:      col = DARK_RED | FOREGROUND_INTENSITY; ch = 'T'; break; // Dark red for trap
            }
            draw(x,y,ch,col);
        }

        // Floor and Ceiling (if player is below)
        CHAR_INFO floor_char_info;
        if (backroomsMode) {
            floor_char_info = {{'.'}, YELLOW}; // Backrooms: floor is yellow
        } else {
            floor_char_info = {{'.'}, DARK_GREEN}; // Normal: floor is dark green
        }

        // Draw floor from wall bottom to screen bottom
        for(int y=max(0, dE); y<SCREEN_HEIGHT; y++) {
            screen[y*SCREEN_WIDTH+x]=floor_char_info;
        }

        // Draw ceiling above wall top (only if player is not at z=1.0 or higher, i.e., looking up at it)
        for(int y=0; y<min(dS, halfH_shifted); y++) {
             screen[y*SCREEN_WIDTH+x]={{' '},skyBaseColor};
        }
    }

    // Particle rendering
    for (const auto& p : particles) {
        double px_rel = p.x - player.x;
        double py_rel = p.y - player.y;
        double pz_rel = p.z - player.z;

        // Rotate relative coordinates based on player's yaw
        double rotX = px_rel * player.cosA + py_rel * player.sinA;
        double rotY_depth = py_rel * player.cosA - px_rel * player.sinA; // Depth before pitch

        // Apply pitch rotation to vertical components
        double final_pz = pz_rel * player.cosP - rotY_depth * player.sinP; // New effective Z after pitch
        double final_depth = rotY_depth * player.cosP + pz_rel * player.sinP; // New effective depth after pitch

        if (final_depth > 0.1) { // Particle is in front of player
            double inv_depth = 1.0 / final_depth;
            int screenX = int(SCREEN_WIDTH / 2 + rotX * SCREEN_HEIGHT * inv_depth);
            int screenY = int(SCREEN_HEIGHT / 2 - final_pz * (SCREEN_HEIGHT/2) * inv_depth); 

            // Clamp screen coordinates
            if (screenX >= 0 && screenX < SCREEN_WIDTH && screenY >= 0 && screenY < SCREEN_HEIGHT) {
                draw(screenX, screenY, p.symbol, p.color);
            }
        }
    }

    // --- New: Monster rendering (similar to particle rendering) ---
    for (const auto& m : monsters) {
        double px_rel = m.x - player.x;
        double py_rel = m.y - player.y;
        double pz_rel = PLAYER_HEIGHT - player.z; // Assume monster is at player height (0.5 from floor)

        // Rotate relative coordinates based on player's yaw
        double rotX = px_rel * player.cosA + py_rel * player.sinA;
        double rotY_depth = py_rel * player.cosA - px_rel * player.sinA; // Depth before pitch

        // Apply pitch rotation to vertical components
        double final_pz = pz_rel * player.cosP - rotY_depth * player.sinP; // New effective Z after pitch
        double final_depth = rotY_depth * player.cosP + pz_rel * player.sinP; // New effective depth after pitch

        if (final_depth > 0.1) { // Monster is in front of player
            double inv_depth = 1.0 / final_depth;
            int screenX = int(SCREEN_WIDTH / 2 + rotX * SCREEN_HEIGHT * inv_depth);
            int screenY = int(SCREEN_HEIGHT / 2 - final_pz * (SCREEN_HEIGHT/2) * inv_depth); 

            // Clamp screen coordinates
            if (screenX >= 0 && screenX < SCREEN_WIDTH && screenY >= 0 && screenY < SCREEN_HEIGHT) {
                // Using 'M' for monster, bright red
                draw(screenX, screenY, 'M', RED | FOREGROUND_INTENSITY);
            }
        }
    }
    // --- End New ---

    // HUD
    string hp_str="HP:"+to_string(player.health);
    for(int i=0;i<hp_str.size();i++) draw(i,0,hp_str[i],(player.health>50?GREEN:RED));

    string tr_str="TREASURES:"+to_string(player.treasures);
    for(int i=0;i<tr_str.size();i++) draw(SCREEN_WIDTH-1-tr_str.size()+i,0,tr_str[i],YELLOW);

    // Game Timer (elapsed time)
    long long currentTime = GetTickCount64(); // 系统启动以来的毫秒数
    long long elapsedTime = (currentTime - startTime) / 1000; // Seconds
    string time_str = "TIME:" + to_string(elapsedTime) + "s";
    for(int i=0;i<time_str.size();i++) draw(0,1,time_str[i],WHITE);

    // Show selected block type for placing
    string selected_block_str = "PLACE: ";
    switch (selectedBlockType) {
        case WALL:      selected_block_str += "WALL"; break;
        case TREASURE:  selected_block_str += "TREASURE"; break;
        case EXIT:      selected_block_str += "EXIT"; break;
        case MONSTER:   selected_block_str += "MONSTER"; break;
        case TRAP:      selected_block_str += "TRAP"; break;
        default:        selected_block_str += "UNKNOWN"; break; // Should not happen
    }
    for(int i=0;i<selected_block_str.size();i++) draw(SCREEN_WIDTH-1-selected_block_str.size()+i,1,selected_block_str[i],CYAN); // Below treasures

    draw(SCREEN_WIDTH/2,halfH_shifted,'+',WHITE); // Crosshair

    // Minimap
    if(showMiniMap){
        // V is the total width/height of the minimap grid (e.g., radius 7 means 15x15)
        int V = 2 * minimap_radius + 1;
        // Calculate starting position for minimap (top-right, with some padding)
        int sx = SCREEN_WIDTH - V - 2; // 2 units padding from right edge
        int sy = 3; // 3 units padding from top edge, below HUD

        // Draw border
        WORD borderColor = GREY;
        char borderChar = (char)219; // Solid block character

        // Top and Bottom borders
        for (int i = 0; i < V + 2; ++i) {
            draw(sx - 1 + i, sy - 1, borderChar, borderColor); // Top line
            draw(sx - 1 + i, sy + V, borderChar, borderColor); // Bottom line
        }
        // Left and Right borders
        for (int i = 0; i < V + 2; ++i) {
            draw(sx - 1, sy - 1 + i, borderChar, borderColor); // Left line
            draw(sx + V, sy - 1 + i, borderChar, borderColor); // Right line
        }

        int cx=static_cast<int>(player.x), cy=static_cast<int>(player.y);
        for(int dy=-minimap_radius;dy<=minimap_radius;++dy){ // Loop from -radius to +radius
            for(int dx=-minimap_radius;dx<=minimap_radius;++dx){
                int wx=cx+dx, wy=cy+dy;
                int v=getWorldAt(wx,wy); char c=' '; WORD col;
                switch(v){
                    case WALL:     c='#'; col=DARK_GREY; break;
                    case TREASURE: c='$'; col=YELLOW;    break;
                    case EXIT:     c='E'; col=GREEN;     break;
                    case MONSTER:  c='M'; col=RED;       break;
                    case TRAP:     c='T'; col=DARK_RED;  break;
                    default:       c=' '; col=backroomsMode ? YELLOW : BLUE; break; // Empty space color matches sky
                }
                // Calculate drawing position within the minimap area relative to sx,sy
                // NEW: Apply horizontal flip for map tiles
                int draw_dx = minimap_flipped ? -dx : dx;
                draw(sx + minimap_radius + draw_dx, sy + minimap_radius + dy, c, col);
            }
        }
        // Draw player at the center of the minimap (always at center, not flipped)
        draw(sx + minimap_radius, sy + minimap_radius,'@',RED | FOREGROUND_INTENSITY); 

        // --- NEW: Draw player direction arrow for 8 directions ---
        char arrow_char;
        int dx_arrow_offset = 0, dy_arrow_offset = 0; // Relative offsets from player's @

        double normalized_angle = fmod(player.angle, 2 * PI);
        if (normalized_angle < 0) normalized_angle += 2 * PI; // Normalize to 0 to 2PI

        // Define 8 sectors of 45 degrees (PI/4) each
        // Angles are clockwise from positive X-axis (Right).
        // Console Y increases downwards, so 'Up' means negative Y offset, 'Down' means positive Y offset.

        // Sector 1: Right (337.5 to 22.5 degrees)
        if (normalized_angle >= (15 * PI / 8.0) || normalized_angle < (PI / 8.0)) {
            arrow_char = '>';
            dx_arrow_offset = 1; dy_arrow_offset = 0;
        } 
        // Sector 2: Up-Right (22.5 to 67.5 degrees)
        else if (normalized_angle >= (PI / 8.0) && normalized_angle < (3 * PI / 8.0)) {
            arrow_char = '.'; // Top-right dot
            dx_arrow_offset = 1; dy_arrow_offset = -1;
        } 
        // Sector 3: Up (67.5 to 112.5 degrees)
        else if (normalized_angle >= (3 * PI / 8.0) && normalized_angle < (5 * PI / 8.0)) {
            arrow_char = '^';
            dx_arrow_offset = 0; dy_arrow_offset = -1;
        } 
        // Sector 4: Up-Left (112.5 to 157.5 degrees)
        else if (normalized_angle >= (5 * PI / 8.0) && normalized_angle < (7 * PI / 8.0)) {
            arrow_char = '.'; // Top-left dot
            dx_arrow_offset = -1; dy_arrow_offset = -1;
        } 
        // Sector 5: Left (157.5 to 202.5 degrees)
        else if (normalized_angle >= (7 * PI / 8.0) && normalized_angle < (9 * PI / 8.0)) {
            arrow_char = '<';
            dx_arrow_offset = -1; dy_arrow_offset = 0;
        } 
        // Sector 6: Down-Left (202.5 to 247.5 degrees)
        else if (normalized_angle >= (9 * PI / 8.0) && normalized_angle < (11 * PI / 8.0)) {
            arrow_char = '.'; // Bottom-left dot
            dx_arrow_offset = -1; dy_arrow_offset = 1;
        } 
        // Sector 7: Down (247.5 to 292.5 degrees)
        else if (normalized_angle >= (11 * PI / 8.0) && normalized_angle < (13 * PI / 8.0)) {
            arrow_char = 'v';
            dx_arrow_offset = 0; dy_arrow_offset = 1;
        } 
        // Sector 8: Down-Right (292.5 to 337.5 degrees)
        else { // This covers the range normalized_angle >= (13 * PI / 8.0) && normalized_angle < (15 * PI / 8.0)
            arrow_char = '.'; // Bottom-right dot
            dx_arrow_offset = 1; dy_arrow_offset = 1;
        }

        // NEW: Apply horizontal flip for arrow offset
        int draw_dx_arrow = minimap_flipped ? -dx_arrow_offset : dx_arrow_offset;

        // Draw the direction character at the calculated relative position
        draw(sx + minimap_radius + draw_dx_arrow, sy + minimap_radius + dy_arrow_offset, arrow_char, RED | FOREGROUND_INTENSITY);
        // --- END NEW ---
    }

    // --- First write of the buffer (normal frame) ---
    WriteConsoleOutputA(hConsole,screen,{SHORT(SCREEN_WIDTH),SHORT(SCREEN_HEIGHT)},bufCoord,&writeRegion);

    // === NEW: Damage Flash Effect ===
    if (damageFlashTimer > 0.0) {
        // Construct a CHAR_INFO for a red background space
        CHAR_INFO redCell;
        redCell.Char.AsciiChar = ' ';
        redCell.Attributes = BACKGROUND_RED | BACKGROUND_INTENSITY; // Bright Red Background

        // Fill the entire screen buffer with the red cell
        for (int y = 0; y < SCREEN_HEIGHT; ++y) {
            for (int x = 0; x < SCREEN_WIDTH; ++x) {
                screen[y * SCREEN_WIDTH + x] = redCell;
            }
        }
        // Write the red buffer to the console. This will temporarily overwrite the normal frame.
        WriteConsoleOutputA(hConsole, screen, {SHORT(SCREEN_WIDTH), SHORT(SCREEN_HEIGHT)}, bufCoord, &writeRegion);
    }
    // ================================
}

// Function to clear the console and print message for game over/win
void displayGameEndScreen(const string& message) {
    system("cls"); // Clear console
    cout << endl << endl;
    cout << string((SCREEN_WIDTH - message.length()) / 2, ' ') << message << endl;
    cout << string((SCREEN_WIDTH - 25) / 2, ' ') << "Press ENTER to exit." << endl;
    cin.ignore(numeric_limits<streamsize>::max(), '\n'); // Clear any leftover input buffer
    cin.get();
}

int main(){
    SetConsoleTitle("迷迷世界");
    srand((unsigned)time(NULL)); // Seed random number generator
    ios::sync_with_stdio(false);

    system("cls"); // Clear screen for menu
    cout << "\n\n";
    cout << "  --------------------------------------------------\n";
    cout << "  |             欢迎来到 迷迷世界             |\n";
    cout << "  |             (Maze Runner 3D)                   |\n";
    cout << "  --------------------------------------------------\n";
    cout << "\n";
    cout << "  请选择地图模式:\n";
    cout << "  1. 无限自动生成地图 (Infinite procedural map)\n";
    cout << "  2. 加载已有地图 (Load existing map from file)\n";
    cout << "  --------------------------------------------------\n";
    cout << "  输入编号并回车:";

    int choice_input = 1; // Default choice
    cin >> choice_input;
    cin.ignore(numeric_limits<streamsize>::max(), '\n'); // Clear rest of line

    if (choice_input == 2) {
        mapMode = MapMode::Existing;
        cout<<"请选择地图:\n";
        cout<<"1.空旷的小房间\n"; 
        cout<<"2.石墙林立\n";
        string fname_choice_str;
        getline(cin, fname_choice_str);
        int fname_choice = 0;
        try {
            fname_choice = stoi(fname_choice_str);
        } catch (const std::invalid_argument& ia) {
            cerr << "无效输入, 默认为 1.\n";
            fname_choice = 1;
        } catch (const std::out_of_range& oor) {
            cerr << "输入超出范围, 默认为 1.\n";
            fname_choice = 1;
        }

        string fname_base;
        if (fname_choice == 1) {
            fname_base = "1"; // Corresponding to "空旷的小房间.txt"
        } else if (fname_choice == 2) {
            fname_base = "2"; // Corresponding to "石墙林立.txt"
        } else {
            cout << "无效选择,默认为 '空旷的小房间'.\n";
            fname_base = "1";
        }
        loadExistingMap(fname_base + ".txt");

    } else {
        mapMode = MapMode::Infinite;
        cout << "  已选择无限自动生成模式。\n";
    }

    // Continue with the rest of the game intro and initialization
    initConsole(); // Initialize console AFTER getting size input

    // !! HERE IS THE FIX !!
    // Set console output code page to OEM 437, which contains the block character (char 219)
    SetConsoleOutputCP(437); 
    SetConsoleCP(437); // Also set input code page for consistency (though not strictly needed for this bug)
    // !! END OF FIX !!

    cout << "\n\n";
    cout << "  --------------------------------------------------\n";
    cout << "  |             欢迎来到 迷迷世界             |\n";
    cout << "  |             (Maze Runner 3D)                   |\n";
    cout << "  --------------------------------------------------\n";
    cout << "\n";
    cout << "  **游戏概述:**\n";
    cout << "  《迷迷世界3D》是一款独特的、在Windows控制台中运行的3D第一人称迷宫冒险游戏。\n";
    cout << "  你将身处一个无限生成、不断变化的像素化迷宫中。你的目标是探索、收集宝藏、\n";
    cout << "  生存下来并找到出口!\n";
    cout << "\n";
    cout << "  **核心玩法与特色:**\n";
    cout << "  - **无限迷宫:** 程序生成,每次体验都不同。\n";
    cout << "  - **动态元素:** 收集'$'宝藏,寻找'E'出口。小心'M'怪物和'T'陷阱,它们会扣除生命!\n";
    cout << "  - **自由互动:** 按'X'挖掘任何方块,按'1'放置方块,改变迷宫。\n";
    cout << "  - **辅助模式:** 按'M'切换小地图,按'K'切换独特的“后室”视觉模式。\n";
    cout << "\n";
    cout << "  **操作指南:**\n";
    cout << "  - **WASD:** 移动 (按住SHIFT冲刺)\n";
    cout << "  - **空格键 (SPACE):** 跳跃\n";
    cout << "  - **左/右箭头键:** 水平转向\n";
    cout << "  - **上/下箭头键:** 垂直俯仰\n";
    cout << "  - **ALT 键:** 切换鼠标视角模式\n";
    cout << "  - **M 键:** 切换小地图显示\n";
    cout << "  - **K 键:** 切换“后室”模式\n";
    cout << "  - **X 键:** 挖掘前方的任何方块 (除了空地)\n";
    cout << "  - **1 键:** 在前方空地放置当前选定的方块类型\n";
    cout << "  - **[ 键 (左方括号):** 循环选择上一个要放置的方块类型\n";
    cout << "  - **] 键 (右方括号):** 循环选择下一个要放置的方块类型\n";
    cout << "  - **ESC 键:** 退出游戏\n";
    cout << "  - **+ 键:** 增大迷你地图尺寸\n"; 
    cout << "  - **- 键:** 减小迷你地图尺寸\n";
    cout << "  - **R 键:** 左右翻转迷你地图显示\n";
    cout << "\n";
    cout << "  --------------------------------------------------\n";
    cout << "  准备好了吗?按下 ENTER 键开始冒险...\n";
    cin.get();    // 等待用户按下Enter键开始游戏
    system("cls");
    startTime = GetTickCount64(); // 记录游戏开始时间

    bool altPrev=false, mPrev=false, xPrev=false, kPrev=false, rPrev=false; // NEW: rPrev for R key
    bool onePrev=false, spacePrev=false;
    bool cyclePrevPrev = false, cycleNextPrev = false; // For new cycle keys
    bool plusPrev = false, minusPrev = false; // For minimap size

    // --- New: For DeltaTime Calculation ---
    long long lastFrameTime = GetTickCount64();

    // Game loop
    while(!gameOver){
        long long currentFrameTime = GetTickCount64();
        double deltaTime = (currentFrameTime - lastFrameTime) / 1000.0; // Time in seconds
        lastFrameTime = currentFrameTime;

        // === NEW: Update damage flash timer ===
        damageFlashTimer = max(0.0, damageFlashTimer - deltaTime);
        // ======================================

        // --- Input Handling ---
        bool alt=key(VK_MENU);
        if(alt&&!altPrev) mouseMode=!mouseMode;
        altPrev=alt;

        if(mouseMode){ 
            POINT p; GetCursorPos(&p); 
            int dx=p.x-lastMousePos.x; 
            int dy=p.y-lastMousePos.y; // For pitch
            player.angle+=dx*MOUSE_SENSITIVITY;
            player.pitch-=dy*MOUSE_SENSITIVITY; // Negative for inverted Y-axis
            SetCursorPos(lastMousePos.x,lastMousePos.y);
        } 

        // Horizontal camera rotation (Yaw)
        if(key(VK_LEFT)) player.angle-=KEY_SENSITIVITY; 
        if(key(VK_RIGHT)) player.angle+=KEY_SENSITIVITY; 

        // Vertical camera rotation (Pitch)
        if(key(VK_UP)) player.pitch+=PITCH_SENSITIVITY; 
        if(key(VK_DOWN)) player.pitch-=PITCH_SENSITIVITY; 
        player.pitch = max(-MAX_PITCH_ANGLE, min(MAX_PITCH_ANGLE, player.pitch));

        // Movement (WASD)
        double nx=player.x, ny=player.y;
        double currentMoveSpeed = MOVE_SPEED;
        // Optional: Sprint if holding SHIFT
        if (key(VK_SHIFT)) currentMoveSpeed *= 1.5;

        if(key('W')){ // Forward
            nx+=player.cosA*currentMoveSpeed; 
            ny+=player.sinA*currentMoveSpeed; 
        }
        if(key('S')){ // Backward
            nx-=player.cosA*currentMoveSpeed; 
            ny-=player.sinA*currentMoveSpeed; 
        }
        if(key('A')){ // Strafe Left
            nx+=player.sinA*currentMoveSpeed; 
            ny-=player.cosA*currentMoveSpeed; 
        }
        if(key('D')){ // Strafe Right
            nx-=player.sinA*currentMoveSpeed; 
            ny+=player.cosA*currentMoveSpeed; 
        }

        // Basic collision detection for horizontal movement
        // Check corners of player's bounding box
        double player_half_width = 0.2; // Player's effective half-width for collision

        // Proposed new position (x component only)
        bool can_move_x = (
            getWorldAt(static_cast<int>(nx - player_half_width), static_cast<int>(player.y - player_half_width)) != WALL &&
            getWorldAt(static_cast<int>(nx + player_half_width), static_cast<int>(player.y - player_half_width)) != WALL &&
            getWorldAt(static_cast<int>(nx - player_half_width), static_cast<int>(player.y + player_half_width)) != WALL &&
            getWorldAt(static_cast<int>(nx + player_half_width), static_cast<int>(player.y + player_half_width)) != WALL
        );

        // Proposed new position (y component only)
        bool can_move_y = (
            getWorldAt(static_cast<int>(player.x - player_half_width), static_cast<int>(ny - player_half_width)) != WALL &&
            getWorldAt(static_cast<int>(player.x + player_half_width), static_cast<int>(ny - player_half_width)) != WALL &&
            getWorldAt(static_cast<int>(player.x - player_half_width), static_cast<int>(ny + player_half_width)) != WALL &&
            getWorldAt(static_cast<int>(player.x + player_half_width), static_cast<int>(ny + player_half_width)) != WALL
        );

        // Apply movement if no collision
        if (can_move_x) player.x = nx;
        if (can_move_y) player.y = ny;

        // Jump
        bool space = key(VK_SPACE);
        if(space && !spacePrev && player.onGround){
            player.vz = JUMP_FORCE;
            player.onGround = false;
            // playSound("jump.wav"); // Requires jump.wav file
        }
        spacePrev = space;

        // Apply gravity
        if(!player.onGround) {
            player.vz -= GRAVITY;
            player.z += player.vz;
        }

        // Collision with floor (z=0)
        if(player.z <= PLAYER_HEIGHT){
            player.z = PLAYER_HEIGHT;
            player.vz = 0;
            player.onGround = true;
        }

        // Minimap toggle
        if(key('M')&&!mPrev) showMiniMap=!showMiniMap; 
        mPrev=key('M');

        // K key toggle for backrooms mode
        if(key('K')&&!kPrev) backroomsMode=!backroomsMode;
        kPrev=key('K');

        // R key toggle for minimap flip (NEW)
        bool r = key('R');
        if(r && !rPrev) minimap_flipped = !minimap_flipped;
        rPrev = r;

        // Cycle selected block type for placing
        bool cyclePrev = key(VK_OEM_4); // VK_OEM_4 is '['
        bool cycleNext = key(VK_OEM_6); // VK_OEM_6 is ']'

        const int min_placeable_type = WALL; // BlockType::WALL (1)
        const int max_placeable_type = TRAP; // BlockType::TRAP (5)
        const int num_placeable_types = max_placeable_type - min_placeable_type + 1; // 5 types

        if(cyclePrev && !cyclePrevPrev){
            selectedBlockType = (BlockType)(((selectedBlockType - min_placeable_type - 1 + num_placeable_types) % num_placeable_types) + min_placeable_type);
            // playSound("change_block.wav"); // Requires sound
        }
        if(cycleNext && !cycleNextPrev){
            selectedBlockType = (BlockType)(((selectedBlockType - min_placeable_type + 1) % num_placeable_types) + min_placeable_type);
            // playSound("change_block.wav"); // Requires sound
        }
        cyclePrevPrev = cyclePrev;
        cycleNextPrev = cycleNext;

        // --- NEW: Minimap size change ---
        bool plus = key(VK_OEM_PLUS); // For '+' key
        bool minus = key(VK_OEM_MINUS); // For '-' key

        if (plus && !plusPrev) {
            minimap_radius = min(MAX_MINIMAP_RADIUS, minimap_radius + 1);
        }
        if (minus && !minusPrev) {
            minimap_radius = max(MIN_MINIMAP_RADIUS, minimap_radius - 1);
        }
        plusPrev = plus;
        minusPrev = minus;
        // --- END NEW ---

        // --- Interaction Logic ---
        // Player's current tile (player.x, player.y are the coordinates for the center of the player,
        // static_cast<int> takes the integer part, effectively the bottom-left corner of the tile)
        int current_tile_x = static_cast<int>(player.x);
        int current_tile_y = static_cast<int>(player.y);
        int current_tile_type = getWorldAt(current_tile_x, current_tile_y);

        if (current_tile_type == TREASURE) {
            player.treasures++;
            setWorldAt(current_tile_x, current_tile_y, EMPTY); // Consume treasure
            // playSound("collect.wav"); // Requires collect.wav file
        } else if (current_tile_type == EXIT) {
            gameOver = true; // Game over, player won
            gameEndReason = 1; // Set win reason
        } else if (current_tile_type == MONSTER || current_tile_type == TRAP) {
            player.health -= 10; // Take damage
            // === NEW: Trigger red flash on damage ===
            damageFlashTimer = DAMAGE_FLASH_DURATION;
            // ========================================

            if (player.health < 0) player.health = 0; // Cap health at 0
            setWorldAt(current_tile_x, current_tile_y, EMPTY); // Remove monster/trap after damage
            // playSound("hit.wav"); // Requires hit.wav file
            // --- New: If it was a monster, remove it from the dynamic list ---
            if (current_tile_type == MONSTER) {
                // Iterate backwards to safely erase
                for (int i = monsters.size() - 1; i >= 0; --i) {
                    // Check if this monster object is at the tile where player took damage
                    // Monster coordinates are centered (X.5), static_cast<int> gives tile index.
                    if (static_cast<int>(monsters[i].x) == current_tile_x && static_cast<int>(monsters[i].y) == current_tile_y) {
                        monsters.erase(monsters.begin() + i);
                        break; // Found and removed, exit loop
                    }
                }
            }
            // --- End New ---
        }

        if (player.health <= 0) {
            gameOver = true; // Game over, player lost
            gameEndReason = 2; // Set lose reason
        }

        // Calculate target tile for digging/placing
        // Assuming 1.5 units in front for interaction distance.
        int target_tile_x = static_cast<int>(player.x + player.cosA * 1.5);
        int target_tile_y = static_cast<int>(player.y + player.sinA * 1.5);

        // Prevent targeting own tile
        if (target_tile_x == current_tile_x && target_tile_y == current_tile_y) {
            // Don't interact with own tile for digging/placing
        } else {
            // Digging (X key) - now destroys any non-empty block
            if(key('X') && !xPrev){
                int block_to_destroy = getWorldAt(target_tile_x, target_tile_y);
                if(block_to_destroy != EMPTY){ // Destroy any non-empty block
                    setWorldAt(target_tile_x, target_tile_y, EMPTY);

                    // --- New: If it was a monster, also remove from dynamic list ---
                    if (block_to_destroy == MONSTER) {
                        // Iterate backwards to safely erase
                        for (int i = monsters.size() - 1; i >= 0; --i) {
                            if (static_cast<int>(monsters[i].x) == target_tile_x && static_cast<int>(monsters[i].y) == target_tile_y) {
                                monsters.erase(monsters.begin() + i);
                                break;
                            }
                        }
                    }
                    // --- End New ---

                    // Add particle effects based on destroyed block type
                    char p_char = '#';
                    WORD p_color = DARK_GREY;
                    switch (block_to_destroy) {
                        case WALL:      p_char = '#'; p_color = DARK_GREY; break;
                        case TREASURE:  p_char = '$'; p_color = YELLOW; break;
                        case EXIT:      p_char = 'E'; p_color = GREEN; break;
                        case MONSTER:   p_char = 'M'; p_color = RED; break; // Monster particles
                        case TRAP:      p_char = 'T'; p_color = DARK_RED; break; // Trap particles
                    }

                    for(int i=0; i<10; ++i){
                        double px = target_tile_x + 0.5;
                        double py = target_tile_y + 0.5;
                        double pz = player.z; // Start particles around player's height

                        double angle = (double)rand()/RAND_MAX * PI * 2;
                        double speed = (double)rand()/RAND_MAX * 0.2 + 0.05;
                        double vz_p = (double)rand()/RAND_MAX * 0.1 + 0.05;

                        particles.push_back({
                            px, py, pz,
                            cos(angle) * speed, sin(angle) * speed, vz_p,
                            30, p_char, p_color
                        });
                    }
                    // playSound("dig.wav"); // Requires dig.wav file
                }
            }
            // Placing Selected Block Type (1 key)
            if (key('1') && !onePrev) {
                if (getWorldAt(target_tile_x, target_tile_y) == EMPTY) { // Only place on empty
                    // --- New: If placing a monster, add to dynamic list as well ---
                    if (selectedBlockType == MONSTER) {
                        monsters.push_back({
                            (double)target_tile_x + 0.5,
                            (double)target_tile_y + 0.5,
                            MONSTER_MOVE_DELAY,
                            (int)monsters.size()
                        });
                    }
                    // --- End New ---
                    setWorldAt(target_tile_x, target_tile_y, selectedBlockType);
                    // playSound("place.wav"); // Requires place.wav file
                }
            }
        }
        xPrev = key('X');
        onePrev = key('1');

        // --- New: Monster AI Update ---
        for (int i = 0; i < monsters.size(); ++i) {
            Monster& m = monsters[i];
            m.moveCooldown -= deltaTime; // Reduce cooldown time
            int old_mx = static_cast<int>(m.x); // Current tile of monster
            int old_my = static_cast<int>(m.y);
            double dx = player.x - m.x;
            double dy = player.y - m.y;
            double dist_to_player_sq = dx*dx + dy*dy; // Use squared distance for efficiency
            // Only move if within detection range (squared for efficiency) and cooldown is ready
            if (dist_to_player_sq <= MONSTER_DETECTION_RANGE * MONSTER_DETECTION_RANGE && m.moveCooldown <= 0) {
                m.moveCooldown = MONSTER_MOVE_DELAY; // Reset cooldown

                int dir_x = 0;
                if (dx > 0.1) dir_x = 1; // Small threshold to avoid issues with dx exactly 0
                else if (dx < -0.1) dir_x = -1;

                int dir_y = 0;
                if (dy > 0.1) dir_y = 1;
                else if (dy < -0.1) dir_y = -1;

                bool moved_this_turn = false;

                // Priority:
                // 1. Try to move diagonally (both X and Y directions) if player is not directly axial.
                // 2. If diagonal fails or not applicable, try moving along the X-axis.
                // 3. If X-axis fails, try moving along the Y-axis.

                // Attempt 1: Diagonal move (if both X and Y movement are desired)
                if (dir_x != 0 && dir_y != 0) {
                    moved_this_turn = tryMoveMonster(m, old_mx, old_my, old_mx + dir_x, old_my + dir_y);
                }

                // Attempt 2: Axial X move (if not already moved diagonally or if only X movement is desired)
                if (!moved_this_turn && dir_x != 0) {
                    moved_this_turn = tryMoveMonster(m, old_mx, old_my, old_mx + dir_x, old_my);
                }

                // Attempt 3: Axial Y move (if not already moved by any previous attempts or if only Y movement is desired)
                if (!moved_this_turn && dir_y != 0) {
                    moved_this_turn = tryMoveMonster(m, old_mx, old_my, old_mx, old_my + dir_y);
                }
            }
        }
        // --- End Monster AI Update ---

        // --- Particle Update ---
        for (int i = particles.size() - 1; i >= 0; --i) {
            Particle& p = particles[i];

            p.x += p.vx;
            p.y += p.vy;
            p.z += p.vz;
            p.vz -= PARTICLE_GRAVITY;

            p.vx *= PARTICLE_FRICTION;
            p.vy *= PARTICLE_FRICTION;
            p.vz *= PARTICLE_FRICTION;

            p.life--;
            if (p.life <= 0) {
                particles.erase(particles.begin() + i);
            }
        }

        // --- Render Frame ---
        renderFrame();

        // --- Exit Condition ---
        if(key(VK_ESCAPE)) {
            gameOver = true; // Manual exit
            gameEndReason = 0; // Set manual exit reason
            break;
        }

        // --- Frame Rate Control ---
         Sleep(15); // Using deltaTime for monster movement, fixed Sleep less critical. Can be used for overall frame rate cap.
    }

    // Now, determine game end message based on gameEndReason
    if (gameEndReason == 2) { // Player lost (health <= 0)
        displayGameEndScreen("GAME OVER! You ran out of health. Time: " + to_string((GetTickCount64() - startTime) / 1000) + "s");
    } else if (gameEndReason == 1) { // Player won (reached exit)
        displayGameEndScreen("YOU ESCAPED! Treasures collected: " + to_string(player.treasures) + ". Time: " + to_string((GetTickCount64() - startTime) / 1000) + "s");
    } else { // Manual exit (gameEndReason == 0)
        displayGameEndScreen("Thanks for playing!");
    }

    free(screen);
    return 0;
}