专栏文章
迷宫1
个人记录参与者 3已保存评论 3
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 3 条
- 当前快照
- 1 份
- 快照标识符
- @mioruvnb
- 此快照首次捕获于
- 2025/12/03 00:07 3 个月前
- 此快照最后确认于
- 2025/12/03 00:07 3 个月前
CPP
#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;
}
相关推荐
评论
共 3 条评论,欢迎与作者交流。
正在加载评论...