#include #include #include #include #include #include #include #include "raylib.h" #include "world.h" #include "player.h" #include "vec_math.h" #include "rendering.h" #include "utils.h" #include "menu.h" //#include "clouds.h" #include "console.h" #if defined(PLATFORM_DESKTOP) #define SDF_GLSL_VER 330 #else #define SDF_GLSL_VER 100 #endif static Shader sdf_shader = {0}; static bool sdf_font_is_sdf = false; // graphics and player constants #define WINDOW_WIDTH 1200 #define WINDOW_HEIGHT 800 #define TARGET_FPS 144 #define RENDER_DISTANCE 50.0f #define FOG_START 30.0f #define CULLING_FOV 110.0f // Helper function to load a specific font variant static Font load_font_variant(const char* font_family, const char* font_variant) { char font_path[512]; snprintf(font_path, sizeof(font_path), "./assets/fonts/%s/ttf/%s", font_family, font_variant); // Build codepoint array with all ASCII and common extended characters int codepoints[1024] = {0}; int codepoint_count = 0; // Full ASCII range (0-127) for (int i = 0; i < 128; i++) { codepoints[codepoint_count++] = i; } // Latin-1 Supplement (128-255) for (int i = 128; i < 256; i++) { codepoints[codepoint_count++] = i; } // Latin Extended-A (256-383) for (int i = 256; i < 384; i++) { codepoints[codepoint_count++] = i; } // Cyrillic block (0x0400-0x04FF) for (int i = 0x0400; i <= 0x04FF && codepoint_count < 1024; i++) { codepoints[codepoint_count++] = i; } // Load font at a large base_size to generate high-quality glyphs // Now that HIGHDPI is disabled, we can use a moderate base_size for smooth scaling. int base_size = 256; Font font = LoadFontEx(font_path, base_size, codepoints, codepoint_count); if (font.glyphCount > 0) { // Use bilinear filtering for smooth downsampling SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); GenTextureMipmaps(&font.texture); sdf_font_is_sdf = false; TraceLog(LOG_INFO, "Loaded high-quality font %s size=%d glyphs=%d (bilinear + mipmaps)", font_path, base_size, font.glyphCount); return font; } // Fallback to default if font not found sdf_font_is_sdf = false; TraceLog(LOG_WARNING, "Failed to load font %s, using default font", font_path); return GetFontDefault(); } // Helper function to load a font by family (uses first variant found) static Font load_font_by_name(const char* font_name) { char ttf_dir[512]; snprintf(ttf_dir, sizeof(ttf_dir), "./assets/fonts/%s/ttf", font_name); // Open the ttf directory and find the first .ttf file DIR* dir = opendir(ttf_dir); if (dir) { struct dirent* entry; while ((entry = readdir(dir))) { // Look for .ttf files char* dot = strrchr(entry->d_name, '.'); if (dot && strcmp(dot, ".ttf") == 0) { // Found a .ttf file, try to load it closedir(dir); return load_font_variant(font_name, entry->d_name); } } closedir(dir); } // Fallback to default if no font found return GetFontDefault(); } // Wrapper that draws `font` - fonts rasterized at 256px with mipmaps for smooth scaling void DrawTextExCustom(Font font, const char *text, Vector2 position, float fontSize, float spacing, Color tint) { // High-quality rasterization with mipmaps produces crisp text at various sizes DrawTextEx(font, text, position, fontSize, spacing, tint); } static const char* get_cardinal_direction(float heading_deg) { if (heading_deg < 22.5f || heading_deg >= 337.5f) return "N"; if (heading_deg < 67.5f) return "NE"; if (heading_deg < 112.5f) return "E"; if (heading_deg < 157.5f) return "SE"; if (heading_deg < 202.5f) return "S"; if (heading_deg < 247.5f) return "SW"; if (heading_deg < 292.5f) return "W"; return "NW"; } static void draw_compass_hud(Font font, float yaw_radians) { float heading_deg = yaw_radians * (180.0f / 3.14159265f); heading_deg = fmodf(heading_deg, 360.0f); if (heading_deg < 0.0f) heading_deg += 360.0f; const char* cardinal = get_cardinal_direction(heading_deg); char heading_text[64]; snprintf(heading_text, sizeof(heading_text), "Heading: %.0f° %s", heading_deg, cardinal); int screen_w = GetScreenWidth(); Vector2 text_size = MeasureTextEx(font, heading_text, 28, 1); Vector2 text_pos = { (float)(screen_w - (int)text_size.x - 20), 20.0f }; DrawRectangle((int)text_pos.x - 10, (int)text_pos.y - 8, (int)text_size.x + 20, (int)text_size.y + 16, (Color){0, 0, 0, 150}); DrawTextExCustom(font, heading_text, text_pos, 28, 1, WHITE); } B3DV_MAIN int b3dv_main(int argc, char **argv) { // if no args, print version and help if (argc == 1) { //puts("b3dv version 0.0.20-beta"); puts("Usage:"); puts(" b3dv [--version|-v] - version info"); puts(" b3dv run - launch game"); return 0; } // version argument if (argc > 1 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0)) { puts("b3dv version 0.0.20-beta"); puts("By Jimena Neumann; BSD-3-Clause License"); #ifdef DEBUG puts("compiled on " __DATE__ " at " __TIME__); #endif return 0; } // run if (argc > 1 && strcmp(argv[1], "run") == 0) { // continue to launch game } else { fprintf(stderr, "Unknown argument: %s\n", argv[1]); return 1; } // Disable HIGHDPI to avoid fractional scaling issues with Hyprland's 1.2x compositor scaling // Render at 1200x800 logical pixels; let window manager handle physical scaling SetConfigFlags(FLAG_WINDOW_RESIZABLE); InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "b3dv 0.0.20-beta"); // Load SDF shader from project assets (avoid external/ references) sdf_shader = (Shader){0}; int try_versions[] = { 330, 120, 100 }; for (int i = 0; i < 3; i++) { int ver = try_versions[i]; if (ver == 0) continue; char vsPath[256]; char fsPath[256]; snprintf(vsPath, sizeof(vsPath), "assets/shaders/glsl%d/sdf.vs", ver); snprintf(fsPath, sizeof(fsPath), "assets/shaders/glsl%d/sdf.fs", ver); if (FileExists(vsPath) && FileExists(fsPath)) { sdf_shader = LoadShader(vsPath, fsPath); if (sdf_shader.id != 0) { TraceLog(LOG_INFO, "Loaded SDF shader from %s and %s", vsPath, fsPath); break; } else { TraceLog(LOG_WARNING, "Failed to load shader files %s / %s", vsPath, fsPath); } } } // Disable default ESC key behavior (we handle it manually for pause menu) SetExitKey(KEY_NULL); // Enable info-level logging for diagnosis SetTraceLogLevel(LOG_INFO); // Debug: print window and DPI info Vector2 dpi = GetWindowScaleDPI(); TraceLog(LOG_INFO, "Window size: %dx%d, DPI scale: (%f, %f)", GetScreenWidth(), GetScreenHeight(), dpi.x, dpi.y); // Initialize world save system (needed for menu world scanning) world_system_init(); // Create menu system MenuSystem* menu = menu_system_create(); // Initialize console input system console_init(); // Set target FPS based on menu settings (will be updated dynamically) SetTargetFPS(menu->max_fps); int last_target_fps = menu->max_fps; // Load initial font from menu settings Font custom_font = load_font_variant(menu->font_families[menu->current_font_family_index], menu->font_variants[menu->current_font_variant_index]); char last_loaded_family[256]; char last_loaded_variant[256]; strncpy(last_loaded_family, menu->font_families[menu->current_font_family_index], sizeof(last_loaded_family) - 1); last_loaded_family[sizeof(last_loaded_family) - 1] = '\0'; strncpy(last_loaded_variant, menu->font_variants[menu->current_font_variant_index], sizeof(last_loaded_variant) - 1); last_loaded_variant[sizeof(last_loaded_variant) - 1] = '\0'; Camera3D camera = { .position = (Vector3){ 20, 15, 20 }, .target = (Vector3){ 8, 4, 8 }, .up = (Vector3){ 0, 1, 0 }, .fovy = 90, .projection = CAMERA_PERSPECTIVE }; // World and player will be created after menu selection World* world = NULL; Player* player = NULL; //CloudSystem* clouds = NULL; // enable mouse capture (will be disabled in menu) bool mouse_captured = false; // HUD mode (0 = default, 1 = performance metrics 2 = player, 3 = system info) int hud_mode = 0; int prev_hud_mode = -1; // Track previous mode to detect changes bool hud_visible = true; // Toggle HUD visibility with F1 char cached_cpu[128] = {0}; char cached_gpu[128] = {0}; char cached_kernel[128] = {0}; // Wireframe rendering mode (toggled with F7) bool show_wireframe = false; bool show_chunk_borders = false; // Flight system bool flight_enabled = false; // Pause state bool paused = false; bool pause_settings_open = false; bool should_quit = false; // Chat system bool chat_active = false; char chat_input[256] = {0}; int chat_input_len = 0; int chat_cursor_pos = 0; // cursor position in input string int history_index = 0; // 0 means no history entry selected, 1+ means Nth-from-last // Chat message display (feedback messages) #define CHAT_MESSAGE_BUFFER_SIZE 16 #define CHAT_MESSAGE_MAX_LEN 512 char chat_messages[CHAT_MESSAGE_BUFFER_SIZE][CHAT_MESSAGE_MAX_LEN] = {0}; double chat_message_times[CHAT_MESSAGE_BUFFER_SIZE] = {0}; int chat_message_count = 0; // Macro to add a chat message to the buffer #define add_chat_message(message) \ do { \ int _msg_idx = chat_message_count % CHAT_MESSAGE_BUFFER_SIZE; \ strncpy(chat_messages[_msg_idx], (message), CHAT_MESSAGE_MAX_LEN - 1); \ chat_messages[_msg_idx][CHAT_MESSAGE_MAX_LEN - 1] = '\0'; \ chat_message_times[_msg_idx] = GetTime(); \ chat_message_count++; \ } while(0) // Camera angle tracking (in radians) float camera_yaw = 0.0f; // horizontal rotation around Y axis float camera_pitch = 0.0f; // vertical rotation (pitch) // Persistent camera basis vectors (to handle gimbal lock) Vector3 camera_right = {1, 0, 0}; Vector3 camera_up = {0, 1, 0}; // Persistent camera forward (actual look direction) Vector3 camera_forward = (Vector3){0, 0, 1}; // Block highlighting for raycast int highlighted_block_x = 0; int highlighted_block_y = 0; int highlighted_block_z = 0; bool has_highlighted_block = false; // Initialize camera angles from initial forward direction camera_yaw = 0.0f; camera_pitch = 0.0f; // Raycast caching - only update every 3 frames int raycast_frame_counter = 0; // FOV values for visibility checks (computed dynamically to handle window resizing) float fov_half_vert_tan = 0.0f; float fov_half_horiz_tan = 0.0f; B3DV_MAIN_LOOP while (!WindowShouldClose() && !should_quit) { float dt = GetFrameTime(); // Temporary debug overlay: show magenta clear for first 120 frames static int debug_frames = 120; if (debug_frames > 0) { debug_frames--; BeginDrawing(); ClearBackground(MAGENTA); DrawText("DEBUG OVERLAY", 50, 50, 40, WHITE); EndDrawing(); continue; } // Check if font family or variant changed in settings and reload if needed if (strcmp(menu->font_families[menu->current_font_family_index], last_loaded_family) != 0 || strcmp(menu->font_variants[menu->current_font_variant_index], last_loaded_variant) != 0) { // Unload old font if it's not the default if (custom_font.glyphCount > 0 && custom_font.glyphCount != GetFontDefault().glyphCount) { UnloadFont(custom_font); } // Reset SDF flag when font changes sdf_font_is_sdf = false; // Load new font variant custom_font = load_font_variant(menu->font_families[menu->current_font_family_index], menu->font_variants[menu->current_font_variant_index]); strcpy(last_loaded_family, menu->font_families[menu->current_font_family_index]); strcpy(last_loaded_variant, menu->font_variants[menu->current_font_variant_index]); } // Handle menu state if (menu->current_state == MENU_STATE_MAIN) { BeginDrawing(); menu_draw_main(menu, custom_font); EndDrawing(); menu_update_input(menu); continue; } else if (menu->current_state == MENU_STATE_WORLD_SELECT) { BeginDrawing(); menu_draw_world_select(menu, custom_font); EndDrawing(); menu_update_input(menu); continue; } else if (menu->current_state == MENU_STATE_CREATE_WORLD) { BeginDrawing(); menu_draw_create_world(menu, custom_font); EndDrawing(); menu_update_input(menu); continue; } else if (menu->current_state == MENU_STATE_CREDITS) { BeginDrawing(); menu_draw_credits(menu, custom_font); EndDrawing(); menu_update_input(menu); continue; } else if (menu->current_state == MENU_STATE_SETTINGS) { BeginDrawing(); menu_draw_settings(menu, custom_font); EndDrawing(); menu_update_input(menu); continue; } else if (menu->current_state == MENU_STATE_GAME && menu->should_start_game) { // Initialize game with selected world if (!world) { world = world_create(); world->compress_chunk_files = menu->create_world_compress; // Try to load the selected world, or generate new one if (!world_load(world, menu->selected_world_name)) { world_generate_prism(world); world_save(world, menu->selected_world_name); } // Load block textures world_load_textures(world); // Create player at saved position (or default if no save) player = player_create(world->last_player_position.x, world->last_player_position.y, world->last_player_position.z); // Register player with world and apply saved inventory if present world->current_player = player; world_apply_players_to(world, player); if (menu->nickname[0] != '\0') { strncpy(world->player_nickname, menu->nickname, sizeof(world->player_nickname) - 1); world->player_nickname[sizeof(world->player_nickname) - 1] = '\0'; strncpy(player->nickname, menu->nickname, sizeof(player->nickname) - 1); player->nickname[sizeof(player->nickname) - 1] = '\0'; } #if 0 // Create cloud system clouds = clouds_create("./assets/textures/clouds.png"); // Apply cloud settings from menu if (clouds) { clouds->enabled = menu->clouds_enabled; clouds->render_distance = menu->clouds_render_distance; } #endif // Enable mouse capture for gameplay mouse_captured = true; DisableCursor(); // Initialize camera forward Vector3 initial_forward = vec3_normalize(vec3_sub(camera.target, camera.position)); camera_forward = initial_forward; camera_yaw = atan2f(initial_forward.x, initial_forward.z); camera_pitch = asinf(initial_forward.y); } menu->should_start_game = false; } // Game logic below (only runs when in GAME state and world is initialized) if (!world || !player) { continue; } // Recalculate FOV values based on current window size (handles window resizing) float window_aspect = (float)GetScreenWidth() / (float)GetScreenHeight(); float fovy_rad = CULLING_FOV * 3.14159265f / 180.0f; fov_half_vert_tan = tanf(fovy_rad / 2.0f); fov_half_horiz_tan = fov_half_vert_tan * window_aspect; // Update target FPS from settings only when it changes to avoid repeated INFO messages if (menu->max_fps != last_target_fps) { SetTargetFPS(menu->max_fps); last_target_fps = menu->max_fps; } // Chat system - handle first to consume all input when active if (chat_active) { // Text input int key = GetCharPressed(); while (key > 0) { if ((key >= 32 && key <= 125) && chat_input_len < 255) { for (int i = chat_input_len; i > chat_cursor_pos; i--) chat_input[i] = chat_input[i - 1]; chat_input[chat_cursor_pos++] = (char)key; chat_input_len++; chat_input[chat_input_len] = '\0'; } key = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE) && chat_cursor_pos > 0) { for (int i = chat_cursor_pos - 1; i < chat_input_len; i++) chat_input[i] = chat_input[i + 1]; chat_cursor_pos--; chat_input_len--; chat_input[chat_input_len] = '\0'; } if (IsKeyPressed(KEY_LEFT) && chat_cursor_pos > 0) chat_cursor_pos--; if (IsKeyPressed(KEY_RIGHT) && chat_cursor_pos < chat_input_len) chat_cursor_pos++; if (IsKeyPressed(KEY_UP)) { history_index++; char history_line[256] = {0}; if (get_chat_history_line(history_index, history_line, sizeof(history_line))) { strncpy(chat_input, history_line, sizeof(chat_input) - 1); chat_input[sizeof(chat_input) - 1] = '\0'; chat_input_len = strlen(chat_input); chat_cursor_pos = chat_input_len; } else history_index--; } if (IsKeyPressed(KEY_DOWN)) { history_index--; if (history_index <= 0) { history_index = 0; chat_input[0] = '\0'; chat_input_len = 0; chat_cursor_pos = 0; } else { char history_line[256] = {0}; if (get_chat_history_line(history_index, history_line, sizeof(history_line))) { strncpy(chat_input, history_line, sizeof(chat_input) - 1); chat_input[sizeof(chat_input) - 1] = '\0'; chat_input_len = strlen(chat_input); chat_cursor_pos = chat_input_len; } } } if (IsKeyPressed(KEY_ENTER)) { if (chat_input_len > 0) { FILE* log_file = fopen("./chathistory", "a"); if (log_file) { fprintf(log_file, "%s\n", chat_input); fclose(log_file); } } // Process command or chat if (chat_input[0] == '/') { // delegate to existing command handlers by reusing the original blocks // (kept identical to previous implementation) if (strncmp(chat_input, "/quit", 5) == 0) { add_chat_message(menu->game_text.msg_quitting); should_quit = true; } else if (strncmp(chat_input, "/tp ", 4) == 0) { float x, y, z; if (sscanf(chat_input, "/tp %f %f %f", &x, &y, &z) == 3) { player->position = (Vector3){ x, y, z }; player->velocity = (Vector3){0,0,0}; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_teleported, x, y, z); add_chat_message(msg); } else add_chat_message(menu->game_text.msg_teleport_usage); } else if (strncmp(chat_input, "/save ", 6) == 0) { char world_name_buf[256]; strncpy(world_name_buf, chat_input + 6, sizeof(world_name_buf) - 1); world_name_buf[255] = '\0'; trim_string(world_name_buf); const char* world_name = world_name_buf; if (world_save(world, world_name)) { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_world_saved, world_name); add_chat_message(msg); } else { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_world_save_failed, world_name); add_chat_message(msg); } } else if (strncmp(chat_input, "/load ", 6) == 0) { char world_name_buf[256]; strncpy(world_name_buf, chat_input + 6, sizeof(world_name_buf) - 1); world_name_buf[255] = '\0'; trim_string(world_name_buf); const char* world_name = world_name_buf; if (strlen(world->world_name) > 0) world_save(world, world->world_name); if (world_load(world, world_name)) { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_world_loaded, world_name); add_chat_message(msg); } else { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_world_load_failed, world_name); add_chat_message(msg); world_generate_prism(world); } } else if (strncmp(chat_input, "/createworld ", 13) == 0) { char world_name_buf[256]; strncpy(world_name_buf, chat_input + 13, sizeof(world_name_buf) - 1); world_name_buf[255] = '\0'; trim_string(world_name_buf); const char* world_name = world_name_buf; bool valid_name = true; if (world_name[0] == '\0') valid_name = false; else for (int i = 0; world_name[i]; i++) { char c = world_name[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) { valid_name = false; break; }} if (!valid_name) add_chat_message("Invalid world name. Use only alphanumeric characters and underscore."); else { world_free(world);/* clouds_reset(clouds);*/ world = world_create(); world_load_textures(world); strncpy(world->world_name, world_name, sizeof(world->world_name) - 1); world->world_name[sizeof(world->world_name) - 1] = '\0'; world_generate_prism(world); if (world_save(world, world_name)) { char msg[512]; snprintf(msg, sizeof(msg), "World '%s' created and saved successfully.", world_name); add_chat_message(msg); player->position = (Vector3){0,105,0}; player->velocity = (Vector3){0,0,0}; } else { char msg[512]; snprintf(msg, sizeof(msg), "Failed to create world '%s'.", world_name); add_chat_message(msg); } } } else if (strncmp(chat_input, "/loadworld ", 11) == 0) { char world_name_buf[256]; strncpy(world_name_buf, chat_input + 11, sizeof(world_name_buf) - 1); world_name_buf[255] = '\0'; trim_string(world_name_buf); const char* world_name = world_name_buf; bool valid_name = true; if (world_name[0] == '\0') valid_name = false; else for (int i = 0; world_name[i]; i++) { char c = world_name[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) { valid_name = false; break; }} if (!valid_name) add_chat_message(menu->game_text.msg_invalid_world_name); else { world_free(world); /* clouds_reset(clouds);*/ world = world_create(); world_load_textures(world); float spawn_x = 8.0f; float spawn_z = 8.0f; float h1 = sinf(spawn_x * 0.1f) * cosf(spawn_z * 0.1f) * 8.0f; float h2 = sinf(spawn_x * 0.05f) * cosf(spawn_z * 0.05f) * 6.0f; float terrain_h = h1 + h2 + 10.0f + 5.0f; float spawn_y = terrain_h + 1.5f; player = player_create(spawn_x, spawn_y, spawn_z); if (world_load(world, world_name)) { player->position = (Vector3){0,105,0}; player->velocity = (Vector3){0,0,0}; char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_world_loaded, world_name); add_chat_message(msg); } else { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_world_load_failed, world_name); add_chat_message(msg); world_generate_prism(world); } } } else if (strncmp(chat_input, "/select ", 8) == 0) { char block_name_buf[32]; strncpy(block_name_buf, chat_input + 8, sizeof(block_name_buf) - 1); block_name_buf[31] = '\0'; trim_string(block_name_buf); const char* block_name = block_name_buf; if (strcmp(block_name, "stone") == 0) { player->selected_block = BLOCK_STONE; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_selected, "stone"); add_chat_message(msg); } else if (strcmp(block_name, "dirt") == 0) { player->selected_block = BLOCK_DIRT; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_selected, "dirt"); add_chat_message(msg); } else if (strcmp(block_name, "grass") == 0) { player->selected_block = BLOCK_GRASS; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_selected, "grass"); add_chat_message(msg); } else if (strcmp(block_name, "sand") == 0) { player->selected_block = BLOCK_SAND; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_selected, "sand"); add_chat_message(msg); } else if (strcmp(block_name, "wood") == 0) { player->selected_block = BLOCK_WOOD; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_selected, "wood"); add_chat_message(msg); } else if (strcmp(block_name, "glowstone") == 0) { player->selected_block = BLOCK_GLOWSTONE; char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_selected, "glowstone"); add_chat_message(msg); } else { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_unknown_block, block_name); add_chat_message(msg); } } else if (strncmp(chat_input, "/fly ", 5) == 0) { char fly_cmd_buf[32]; strncpy(fly_cmd_buf, chat_input + 5, sizeof(fly_cmd_buf) - 1); fly_cmd_buf[31] = '\0'; trim_string(fly_cmd_buf); const char* fly_cmd = fly_cmd_buf; if (strcmp(fly_cmd, "enable") == 0) { flight_enabled = true; add_chat_message(menu->game_text.msg_flight_enabled); } else if (strcmp(fly_cmd, "disable") == 0) { flight_enabled = false; player->is_flying = false; add_chat_message(menu->game_text.msg_flight_disabled); } else add_chat_message(menu->game_text.msg_fly_usage); } else if (strncmp(chat_input, "/noclip ", 8) == 0) { char noclip_cmd_buf[32]; strncpy(noclip_cmd_buf, chat_input + 8, sizeof(noclip_cmd_buf) - 1); noclip_cmd_buf[31] = '\0'; trim_string(noclip_cmd_buf); const char* noclip_cmd = noclip_cmd_buf; if (strcmp(noclip_cmd, "enable") == 0) { player->no_clip = true; add_chat_message(menu->game_text.msg_noclip_enabled); } else if (strcmp(noclip_cmd, "disable") == 0) { player->no_clip = false; add_chat_message(menu->game_text.msg_noclip_disabled); } else add_chat_message(menu->game_text.msg_noclip_usage); } else if (strncmp(chat_input, "/show_chunk_borders", 19) == 0) { const char* arg = chat_input + 19; while (*arg == ' ') arg++; if (*arg == '\0') { show_chunk_borders = !show_chunk_borders; add_chat_message(show_chunk_borders ? menu->game_text.msg_chunk_borders_enabled : menu->game_text.msg_chunk_borders_disabled); } else if (strncmp(arg, "on", 2) == 0 && (arg[2] == '\0' || arg[2] == ' ')) { show_chunk_borders = true; add_chat_message(menu->game_text.msg_chunk_borders_enabled); } else if (strncmp(arg, "off", 3) == 0 && (arg[3] == '\0' || arg[3] == ' ')) { show_chunk_borders = false; add_chat_message(menu->game_text.msg_chunk_borders_disabled); } else { add_chat_message(menu->game_text.msg_chunk_borders_usage); } } else if (strncmp(chat_input, "/setblock ", 10) == 0) { float fx, fy, fz; char block_name[32] = {0}; int parsed = sscanf(chat_input, "/setblock %f %f %f %31s", &fx, &fy, &fz, block_name); if (parsed >= 3) { if (parsed == 4) trim_string(block_name); int ix = (int)floorf(fx); int iy = (int)floorf(fy); int iz = (int)floorf(fz); BlockType block_type = BLOCK_AIR; const char* type_str = "air"; if (parsed == 3) { block_type = player->selected_block; if (block_type == BLOCK_STONE) type_str = "stone"; else if (block_type == BLOCK_DIRT) type_str = "dirt"; else if (block_type == BLOCK_GRASS) type_str = "grass"; else if (block_type == BLOCK_SAND) type_str = "sand"; else if (block_type == BLOCK_WOOD) type_str = "wood"; else if (block_type == BLOCK_GLOWSTONE) type_str = "glowstone"; } else { if (strcmp(block_name, "stone") == 0) { block_type = BLOCK_STONE; type_str = "stone"; } else if (strcmp(block_name, "dirt") == 0) { block_type = BLOCK_DIRT; type_str = "dirt"; } else if (strcmp(block_name, "sand") == 0) { block_type = BLOCK_SAND; type_str = "sand"; } else if (strcmp(block_name, "wood") == 0) { block_type = BLOCK_WOOD; type_str = "wood"; } else if (strcmp(block_name, "grass") == 0) { block_type = BLOCK_GRASS; type_str = "grass"; } else if (strcmp(block_name, "glowstone") == 0) { block_type = BLOCK_GLOWSTONE; type_str = "glowstone"; } else if (strcmp(block_name, "air") == 0) { block_type = BLOCK_AIR; type_str = "air"; } else { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_unknown_block, block_name); add_chat_message(msg); ix = -1; } } if (iy >= 0 && iy < 256) { world_set_block(world, ix, iy, iz, block_type); char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_block_set, fx, fy, fz, type_str); add_chat_message(msg); } else if (ix != -1) add_chat_message(menu->game_text.msg_out_of_bounds); } else add_chat_message(menu->game_text.msg_setblock_usage); } else if (strncmp(chat_input, "/give ", 6) == 0) { char item_buf[64] = {0}; int count = 1; int parsed = sscanf(chat_input + 6, "%63s %d", item_buf, &count); if (parsed >= 1) { trim_string(item_buf); if (parsed == 1) count = 1; BlockType bt = BLOCK_AIR; if (strcmp(item_buf, "stone") == 0) bt = BLOCK_STONE; else if (strcmp(item_buf, "dirt") == 0) bt = BLOCK_DIRT; else if (strcmp(item_buf, "grass") == 0) bt = BLOCK_GRASS; else if (strcmp(item_buf, "sand") == 0) bt = BLOCK_SAND; else if (strcmp(item_buf, "wood") == 0) bt = BLOCK_WOOD; else if (strcmp(item_buf, "glowstone") == 0) bt = BLOCK_GLOWSTONE; if (bt == BLOCK_AIR) { char msg[256]; snprintf(msg, sizeof(msg), menu->game_text.msg_unknown_block, item_buf); add_chat_message(msg); } else { if (inventory_give(player, bt, count)) { char msg[256]; if (count == 1) snprintf(msg, sizeof(msg), "Gave 1 %s.", item_buf); else snprintf(msg, sizeof(msg), "Gave %d %s.", count, item_buf); add_chat_message(msg); } else { add_chat_message("Not enough inventory space to give items."); } } } else { add_chat_message("Usage: /give [count]"); } } else { char msg[512]; snprintf(msg, sizeof(msg), menu->game_text.msg_unknown_command, chat_input); add_chat_message(msg); } } else { char out_msg[1024]; const char* nick = (world->player_nickname[0] != '\0') ? world->player_nickname : "Player"; snprintf(out_msg, sizeof(out_msg), "<%s> %s", nick, chat_input); add_chat_message(out_msg); } // finalize chat state chat_active = false; chat_cursor_pos = 0; history_index = 0; // Recapture mouse if it was captured before chat mouse_captured = true; DisableCursor(); } if (IsKeyPressed(KEY_ESCAPE)) { chat_active = false; chat_cursor_pos = 0; history_index = 0; mouse_captured = true; DisableCursor(); } } else { // Only process game keys when chat is not active // HUD visibility toggle with F1 if (IsKeyPressed(KEY_F1)) { hud_visible = !hud_visible; // Toggle HUD on/off } // HUD mode toggle with F2/F3/F4/F5 if (IsKeyPressed(KEY_F2)) { hud_mode = 0; // default HUD } if (IsKeyPressed(KEY_F3)) { hud_mode = 1; // performance metrics } if (IsKeyPressed(KEY_F4)) { hud_mode = 2; // player } if (IsKeyPressed(KEY_F5)) { hud_mode = 3; // system info // Fetch system info only when entering this mode if (prev_hud_mode != 3) { get_cpu_model(cached_cpu, sizeof(cached_cpu)); get_gpu_model(cached_gpu, sizeof(cached_gpu)); get_kernel_info(cached_kernel, sizeof(cached_kernel)); } } if (IsKeyPressed(KEY_F7)) { show_wireframe = !show_wireframe; // Toggle wireframe rendering } prev_hud_mode = hud_mode; // If inventory is open and ESC pressed, close inventory (don't toggle pause) if (IsKeyPressed(KEY_ESCAPE) && inventory_is_big_open(player)) { inventory_toggle_big(player); // When closing inventory, recapture mouse if gameplay had it captured if (inventory_is_big_open(player)) { // still open -> ensure cursor enabled mouse_captured = false; EnableCursor(); } else { // closed -> restore capture state if (!paused && mouse_captured) DisableCursor(); } } else { // toggle pause with P key or ESC (only if inventory wasn't handled above) if (IsKeyPressed(KEY_P) || IsKeyPressed(KEY_ESCAPE)) { paused = !paused; // Automatically uncapture mouse when pausing, recapture when resuming if (paused) { mouse_captured = false; EnableCursor(); } else if (mouse_captured) { DisableCursor(); } } } // Auto-manage mouse capture: captured only when gameplay (not paused/chat/inventory) { bool should_capture = (!paused && !chat_active && !inventory_is_big_open(player)); if (should_capture && !mouse_captured) { mouse_captured = true; DisableCursor(); } else if (!should_capture && mouse_captured) { mouse_captured = false; EnableCursor(); } } // toggle fullscreen with F11 if (IsKeyPressed(KEY_F11)) { ToggleFullscreen(); } // teleport with R key if (IsKeyPressed(KEY_R)) { player->position = (Vector3){ 8.0f, 105.0f, 8.0f }; player->velocity = (Vector3){ 0, 0, 0 }; } // Chat system - open with T key if (IsKeyPressed(KEY_T) && !paused) { chat_active = true; chat_input_len = 0; chat_input[0] = '\0'; history_index = 0; mouse_captured = false; EnableCursor(); } } // End of else block for non-chat input // Console system - check for commands from terminal ConsoleCommand console_cmd; while (console_get_next_command(&console_cmd)) { // Process console commands (simplified - console can only run certain commands) //printf("[console] Executing: %s\n", console_cmd.raw_input); // Determine target player - for now, always use current player // (In future, could support multiple players) Player* target_player = player; // Process by command type switch (console_cmd.type) { case CMD_QUIT: printf("[console] Quitting...\n"); should_quit = true; break; case CMD_CHAT: add_chat_message(console_cmd.args); break; case CMD_TP: if (target_player) { float x, y, z; if (sscanf(console_cmd.args, "%f %f %f", &x, &y, &z) == 3) { target_player->position = (Vector3){ x, y, z }; target_player->velocity = (Vector3){0,0,0}; printf("[console] Teleported %s to (%.1f, %.1f, %.1f)\n", target_player->nickname, x, y, z); char msg[256]; snprintf(msg, sizeof(msg), "[console] Teleported to (%.1f, %.1f, %.1f)", x, y, z); add_chat_message(msg); } else { printf("[console] Usage: /tp \n"); } } break; case CMD_GIVE: if (target_player && console_cmd.args[0] != '\0') { char item_buf[64] = {0}; int count = 1; sscanf(console_cmd.args, "%63s %d", item_buf, &count); BlockType block_type = BLOCK_AIR; const char* type_str = NULL; if (strcmp(item_buf, "stone") == 0) { block_type = BLOCK_STONE; type_str = "stone"; } else if (strcmp(item_buf, "dirt") == 0) { block_type = BLOCK_DIRT; type_str = "dirt"; } else if (strcmp(item_buf, "grass") == 0) { block_type = BLOCK_GRASS; type_str = "grass"; } else if (strcmp(item_buf, "sand") == 0) { block_type = BLOCK_SAND; type_str = "sand"; } else if (strcmp(item_buf, "wood") == 0) { block_type = BLOCK_WOOD; type_str = "wood"; } else if (strcmp(item_buf, "glowstone") == 0) { block_type = BLOCK_GLOWSTONE; type_str = "glowstone"; } if (type_str && inventory_give(target_player, block_type, count)) { printf("[console] Gave %d of %s to %s\n", count, type_str, target_player->nickname); char msg[256]; snprintf(msg, sizeof(msg), "[console] Gave %d of %s", count, type_str); add_chat_message(msg); } else { printf("[console] Failed to give item: %s\n", item_buf); } } break; case CMD_SELECT: if (target_player && console_cmd.args[0] != '\0') { const char* block_name = console_cmd.args; if (strcmp(block_name, "stone") == 0) { target_player->selected_block = BLOCK_STONE; printf("[console] Selected stone\n"); add_chat_message("[console] Selected stone"); } else if (strcmp(block_name, "dirt") == 0) { target_player->selected_block = BLOCK_DIRT; printf("[console] Selected dirt\n"); add_chat_message("[console] Selected dirt"); } else if (strcmp(block_name, "grass") == 0) { target_player->selected_block = BLOCK_GRASS; printf("[console] Selected grass\n"); add_chat_message("[console] Selected grass"); } else if (strcmp(block_name, "sand") == 0) { target_player->selected_block = BLOCK_SAND; printf("[console] Selected sand\n"); add_chat_message("[console] Selected sand"); } else if (strcmp(block_name, "wood") == 0) { target_player->selected_block = BLOCK_WOOD; printf("[console] Selected wood\n"); add_chat_message("[console] Selected wood"); } else if (strcmp(block_name, "glowstone") == 0) { target_player->selected_block = BLOCK_GLOWSTONE; printf("[console] Selected glowstone\n"); add_chat_message("[console] Selected glowstone"); } else { printf("[console] Unknown block: %s\n", block_name); } } break; case CMD_HELP: printf("[console] Available commands:\n"); printf(" /tp - Teleport to coordinates\n"); printf(" /give [count] - Give items (stone, dirt, grass, sand, wood, glowstone)\n"); printf(" /select - Select block type\n"); printf(" /quit - Quit the game\n"); printf(" /help - Show this help message\n"); break; case CMD_UNKNOWN: printf("[console] Unknown command: %s\n", console_cmd.raw_input); break; default: printf("[console] Command not yet supported from console\n"); break; } } // handle mouse look (must happen before getting forward/right) if (mouse_captured) { Vector2 mouse_delta = GetMouseDelta(); float mouse_speed = 0.005f; // Update pitch (vertical rotation) with clamping float new_pitch = camera_pitch - mouse_delta.y * mouse_speed; // Clamp pitch to very close to ±90 degrees (e.g., 89.9°) float pitch_limit = 1.56905f; // ~89.9 degrees in radians if (new_pitch > pitch_limit) new_pitch = pitch_limit; if (new_pitch < -pitch_limit) new_pitch = -pitch_limit; camera_pitch = new_pitch; // Update yaw (horizontal rotation) camera_yaw -= mouse_delta.x * mouse_speed; // Reconstruct forward vector from angles float sin_pitch = sinf(camera_pitch); float cos_pitch = cosf(camera_pitch); float sin_yaw = sinf(camera_yaw); float cos_yaw = cosf(camera_yaw); Vector3 new_forward = (Vector3){ sin_yaw * cos_pitch, sin_pitch, cos_yaw * cos_pitch }; new_forward = vec3_normalize(new_forward); // Store actual look direction camera_forward = new_forward; // Only update right/up vectors when not at gimbal lock (cos_pitch > 0.01) // When in gimbal lock, compute right directly from yaw to keep it consistent if (fabsf(cos_pitch) > 0.01f) { // Safe to compute from cross product camera_right = vec3_cross(new_forward, (Vector3){0, 1, 0}); camera_right = vec3_normalize(camera_right); camera_up = vec3_cross(camera_right, new_forward); camera_up = vec3_normalize(camera_up); } else { // Gimbal lock: compute right directly from current yaw // right is perpendicular to world up, pointing in the direction of yaw camera_right = (Vector3){cosf(camera_yaw), 0, -sinf(camera_yaw)}; camera_up = (Vector3){0, 1, 0}; // Keep world up } } // Movement should follow the camera's horizontal basis. Project the computed // `camera_right` to the XZ plane and derive a horizontal forward from it. // Movement should always be parallel to the XZ plane, regardless of pitch // Invert right and forward to match standard FPS controls Vector3 right = (Vector3){ -cosf(camera_yaw), 0.0f, sinf(camera_yaw) }; right = vec3_normalize(right); Vector3 forward = (Vector3){ -sinf(camera_yaw), 0.0f, -cosf(camera_yaw) }; forward = vec3_normalize(forward); // Update physics and world always (unless game is paused), even if chat is active if (!paused) { player_update(player, world, dt, flight_enabled); // Pass current render distance (in blocks) so chunk loading scales with it world_update_chunks(world, player->position, camera_forward, menu->render_distance); // Load/unload chunks based on player position and camera direction //clouds_update(clouds, player->position); // Update cloud positions } // Handle player input only if chat is not active and inventory is not open if (!paused && !chat_active && !inventory_is_big_open(player)) { player_move_input(player, forward, right, flight_enabled); // Handle block breaking (left click) if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { int hit_x, hit_y, hit_z; int adj_x, adj_y, adj_z; if (raycast_block(world, camera, 10.0f, &hit_x, &hit_y, &hit_z, &adj_x, &adj_y, &adj_z)) { BlockType broken_block = world_get_block(world, hit_x, hit_y, hit_z); if (broken_block != BLOCK_BEDROCK && broken_block != BLOCK_AIR) { // Add broken block to inventory inventory_add_block(player, broken_block); world_set_block(world, hit_x, hit_y, hit_z, BLOCK_AIR); } } } // Handle block placing (right click) if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { int hit_x, hit_y, hit_z; int adj_x, adj_y, adj_z; if (raycast_block(world, camera, 10.0f, &hit_x, &hit_y, &hit_z, &adj_x, &adj_y, &adj_z)) { // Place block in the adjacent empty space BlockType adjacent_block = world_get_block(world, adj_x, adj_y, adj_z); if (adjacent_block == BLOCK_AIR) { // Check if placing the block would intersect with player hitbox Vector3 block_center = (Vector3){ adj_x + 0.5f, adj_y + 0.5f, adj_z + 0.5f }; // Simple AABB collision check: block (1x1x1) vs player (radius 0.35 cylinder with height 1.9) // Check if block center is within player's horizontal radius float dx = player->position.x - block_center.x; float dz = player->position.z - block_center.z; float horizontal_dist = sqrtf(dx*dx + dz*dz); // Check vertical overlap (player head at position.y, feet at position.y - PLAYER_HEIGHT) float player_bottom = player->position.y - PLAYER_HEIGHT; float player_top = player->position.y; float block_bottom = adj_y; float block_top = adj_y + 1.0f; bool vertical_overlap = (block_bottom < player_top && block_top > player_bottom); bool horizontal_overlap = (horizontal_dist < PLAYER_RADIUS + 0.5f); // 0.5 is half block width // Only place if it doesn't intersect with player if (!(vertical_overlap && horizontal_overlap)) { // Get block type from selected inventory slot only BlockType block_to_place = inventory_get_selected_block(player); // Only place if selected slot has blocks (not BLOCK_AIR) if (block_to_place != BLOCK_AIR && inventory_remove_block(player, block_to_place)) { world_set_block(world, adj_x, adj_y, adj_z, block_to_place); } } } } } // Update highlighted block (raycast every 3 frames for performance) raycast_frame_counter++; if (raycast_frame_counter >= 3) { raycast_frame_counter = 0; int hit_x, hit_y, hit_z; int adj_x, adj_y, adj_z; if (raycast_block(world, camera, 10.0f, &hit_x, &hit_y, &hit_z, &adj_x, &adj_y, &adj_z)) { highlighted_block_x = hit_x; highlighted_block_y = hit_y; highlighted_block_z = hit_z; has_highlighted_block = true; } else { has_highlighted_block = false; } } // Handle inventory slot selection (keys 1-9) if (IsKeyPressed(KEY_ONE)) player->selected_slot = 0; if (IsKeyPressed(KEY_TWO)) player->selected_slot = 1; if (IsKeyPressed(KEY_THREE)) player->selected_slot = 2; if (IsKeyPressed(KEY_FOUR)) player->selected_slot = 3; if (IsKeyPressed(KEY_FIVE)) player->selected_slot = 4; if (IsKeyPressed(KEY_SIX)) player->selected_slot = 5; if (IsKeyPressed(KEY_SEVEN)) player->selected_slot = 6; if (IsKeyPressed(KEY_EIGHT)) player->selected_slot = 7; if (IsKeyPressed(KEY_NINE)) player->selected_slot = 8; // Handle big inventory toggle (I key) if (IsKeyPressed(KEY_I)) { inventory_toggle_big(player); if (inventory_is_big_open(player)) { // Opened inventory: show cursor and stop mouse-look mouse_captured = false; EnableCursor(); } else { // Closed inventory: restore capture if gameplay expects it if (!paused && mouse_captured) DisableCursor(); } } } // update camera to follow player (position it at eye level, slightly above center) float eye_height = 0.7f; camera.position = (Vector3){ player->position.x, player->position.y + eye_height, player->position.z }; // Use the actual look direction for camera target (includes pitch) camera.target = (Vector3){ player->position.x + camera_forward.x, player->position.y + eye_height + camera_forward.y, player->position.z + camera_forward.z }; // (mouse look handled earlier in the loop) BeginDrawing(); ClearBackground(SKYBLUE); // Camera-relative rendering: shift camera to block-aligned grid for float precision // Align to the nearest block boundary to avoid jittering with negative coordinates Vector3 original_camera_pos = camera.position; Vector3 camera_offset = (Vector3){ floorf(camera.position.x), 0, floorf(camera.position.z) }; // Shift camera and target for rendering (only for graphics) camera.position.x -= camera_offset.x; camera.position.y -= camera_offset.y; camera.position.z -= camera_offset.z; camera.target.x -= camera_offset.x; camera.target.y -= camera_offset.y; camera.target.z -= camera_offset.z; // Use shifted camera position for visibility checks Vector3 shifted_cam_pos = camera.position; int blocks_rendered = 0; // Declared here so it's accessible after the if block BeginMode3D(camera); // Use the actual camera orientation for rendering/frustum culling Vector3 cam_forward = camera_forward; Vector3 cam_right = camera_right; // CRITICAL: Take a snapshot of chunk pointers while holding cache_mutex // This prevents chunks from being unloaded during rendering pthread_mutex_lock(&world->cache_mutex); int chunk_count_snapshot = world->chunk_cache.chunk_count; // Allocate temporary array for chunk pointers Chunk** chunks_snapshot = (Chunk**)malloc(chunk_count_snapshot * sizeof(Chunk*)); for (int i = 0; i < chunk_count_snapshot; i++) { chunks_snapshot[i] = &world->chunk_cache.chunks[i]; } pthread_mutex_unlock(&world->cache_mutex); // draw all chunks and their blocks with frustum culling and face culling for (int c = 0; c < chunk_count_snapshot; c++) { Chunk* chunk = chunks_snapshot[c]; // Skip unloaded chunks if (!chunk->loaded) continue; // Skip chunks that haven't been generated yet if (!chunk->generated) continue; // OPTIMIZATION: Iterate only through cached visible blocks instead of all blocks // This is the main performance win - avoids triple-nested loop of 2048 blocks per chunk // Lock chunk while accessing visible_blocks to avoid race with worker thread pthread_mutex_lock(&chunk->mesh_swap_mutex); // Get active mesh buffer (updated by worker thread via double-buffer swap) // Use atomic load with acquire semantics to ensure we see the latest update int active = __atomic_load_n(&chunk->active_mesh, __ATOMIC_ACQUIRE); // Safety checks after locking // Render even if worker is updating - we see consistent data from active buffer // to prevent use-after-free if worker thread updates the other buffer int visible_count = chunk->visible_count[active]; CachedVisibleBlock* visible_blocks_copy = (CachedVisibleBlock*)malloc(visible_count * sizeof(CachedVisibleBlock)); if (!visible_blocks_copy) { pthread_mutex_unlock(&chunk->mesh_swap_mutex); continue; } // CRITICAL: Hold lock during memcpy to prevent another thread from updating // chunk->visible_blocks[active] or visible_count[active] while we're copying // This prevents reading beyond buffer bounds or using freed memory if (chunk->visible_blocks[active] != NULL && visible_count > 0) { memcpy(visible_blocks_copy, chunk->visible_blocks[active], visible_count * sizeof(CachedVisibleBlock)); } else { // Buffer not ready yet, skip rendering this chunk free(visible_blocks_copy); pthread_mutex_unlock(&chunk->mesh_swap_mutex); continue; } pthread_mutex_unlock(&chunk->mesh_swap_mutex); // Render blocks with the copied data (no lock held) // Pre-compute render distance thresholds for LOD float aggressive_lod_dist = menu->render_distance * 0.75f; float aggressive_lod_dist_sq = aggressive_lod_dist * aggressive_lod_dist; float render_dist_sq = menu->render_distance * menu->render_distance; for (int i = 0; i < visible_count; i++) { int x = visible_blocks_copy[i].x; int y = visible_blocks_copy[i].y; int z = visible_blocks_copy[i].z; BlockType block = world_chunk_get_block(chunk, x, y, z); // Calculate world coordinates int world_x = chunk->chunk_x * CHUNK_WIDTH + x; int world_y = chunk->chunk_y * CHUNK_HEIGHT + y; int world_z = chunk->chunk_z * CHUNK_DEPTH + z; Vector3 world_pos = (Vector3){ world_x + 0.5f - camera_offset.x, world_y + 0.5f - camera_offset.y, world_z + 0.5f - camera_offset.z }; // distance-based LOD: use squared distance to avoid sqrt Vector3 to_block = vec3_sub(world_pos, shifted_cam_pos); float dist_sq = to_block.x*to_block.x + to_block.y*to_block.y + to_block.z*to_block.z; // hard render distance limit if (dist_sq > render_dist_sq) { continue; } // AGGRESSIVE LOD: Skip blocks beyond 75% of render distance // This culls ~60% of blocks while maintaining visual quality // Blocks at render_distance are mostly fog-shrouded anyway if (dist_sq > aggressive_lod_dist_sq) { continue; } // OPTIMIZATION: Skip is_block_visible_fast() - chunk frustum culling is sufficient // OPTIMIZATION: Skip is_block_occluded() - visible_blocks cache already filters these float dist = sqrtf(dist_sq); // Only calc sqrt if we're actually rendering Color color = world_get_block_color(block); // apply fog effect: fade color towards sky blue based on distance float fog_factor = 0.0f; float fog_start = menu->render_distance * 0.6f; // Fog starts at 60% of render distance if (dist > fog_start) { fog_factor = (dist - fog_start) / (menu->render_distance - fog_start); fog_factor = fog_factor > 1.0f ? 1.0f : fog_factor; // Clamp to 0-1 // blend color towards sky blue color.r = (unsigned char)(color.r * (1.0f - fog_factor) + SKYBLUE.r * fog_factor); color.g = (unsigned char)(color.g * (1.0f - fog_factor) + SKYBLUE.g * fog_factor); color.b = (unsigned char)(color.b * (1.0f - fog_factor) + SKYBLUE.b * fog_factor); } // apply fog to wireframe too Color wire_color = DARKGRAY; if (fog_factor > 0.0f) { wire_color.r = (unsigned char)(wire_color.r * (1.0f - fog_factor) + SKYBLUE.r * fog_factor); wire_color.g = (unsigned char)(wire_color.g * (1.0f - fog_factor) + SKYBLUE.g * fog_factor); wire_color.b = (unsigned char)(wire_color.b * (1.0f - fog_factor) + SKYBLUE.b * fog_factor); wire_color.a = (unsigned char)(255 * (1.0f - fog_factor)); // Fade out alpha too } // draw only visible faces draw_cube_faces(world_pos, 1.0f, color, camera.position, wire_color, world, world_x, world_y, world_z, block, visible_blocks_copy[i].exposed_faces, visible_blocks_copy[i].face_light, show_wireframe); blocks_rendered++; } // Free the temporary copy free(visible_blocks_copy); } // Draw highlighting box around the block being looked at if (has_highlighted_block) { Vector3 block_pos = (Vector3){ highlighted_block_x + 0.5f - camera_offset.x, highlighted_block_y + 0.5f - camera_offset.y, highlighted_block_z + 0.5f - camera_offset.z }; DrawCubeWires(block_pos, 1.0f, 1.0f, 1.0f, YELLOW); } DrawGrid(30, 1.0f); if (show_chunk_borders) { Color border_color = (Color){255, 255, 0, 150}; Color fill_color = (Color){255, 255, 0, 45}; int player_chunk_x = (int)floorf(original_camera_pos.x / CHUNK_WIDTH); int player_chunk_z = (int)floorf(original_camera_pos.z / CHUNK_DEPTH); for (int c = 0; c < chunk_count_snapshot; c++) { Chunk* chunk = chunks_snapshot[c]; if (!chunk->loaded || !chunk->generated) continue; if (chunk->chunk_x < player_chunk_x - 1 || chunk->chunk_x > player_chunk_x + 1) continue; if (chunk->chunk_z < player_chunk_z - 1 || chunk->chunk_z > player_chunk_z + 1) continue; Vector3 chunk_min = (Vector3){ chunk->chunk_x * CHUNK_WIDTH - camera_offset.x, chunk->chunk_y * CHUNK_HEIGHT - camera_offset.y, chunk->chunk_z * CHUNK_DEPTH - camera_offset.z }; Vector3 chunk_center = (Vector3){ chunk_min.x + CHUNK_WIDTH * 0.5f, chunk_min.y + CHUNK_HEIGHT * 0.5f, chunk_min.z + CHUNK_DEPTH * 0.5f }; // Draw translucent faces to show full chunk bounds DrawCube(chunk_center, (float)CHUNK_WIDTH, (float)CHUNK_HEIGHT, (float)CHUNK_DEPTH, fill_color); DrawCubeWires(chunk_center, (float)CHUNK_WIDTH, (float)CHUNK_HEIGHT, (float)CHUNK_DEPTH, border_color); } } #if 0 // Draw clouds (sync with menu settings) if (clouds) { clouds->enabled = menu->clouds_enabled; clouds->render_distance = menu->clouds_render_distance; } clouds_draw(clouds, camera.position, camera_offset); #endif EndMode3D(); // Free the chunk snapshot free(chunks_snapshot); // Restore original camera position camera.position = original_camera_pos; camera.target.x += camera_offset.x; camera.target.y += camera_offset.y; camera.target.z += camera_offset.z; // Draw crosshair at center of screen int center_x = GetScreenWidth() / 2; int center_y = GetScreenHeight() / 2; int crosshair_size = 10; int crosshair_thickness = 2; // Draw crosshair - two perpendicular lines DrawLineEx((Vector2){center_x - crosshair_size, center_y}, (Vector2){center_x + crosshair_size, center_y}, crosshair_thickness, BLACK); DrawLineEx((Vector2){center_x, center_y - crosshair_size}, (Vector2){center_x, center_y + crosshair_size}, crosshair_thickness, BLACK); // Draw inventory UI at bottom of screen { int slot_size = 50; int slot_spacing = 10; int inv_width = INVENTORY_SIZE * slot_size + (INVENTORY_SIZE - 1) * slot_spacing; int inv_x = (GetScreenWidth() - inv_width) / 2; int inv_y = GetScreenHeight() - slot_size - 30; // Draw inventory background DrawRectangle(inv_x - 5, inv_y - 5, inv_width + 10, slot_size + 10, (Color){0, 0, 0, 150}); // Draw each inventory slot for (int i = 0; i < INVENTORY_SIZE; i++) { int x = inv_x + i * (slot_size + slot_spacing); int y = inv_y; // Slot background Color slot_bg = (Color){80, 80, 80, 200}; if (i == player->selected_slot) { slot_bg = (Color){100, 100, 100, 255}; // Highlight selected slot } DrawRectangle(x, y, slot_size, slot_size, slot_bg); // Draw slot border Color border_color = (i == player->selected_slot) ? (Color){255, 255, 255, 255} : (Color){100, 100, 100, 255}; DrawRectangleLines(x, y, slot_size, slot_size, border_color); // Draw block indicator (colored square representing block type) InventorySlot* slot = &player->inventory[i]; if (slot->count > 0) { // Map block type to color Color block_color; switch (slot->type) { case BLOCK_STONE: block_color = (Color){128, 128, 128, 255}; break; case BLOCK_DIRT: block_color = (Color){139, 69, 19, 255}; break; case BLOCK_GRASS: block_color = (Color){34, 139, 34, 255}; break; case BLOCK_SAND: block_color = (Color){194, 178, 128, 255}; break; case BLOCK_WOOD: block_color = (Color){139, 90, 43, 255}; break; case BLOCK_BEDROCK: block_color = (Color){64, 64, 64, 255}; break; case BLOCK_GLOWSTONE: block_color = (Color){255, 255, 0, 255}; break; default: block_color = (Color){200, 200, 200, 255}; break; } // Draw colored block indicator (smaller than slot) int block_indent = 10; DrawRectangle(x + block_indent, y + block_indent, slot_size - 2*block_indent, slot_size - 2*block_indent, block_color); // Draw item count char count_str[8]; snprintf(count_str, sizeof(count_str), "%d", slot->count); int text_x = x + slot_size - MeasureTextEx(custom_font, count_str, 20, 1).x - 5; int text_y = y + slot_size - 20; DrawTextExCustom(custom_font, count_str, (Vector2){text_x, text_y}, 20, 1, WHITE); } // Draw slot number char slot_num[4]; snprintf(slot_num, sizeof(slot_num), "%d", i + 1); DrawTextExCustom(custom_font, slot_num, (Vector2){x + 3, y + 3}, 16, 1, (Color){150, 150, 150, 255}); } } // Draw big inventory UI when open if (inventory_is_big_open(player)) { int slot_size = 40; int slot_spacing = 4; int inv_width = BIG_INVENTORY_COLS * slot_size + (BIG_INVENTORY_COLS - 1) * slot_spacing; int inv_height = (BIG_INVENTORY_ROWS + 1) * slot_size + (BIG_INVENTORY_ROWS) * slot_spacing; int inv_x = (GetScreenWidth() - inv_width) / 2; int inv_y = (GetScreenHeight() - inv_height) / 2; // Draw background panel DrawRectangle(inv_x - 10, inv_y - 10, inv_width + 20, inv_height + 20, (Color){50, 50, 50, 220}); DrawRectangleLines(inv_x - 10, inv_y - 10, inv_width + 20, inv_height + 20, (Color){100, 100, 100, 255}); // Draw title DrawTextExCustom(custom_font, "Inventory", (Vector2){inv_x, inv_y - 35}, 24, 1, WHITE); // Draw big inventory slots (4 rows x 9 columns) for (int row = 0; row < BIG_INVENTORY_ROWS; row++) { for (int col = 0; col < BIG_INVENTORY_COLS; col++) { int idx = row * BIG_INVENTORY_COLS + col; int x = inv_x + col * (slot_size + slot_spacing); int y = inv_y + row * (slot_size + slot_spacing); // Slot background DrawRectangle(x, y, slot_size, slot_size, (Color){80, 80, 80, 200}); DrawRectangleLines(x, y, slot_size, slot_size, (Color){100, 100, 100, 255}); // Draw block indicator InventorySlot* slot = &player->big_inventory[idx]; if (slot->count > 0) { Color block_color; switch (slot->type) { case BLOCK_STONE: block_color = (Color){128, 128, 128, 255}; break; case BLOCK_DIRT: block_color = (Color){139, 69, 19, 255}; break; case BLOCK_GRASS: block_color = (Color){34, 139, 34, 255}; break; case BLOCK_SAND: block_color = (Color){194, 178, 128, 255}; break; case BLOCK_WOOD: block_color = (Color){139, 90, 43, 255}; break; case BLOCK_BEDROCK: block_color = (Color){64, 64, 64, 255}; break; case BLOCK_GLOWSTONE: block_color = (Color){255, 255, 0, 255}; break; default: block_color = (Color){200, 200, 200, 255}; break; } int block_indent = 8; DrawRectangle(x + block_indent, y + block_indent, slot_size - 2*block_indent, slot_size - 2*block_indent, block_color); // Draw item count char count_str[8]; snprintf(count_str, sizeof(count_str), "%d", slot->count); int text_x = x + slot_size - MeasureTextEx(custom_font, count_str, 16, 1).x - 3; int text_y = y + slot_size - 16; DrawTextExCustom(custom_font, count_str, (Vector2){text_x, text_y}, 16, 1, WHITE); } // Handle mouse interaction for big inventory Vector2 mouse_pos = GetMousePosition(); Rectangle slot_rect = (Rectangle){ (float)x, (float)y, (float)slot_size, (float)slot_size }; if (CheckCollisionPointRec(mouse_pos, slot_rect)) { if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { // Click handling: pick up or place if (!player->holding_item) { // pick up entire stack if (slot->count > 0) { player->held_slot = *slot; player->holding_item = true; slot->type = BLOCK_AIR; slot->count = 0; } } else { // place or combine if (slot->count == 0) { // place held into this slot *slot = player->held_slot; player->held_slot.type = BLOCK_AIR; player->held_slot.count = 0; player->holding_item = false; } else if (slot->type == player->held_slot.type) { // merge as much as possible int can_add = INVENTORY_MAX_STACK - slot->count; int moved = (player->held_slot.count <= can_add) ? player->held_slot.count : can_add; slot->count += moved; player->held_slot.count -= moved; if (player->held_slot.count <= 0) { player->held_slot.type = BLOCK_AIR; player->holding_item = false; } } else { // swap InventorySlot tmp = *slot; *slot = player->held_slot; player->held_slot = tmp; } } } } } } // Draw hotbar at bottom of big inventory int hotbar_y = inv_y + (BIG_INVENTORY_ROWS + 1) * (slot_size + slot_spacing) + 20; for (int i = 0; i < INVENTORY_SIZE; i++) { int x = inv_x + i * (slot_size + slot_spacing); int y = hotbar_y; // Slot background Color slot_bg = (Color){80, 80, 80, 200}; if (i == player->selected_slot) { slot_bg = (Color){100, 100, 100, 255}; } DrawRectangle(x, y, slot_size, slot_size, slot_bg); // Draw slot border Color border_color = (i == player->selected_slot) ? (Color){255, 255, 255, 255} : (Color){100, 100, 100, 255}; DrawRectangleLines(x, y, slot_size, slot_size, border_color); // Draw block indicator InventorySlot* slot = &player->inventory[i]; if (slot->count > 0) { Color block_color; switch (slot->type) { case BLOCK_STONE: block_color = (Color){128, 128, 128, 255}; break; case BLOCK_DIRT: block_color = (Color){139, 69, 19, 255}; break; case BLOCK_GRASS: block_color = (Color){34, 139, 34, 255}; break; case BLOCK_SAND: block_color = (Color){194, 178, 128, 255}; break; case BLOCK_WOOD: block_color = (Color){139, 90, 43, 255}; break; case BLOCK_BEDROCK: block_color = (Color){64, 64, 64, 255}; break; case BLOCK_GLOWSTONE: block_color = (Color){255, 255, 0, 255}; break; default: block_color = (Color){200, 200, 200, 255}; break; } int block_indent = 8; DrawRectangle(x + block_indent, y + block_indent, slot_size - 2*block_indent, slot_size - 2*block_indent, block_color); // Draw item count char count_str[8]; snprintf(count_str, sizeof(count_str), "%d", slot->count); int text_x = x + slot_size - MeasureTextEx(custom_font, count_str, 16, 1).x - 3; int text_y = y + slot_size - 16; DrawTextExCustom(custom_font, count_str, (Vector2){text_x, text_y}, 16, 1, WHITE); } // Draw slot number char slot_num[4]; snprintf(slot_num, sizeof(slot_num), "%d", i + 1); DrawTextExCustom(custom_font, slot_num, (Vector2){x + 3, y + 3}, 14, 1, (Color){150, 150, 150, 255}); } // Handle hotbar mouse interaction { Vector2 mouse_pos = GetMousePosition(); for (int i = 0; i < INVENTORY_SIZE; i++) { int x = inv_x + i * (slot_size + slot_spacing); int y = hotbar_y; Rectangle slot_rect = (Rectangle){ (float)x, (float)y, (float)slot_size, (float)slot_size }; InventorySlot* slot = &player->inventory[i]; if (CheckCollisionPointRec(mouse_pos, slot_rect) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { if (!player->holding_item) { if (slot->count > 0) { player->held_slot = *slot; player->holding_item = true; slot->type = BLOCK_AIR; slot->count = 0; } } else { if (slot->count == 0) { *slot = player->held_slot; player->held_slot.type = BLOCK_AIR; player->held_slot.count = 0; player->holding_item = false; } else if (slot->type == player->held_slot.type) { int can_add = INVENTORY_MAX_STACK - slot->count; int moved = (player->held_slot.count <= can_add) ? player->held_slot.count : can_add; slot->count += moved; player->held_slot.count -= moved; if (player->held_slot.count <= 0) { player->held_slot.type = BLOCK_AIR; player->holding_item = false; } } else { InventorySlot tmp = *slot; *slot = player->held_slot; player->held_slot = tmp; } } } } } // Draw help text // localized close help DrawTextExCustom(custom_font, menu->game_text.inventory_close, (Vector2){inv_x, hotbar_y + slot_size + 10}, 16, 1, (Color){150, 150, 150, 255}); } // draw HUD based on mode if (hud_visible && hud_mode == 0) { // default HUD DrawTextExCustom(custom_font, menu->game_text.move_controls, (Vector2){10, 10}, 32, 1, BLACK); DrawTextExCustom(custom_font, menu->game_text.metrics_help, (Vector2){10, 50}, 32, 1, BLACK); DrawTextExCustom(custom_font, menu->game_text.mouse_help, (Vector2){10, 90}, 32, 1, BLACK); DrawTextExCustom(custom_font, menu->game_text.look_help, (Vector2){10, 130}, 32, 1, BLACK); DrawTextExCustom(custom_font, menu->game_text.pause_help, (Vector2){10, 170}, 32, 1, BLACK); char coord_text[128]; snprintf(coord_text, sizeof(coord_text), "%s (%.1f, %.1f, %.1f)", menu->game_text.coord_label, player->position.x, player->position.y, player->position.z); DrawTextExCustom(custom_font, coord_text, (Vector2){10, 210}, 32, 1, BLACK); char fps_text[64]; snprintf(fps_text, sizeof(fps_text), "%s %d", menu->game_text.fps_label, GetFPS()); DrawTextExCustom(custom_font, fps_text, (Vector2){10, 250}, 32, 1, BLACK); DrawTextExCustom(custom_font, menu->game_text.version, (Vector2){10, 290}, 32, 1, DARKGRAY); } else if (hud_visible && hud_mode == 1) { // performance metrics HUD DrawTextExCustom(custom_font, menu->game_text.perf_metrics, (Vector2){10, 10}, 32, 1, BLACK); char frame_time[64]; snprintf(frame_time, sizeof(frame_time), "Frame Time: %.2f ms", dt * 1000.0f); DrawTextExCustom(custom_font, frame_time, (Vector2){10, 50}, 32, 1, BLACK); char fps_text[32]; snprintf(fps_text, sizeof(fps_text), "%s %d", menu->game_text.fps_label, GetFPS()); DrawTextExCustom(custom_font, fps_text, (Vector2){10, 90}, 32, 1, BLACK); char blocks_text[64]; snprintf(blocks_text, sizeof(blocks_text), "Blocks Rendered: %d", blocks_rendered); DrawTextExCustom(custom_font, blocks_text, (Vector2){10, 130}, 32, 1, BLACK); int memory_mb = get_process_memory_mb(); char memory_text[64]; snprintf(memory_text, sizeof(memory_text), "Memory Usage: %d MB", memory_mb); DrawTextExCustom(custom_font, memory_text, (Vector2){10, 170}, 32, 1, BLACK); char pos_text[64]; snprintf(pos_text, sizeof(pos_text), "Pos: (%.1f, %.1f, %.1f)", player->position.x, player->position.y, player->position.z); DrawTextExCustom(custom_font, pos_text, (Vector2){10, 210}, 32, 1, BLACK); DrawTextExCustom(custom_font, "b3dv 0.0.20-beta", (Vector2){10, 250}, 32, 1, DARKGRAY); } else if (hud_visible && hud_mode == 2) { // player stats HUD DrawTextExCustom(custom_font, "=== PLAYER STATS ===", (Vector2){10, 10}, 32, 1, BLACK); char fps_text[32]; snprintf(fps_text, sizeof(fps_text), "FPS: %d", GetFPS()); DrawTextExCustom(custom_font, fps_text, (Vector2){10, 50}, 32, 1, BLACK); char pos_text[64]; snprintf(pos_text, sizeof(pos_text), "Pos: (%.1f, %.1f, %.1f)", player->position.x, player->position.y, player->position.z); DrawTextExCustom(custom_font, pos_text, (Vector2){10, 90}, 32, 1, BLACK); // calculate speed (magnitude of velocity) // Use actual movement for speedometer float dx = player->position.x - player->prev_position.x; float dy = player->position.y - player->prev_position.y; float dz = player->position.z - player->prev_position.z; float actual_speed = sqrtf(dx*dx + dy*dy + dz*dz) / dt; char speed_text[64]; snprintf(speed_text, sizeof(speed_text), "Speed: %.2f m/s", actual_speed); DrawTextExCustom(custom_font, speed_text, (Vector2){10, 130}, 32, 1, BLACK); // momentum/velocity components char momentum_text[96]; snprintf(momentum_text, sizeof(momentum_text), "Vel: (%.2f, %.2f, %.2f) m/s", player->velocity.x, player->velocity.y, player->velocity.z); DrawTextExCustom(custom_font, momentum_text, (Vector2){10, 170}, 32, 1, BLACK); DrawTextExCustom(custom_font, "b3dv 0.0.20-beta", (Vector2){10, 250}, 32, 1, DARKGRAY); } else if (hud_visible && hud_mode == 3) { // system info HUD (using cached values) DrawTextExCustom(custom_font, "=== SYSTEM INFO ===", (Vector2){10, 10}, 32, 1, BLACK); DrawTextExCustom(custom_font, cached_cpu, (Vector2){10, 50}, 32, 1, BLACK); DrawTextExCustom(custom_font, cached_gpu, (Vector2){10, 90}, 32, 1, BLACK); DrawTextExCustom(custom_font, cached_kernel, (Vector2){10, 130}, 32, 1, BLACK); DrawTextExCustom(custom_font, "b3dv 0.0.20-beta", (Vector2){10, 250}, 32, 1, DARKGRAY); } if (hud_visible && menu->compass_enabled) { draw_compass_hud(custom_font, camera_yaw); } // Draw chat message history (last few messages with fade-out) double current_time = GetTime(); double message_lifetime = 5.0; // Messages stay for 5 seconds int screen_height = GetScreenHeight(); int message_start_y = screen_height - 200; // Start drawing messages from bottom-left int messages_shown = 0; for (int i = 0; i < CHAT_MESSAGE_BUFFER_SIZE && messages_shown < 5; i++) { // Find the most recent messages (iterate backwards) int msg_index = (chat_message_count - 1 - i) % CHAT_MESSAGE_BUFFER_SIZE; if (i >= chat_message_count) break; // Don't show messages that haven't been set yet double message_age = current_time - chat_message_times[msg_index]; if (message_age < message_lifetime && strlen(chat_messages[msg_index]) > 0) { // Calculate fade: full opacity for first 4 seconds, fade out in last 1 second float fade_factor = 1.0f; if (message_age > 4.0) { fade_factor = 1.0f - (message_age - 4.0f); // Fade over last 1 second fade_factor = fade_factor < 0 ? 0 : fade_factor; } Color msg_color = (Color){255, 255, 255, (unsigned char)(255 * fade_factor)}; int y_offset = message_start_y - (messages_shown * 35); DrawTextExCustom(custom_font, chat_messages[msg_index], (Vector2){10, y_offset}, 28, 1, msg_color); messages_shown++; } } // display pause menu with buttons if paused if (paused) { if (pause_settings_open) { // Draw settings panel in pause menu int screen_width = GetScreenWidth(); int screen_height = GetScreenHeight(); // Clear background DrawRectangle(0, 0, screen_width, screen_height, (Color){0, 0, 0, 150}); // Draw title Vector2 title_size = MeasureTextEx(custom_font, menu->game_text.settings, 64, 2); DrawTextExCustom(custom_font, menu->game_text.settings, (Vector2){(screen_width - title_size.x) / 2, 40}, 64, 2, WHITE); // Settings panel int panel_width = 700; int panel_height = 700; int panel_x = (screen_width - panel_width) / 2; int panel_y = 30; DrawRectangle(panel_x - 10, panel_y - 10, panel_width + 20, panel_height + 20, (Color){40, 40, 40, 255}); DrawRectangleLines(panel_x - 10, panel_y - 10, panel_width + 20, panel_height + 20, WHITE); // Render Distance slider int slider_y = panel_y + 30; int slider_x = panel_x + 50; int slider_width = 500; int slider_height = 20; // Draw label DrawTextExCustom(custom_font, menu->game_text.render_dist_label, (Vector2){panel_x + 30, slider_y - 35}, 28, 1, WHITE); // Draw value char render_dist_str[32]; snprintf(render_dist_str, sizeof(render_dist_str), "%.0f", menu->render_distance); DrawTextExCustom(custom_font, render_dist_str, (Vector2){panel_x + 500, slider_y - 35}, 28, 1, GRAY); // Draw slider background DrawRectangle(slider_x, slider_y, slider_width, slider_height, (Color){60, 60, 60, 255}); DrawRectangleLines(slider_x, slider_y, slider_width, slider_height, WHITE); // Calculate slider knob position float render_dist_normalized = (menu->render_distance - 10.0f) / (100.0f - 10.0f); render_dist_normalized = render_dist_normalized < 0 ? 0 : (render_dist_normalized > 1 ? 1 : render_dist_normalized); int knob_x = slider_x + (int)(render_dist_normalized * slider_width); // Draw knob DrawRectangle(knob_x - 6, slider_y - 5, 12, slider_height + 10, LIGHTGRAY); DrawRectangleLines(knob_x - 6, slider_y - 5, 12, slider_height + 10, WHITE); // Handle render distance slider input Vector2 mouse_pos = GetMousePosition(); Rectangle render_slider_rect = {(float)slider_x, (float)(slider_y - 10), (float)slider_width, slider_height + 20}; if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && CheckCollisionPointRec(mouse_pos, render_slider_rect)) { float new_pos = (mouse_pos.x - slider_x) / slider_width; new_pos = new_pos < 0 ? 0 : (new_pos > 1 ? 1 : new_pos); menu->render_distance = 10.0f + (new_pos * (100.0f - 10.0f)); menu_save_settings(menu); } // Max FPS slider int fps_slider_y = slider_y + 100; // Draw label DrawTextExCustom(custom_font, menu->game_text.max_fps_label, (Vector2){panel_x + 30, fps_slider_y - 35}, 28, 1, WHITE); // Draw value char fps_str[32]; if (menu->max_fps == 0) { snprintf(fps_str, sizeof(fps_str), "%s", menu->game_text.uncapped); } else { snprintf(fps_str, sizeof(fps_str), "%d", menu->max_fps); } DrawTextExCustom(custom_font, fps_str, (Vector2){panel_x + 500, fps_slider_y - 35}, 28, 1, GRAY); // Draw slider background DrawRectangle(slider_x, fps_slider_y, slider_width, slider_height, (Color){60, 60, 60, 255}); DrawRectangleLines(slider_x, fps_slider_y, slider_width, slider_height, WHITE); // Calculate FPS slider knob position (30-240, or 0 for uncapped) float fps_normalized; if (menu->max_fps == 0) { fps_normalized = 1.0f; // Show at the right end when uncapped } else { fps_normalized = (menu->max_fps - 30) / (240.0f - 30); fps_normalized = fps_normalized < 0 ? 0 : (fps_normalized > 1 ? 1 : fps_normalized); } int fps_knob_x = slider_x + (int)(fps_normalized * slider_width); // Draw knob DrawRectangle(fps_knob_x - 6, fps_slider_y - 5, 12, slider_height + 10, LIGHTGRAY); DrawRectangleLines(fps_knob_x - 6, fps_slider_y - 5, 12, slider_height + 10, WHITE); // Handle FPS slider input Rectangle fps_slider_rect = {(float)slider_x, (float)(fps_slider_y - 10), (float)slider_width, slider_height + 20}; if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && CheckCollisionPointRec(mouse_pos, fps_slider_rect)) { float new_pos = (mouse_pos.x - slider_x) / slider_width; new_pos = new_pos < 0 ? 0 : (new_pos > 1 ? 1 : new_pos); if (new_pos >= 0.95f) { menu->max_fps = 0; // 0 means uncapped } else { menu->max_fps = (int)(30 + (new_pos * (240 - 30))); if (menu->max_fps < 30) menu->max_fps = 30; } menu_save_settings(menu); } // Nickname input int nickname_y = fps_slider_y + 90; int nickname_box_x = panel_x + 50; int nickname_box_width = 500; int nickname_box_height = 40; Rectangle nickname_box = { (float)nickname_box_x, (float)nickname_y, (float)nickname_box_width, (float)nickname_box_height }; DrawTextExCustom(custom_font, "Nickname:", (Vector2){panel_x + 30, nickname_y - 35}, 24, 1, WHITE); DrawRectangleRec(nickname_box, (Color){60, 60, 60, 255}); DrawRectangleLinesEx(nickname_box, 2, menu->nickname_edit_active ? YELLOW : WHITE); DrawTextExCustom(custom_font, menu->nickname, (Vector2){nickname_box.x + 10, nickname_box.y + 8}, 24, 1, WHITE); if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { if (CheckCollisionPointRec(mouse_pos, nickname_box)) { menu->nickname_edit_active = true; } else { menu->nickname_edit_active = false; } } if (menu->nickname_edit_active) { int key = GetCharPressed(); bool changed = false; while (key > 0) { if ((key >= 32 && key <= 126) && menu->nickname_len < (int)sizeof(menu->nickname) - 1) { menu->nickname[menu->nickname_len++] = (char)key; menu->nickname[menu->nickname_len] = '\0'; changed = true; } key = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE) && menu->nickname_len > 0) { menu->nickname_len--; menu->nickname[menu->nickname_len] = '\0'; changed = true; } if (changed) { menu_save_settings(menu); } } if (menu->nickname_edit_active && (int)(GetTime() * 2) % 2 == 0) { Vector2 cursor_pos = MeasureTextEx(custom_font, menu->nickname, 24, 1); DrawLineEx((Vector2){nickname_box.x + 10 + cursor_pos.x, nickname_box.y + 8}, (Vector2){nickname_box.x + 10 + cursor_pos.x, nickname_box.y + nickname_box_height - 8}, 2, WHITE); } #if 0 // Cloud render distance slider int cloud_dist_y = nickname_y + 90; // Draw label DrawTextExCustom(custom_font, "Cloud Distance:", (Vector2){panel_x + 30, cloud_dist_y - 35}, 24, 1, WHITE); // Draw value char cloud_dist_str[32]; snprintf(cloud_dist_str, sizeof(cloud_dist_str), "%.0f", menu->clouds_render_distance); DrawTextExCustom(custom_font, cloud_dist_str, (Vector2){panel_x + 500, cloud_dist_y - 35}, 24, 1, GRAY); // Draw slider background DrawRectangle(slider_x, cloud_dist_y, slider_width, slider_height, (Color){60, 60, 60, 255}); DrawRectangleLines(slider_x, cloud_dist_y, slider_width, slider_height, WHITE); // Calculate cloud distance slider knob position float cloud_dist_normalized = (menu->clouds_render_distance - 32.0f) / (512.0f - 32.0f); cloud_dist_normalized = cloud_dist_normalized < 0 ? 0 : (cloud_dist_normalized > 1 ? 1 : cloud_dist_normalized); int cloud_knob_x = slider_x + (int)(cloud_dist_normalized * slider_width); // Draw knob DrawRectangle(cloud_knob_x - 6, cloud_dist_y - 5, 12, slider_height + 10, LIGHTGRAY); DrawRectangleLines(cloud_knob_x - 6, cloud_dist_y - 5, 12, slider_height + 10, WHITE); // Handle cloud distance slider input Rectangle cloud_slider_rect = {(float)slider_x, (float)(cloud_dist_y - 10), (float)slider_width, slider_height + 20}; if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && CheckCollisionPointRec(mouse_pos, cloud_slider_rect)) { float new_pos = (mouse_pos.x - slider_x) / slider_width; new_pos = new_pos < 0 ? 0 : (new_pos > 1 ? 1 : new_pos); menu->clouds_render_distance = 32.0f + (new_pos * (512.0f - 32.0f)); menu_save_settings(menu); } // Cloud toggle checkbox int cloud_toggle_y = cloud_dist_y + 60; int checkbox_size = 30; int checkbox_x = panel_x + 50; Rectangle cloud_checkbox = {(float)checkbox_x, (float)cloud_toggle_y, (float)checkbox_size, (float)checkbox_size}; DrawRectangleRec(cloud_checkbox, (Color){60, 60, 60, 255}); DrawRectangleLinesEx(cloud_checkbox, 2, WHITE); if (menu->clouds_enabled) { DrawRectangle(checkbox_x + 5, cloud_toggle_y + 5, checkbox_size - 10, checkbox_size - 10, (Color){100, 200, 100, 255}); } DrawTextExCustom(custom_font, "Clouds Enabled", (Vector2){checkbox_x + checkbox_size + 15, cloud_toggle_y + 3}, 24, 1, WHITE); if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && CheckCollisionPointRec(mouse_pos, cloud_checkbox)) { menu->clouds_enabled = !menu->clouds_enabled; menu_save_settings(menu); } #endif // Back button to return to pause menu int button_width = 450; int button_height = 60; int button_y = nickname_y + 150; // Adjusted position since cloud UI is now disabled Rectangle back_button = { ((float)screen_width - (float)button_width) / 2.0f, (float)button_y, (float)button_width, (float)button_height }; bool back_hover = CheckCollisionPointRec(mouse_pos, back_button); DrawRectangleRec(back_button, back_hover ? LIGHTGRAY : GRAY); DrawRectangleLinesEx(back_button, 2, WHITE); Vector2 back_text_size = MeasureTextEx(custom_font, menu->text_back, 32, 1); DrawTextExCustom(custom_font, menu->text_back, (Vector2){((float)screen_width / 2.0f) - (back_text_size.x / 2.0f), (float)button_y + 12.0f}, 32, 1, BLACK); // Handle back button if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && back_hover) { pause_settings_open = false; } // Also allow ESC to go back if (IsKeyPressed(KEY_ESCAPE)) { pause_settings_open = false; } } else { // Draw main pause menu // get screen dimensions dynamically int screen_width = GetScreenWidth(); int screen_height = GetScreenHeight(); // draw semi-transparent overlay DrawRectangle(0, 0, screen_width, screen_height, (Color){0, 0, 0, 150}); // measure text to center it Vector2 paused_size = MeasureTextEx(custom_font, menu->game_text.paused, 64, 2); // draw title DrawTextExCustom(custom_font, menu->game_text.paused, (Vector2){((float)screen_width - paused_size.x) / 2.0f, (float)screen_height / 2.0f - 120.0f}, 64, 2, /*RED*/WHITE); // button dimensions int button_width = 450; int button_height = 60; int button_spacing = 20; int center_x = screen_width / 2; int center_y = screen_height / 2 - 20; // resume button Rectangle resume_button = { ((float)center_x - (float)button_width / 2.0f), (float)center_y, (float)button_width, (float)button_height }; // settings button Rectangle settings_button = { ((float)center_x - (float)button_width / 2.0f), (float)(center_y + button_height + button_spacing), (float)button_width, (float)button_height }; // quit button Rectangle quit_button = { ((float)center_x - (float)button_width / 2.0f), (float)(center_y + 2 * (button_height + button_spacing)), (float)button_width, (float)button_height }; // get mouse position Vector2 mouse_pos = GetMousePosition(); bool resume_hover = CheckCollisionPointRec(mouse_pos, resume_button); bool settings_hover = CheckCollisionPointRec(mouse_pos, settings_button); bool quit_hover = CheckCollisionPointRec(mouse_pos, quit_button); // draw resume button DrawRectangleRec(resume_button, resume_hover ? LIGHTGRAY : GRAY); DrawRectangleLinesEx(resume_button, 2, WHITE); Vector2 resume_text_size = MeasureTextEx(custom_font, menu->game_text.resume, 32, 1); DrawTextExCustom(custom_font, menu->game_text.resume, (Vector2){center_x - resume_text_size.x / 2, center_y + 12}, 32, 1, BLACK); // draw settings button DrawRectangleRec(settings_button, settings_hover ? LIGHTGRAY : GRAY); DrawRectangleLinesEx(settings_button, 2, WHITE); Vector2 settings_text_size = MeasureTextEx(custom_font, menu->game_text.settings, 32, 1); DrawTextExCustom(custom_font, menu->game_text.settings, (Vector2){center_x - settings_text_size.x / 2, center_y + button_height + button_spacing + 12}, 32, 1, BLACK); // draw quit button DrawRectangleRec(quit_button, quit_hover ? LIGHTGRAY : GRAY); DrawRectangleLinesEx(quit_button, 2, WHITE); Vector2 quit_text_size = MeasureTextEx(custom_font, menu->game_text.back_to_menu, 32, 1); DrawTextExCustom(custom_font, menu->game_text.back_to_menu, (Vector2){center_x - quit_text_size.x / 2, center_y + 2 * (button_height + button_spacing) + 12}, 32, 1, BLACK); // handle button clicks if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { if (resume_hover) { paused = false; // Recapture mouse if it was captured before pause mouse_captured = true; DisableCursor(); } else if (settings_hover) { pause_settings_open = true; } else if (quit_hover) { // Save world and return to main menu if (player) { world->last_player_position = player->position; } world_save(world, world->world_name); paused = false; should_quit = false; // Reset menu state to main menu->current_state = MENU_STATE_MAIN; // Free world and player world_unload_textures(world); world_free(world); player_free(player); //clouds_free(clouds); world = NULL; player = NULL; //clouds = NULL; // Release mouse mouse_captured = false; EnableCursor(); } } } } // display chat input if active if (chat_active) { int screen_width = GetScreenWidth(); int screen_height = GetScreenHeight(); // chat box at bottom int chat_box_height = 50; int chat_box_y = screen_height - chat_box_height - 10; DrawRectangle(10, chat_box_y, screen_width - 20, chat_box_height, (Color){30, 30, 30, 200}); DrawRectangleLinesEx((Rectangle){10, chat_box_y, screen_width - 20, chat_box_height}, 2, WHITE); // draw chat input text with prompt char chat_display[512]; snprintf(chat_display, sizeof(chat_display), "> %s", chat_input); DrawTextExCustom(custom_font, chat_display, (Vector2){20, chat_box_y + 8}, 28, 1, WHITE); // draw blinking cursor at cursor position if ((int)(GetTime() * 2) % 2 == 0) { // measure text up to cursor position (accounting for "> " prefix) char text_before_cursor[256]; strncpy(text_before_cursor, chat_input, chat_cursor_pos); text_before_cursor[chat_cursor_pos] = '\0'; char display_before[512]; snprintf(display_before, sizeof(display_before), "> %s", text_before_cursor); Vector2 cursor_pos = MeasureTextEx(custom_font, display_before, 28, 1); DrawLineEx((Vector2){20 + cursor_pos.x, chat_box_y + 8}, (Vector2){20 + cursor_pos.x, chat_box_y + 38}, 2, WHITE); } } EndDrawing(); } // Save world before closing (if one was loaded) if (world && player) { world->last_player_position = player->position; world_save(world, world->world_name); } // Save settings before closing menu_save_settings(menu); // Clean up menu system menu_system_free(menu); UnloadFont(custom_font); if (player) player_free(player); //if (clouds) clouds_free(clouds); if (world) { world_unload_textures(world); // Unload textures before closing world_free(world); } if (sdf_shader.id != 0) UnloadShader(sdf_shader); // Shutdown console input system console_shutdown(); CloseWindow(); return 0; }