#include "network.h" #include "raylib.h" #include "raymath.h" #include "rcamera.h" #include #include #include #include #include #include #include #include #include #include #include #include "rlgl.h" #include #define CHUNK_SIZE 32 #ifndef PI #define PI 3.1415926535f #endif #define CHUNK_HEIGHT 128 #define RENDER_DISTANCE 8 enum BlockType { AIR = 0, GRASS = 1, DIRT = 2, COBBLESTONE = 3, LOG = 4, LEAVES = 5, PLANK = 6, STONE = 7, BEDROCK = 8, DIAMOND_ORE = 9, IRON_ORE = 10, GRAVEL = 11, CRAFTING_TABLE = 12, SAND = 13, STICK = 14, WOOD_AXE = 15, WOOD_PICKAXE = 16, WOOD_SWORD = 17, WOOD_SHOVEL = 18, WOOD_HOE = 19, STONE_AXE = 20, STONE_PICKAXE = 21, STONE_SWORD = 22, STONE_SHOVEL = 23, STONE_HOE = 24, FURNACE = 25, CHEST = 26, LADDER = 27, FENCE = 28, TORCH = 29, DOOR = 30, STONE_SLAB = 31, IRON_AXE = 32, IRON_PICKAXE = 33, IRON_SWORD = 34, IRON_SHOVEL = 35, IRON_HOE = 36, DIAMOND_AXE = 37, DIAMOND_PICKAXE = 38, DIAMOND_SWORD = 39, DIAMOND_SHOVEL = 40, DIAMOND_HOE = 41, APPLE = 42 }; std::string GetBlockName(int type) { switch (type) { case AIR: return ""; case GRASS: return "Grass Block"; case DIRT: return "Dirt"; case COBBLESTONE: return "Cobblestone"; case LOG: return "Oak Log"; case LEAVES: return "Leaves"; case PLANK: return "Oak Planks"; case STONE: return "Stone"; case BEDROCK: return "Bedrock"; case DIAMOND_ORE: return "Diamond Ore"; case IRON_ORE: return "Iron Ore"; case GRAVEL: return "Gravel"; case CRAFTING_TABLE: return "Crafting Table"; case SAND: return "Sand"; case STICK: return "Stick"; case WOOD_AXE: return "Wooden Axe"; case WOOD_PICKAXE: return "Wooden Pickaxe"; case WOOD_SWORD: return "Wooden Sword"; case WOOD_SHOVEL: return "Wooden Shovel"; case WOOD_HOE: return "Wooden Hoe"; case STONE_AXE: return "Stone Axe"; case STONE_PICKAXE: return "Stone Pickaxe"; case STONE_SWORD: return "Stone Sword"; case STONE_SHOVEL: return "Stone Shovel"; case STONE_HOE: return "Stone Hoe"; case FURNACE: return "Furnace"; case CHEST: return "Chest"; case LADDER: return "Ladder"; case FENCE: return "Fence"; case TORCH: return "Torch"; case DOOR: return "Wooden Door"; case STONE_SLAB: return "Stone Slab"; case IRON_AXE: return "Iron Axe"; case IRON_PICKAXE: return "Iron Pickaxe"; case IRON_SWORD: return "Iron Sword"; case IRON_SHOVEL: return "Iron Shovel"; case IRON_HOE: return "Iron Hoe"; case DIAMOND_AXE: return "Diamond Axe"; case DIAMOND_PICKAXE: return "Diamond Pickaxe"; case DIAMOND_SWORD: return "Diamond Sword"; case DIAMOND_SHOVEL: return "Diamond Shovel"; case DIAMOND_HOE: return "Diamond Hoe"; case APPLE: return "Apple"; default: return "Unknown"; } } int GetMaxDurability(int type) { if (type >= WOOD_AXE && type <= WOOD_HOE) return 60; if (type >= STONE_AXE && type <= STONE_HOE) return 132; if (type >= IRON_AXE && type <= IRON_HOE) return 251; if (type >= DIAMOND_AXE && type <= DIAMOND_HOE) return 1561; return 0; } // Simple 2D Perlin Noise implementation float dotGridGradient(int ix, int iy, float x, float y, unsigned int seed) { // Proper hash with avalanche effect - seed fundamentally changes the terrain unsigned int hash = seed; hash ^= (unsigned int)ix * 374761393u; hash = (hash << 17) | (hash >> 15); hash *= 1103515245u; hash ^= (unsigned int)iy * 668265263u; hash = (hash << 13) | (hash >> 19); hash *= 2654435761u; hash ^= hash >> 16; hash *= 2246822519u; hash ^= hash >> 13; float random = hash * (3.14159265f / ~(~0u >> 1)); float gradientX = cosf(random); float gradientY = sinf(random); float dx = x - (float)ix; float dy = y - (float)iy; return (dx*gradientX + dy*gradientY); } float interpolate(float a0, float a1, float w) { return (a1 - a0) * (3.0f - w * 2.0f) * w * w; } float perlin(float x, float y, unsigned int seed) { int x0 = (int)floorf(x); int x1 = x0 + 1; int y0 = (int)floorf(y); int y1 = y0 + 1; float sx = x - (float)x0; float sy = y - (float)y0; float n0 = dotGridGradient(x0, y0, x, y, seed); float n1 = dotGridGradient(x1, y0, x, y, seed); float ix0 = interpolate(n0, n1, sx); n0 = dotGridGradient(x0, y1, x, y, seed); n1 = dotGridGradient(x1, y1, x, y, seed); float ix1 = interpolate(n0, n1, sx); return interpolate(ix0, ix1, sy); // returns approximately -1.0 to 1.0 } struct ChunkPos { int x, z; bool operator==(const ChunkPos& other) const { return x == other.x && z == other.z; } }; struct ChunkPosHash { std::size_t operator()(const ChunkPos& k) const { return ((std::hash()(k.x) ^ (std::hash()(k.z) << 1)) >> 1); } }; struct BlockRenderData { float x, y, z; unsigned char faces; // 1:front, 2:back, 4:top, 8:bottom, 16:right, 32:left }; struct DroppedItem { Vector3 pos; int type; bool active; }; static std::vector droppedItems; struct Chunk { int blocks[CHUNK_SIZE][CHUNK_HEIGHT][CHUNK_SIZE]; int maxY = 0; bool generated = false; bool modified = false; bool dirty = true; std::vector renderLists[64]; Chunk() : generated(false), modified(false), maxY(0), dirty(true) {} }; // Global variables for persistence std::unordered_map worldChunks; static unsigned int globalSeedHash = 0; static std::string currentWorldName = ""; static std::string playerName = "Player"; static std::string hoveredItemName = ""; static bool serverMode = false; static float masterMusicVolume = 1.0f; static float masterSoundVolume = 1.0f; static Color myShirtColor = BLUE; static Color myPantsColor = DARKBLUE; static float playerHealth = 16.0f; static float playerHunger = 20.0f; static float hungerTimer = 0.0f; static float healthRegenTimer = 0.0f; static float healthStarveTimer = 0.0f; static float damageFlashTimer = 0.0f; static uint32_t localPlayerID = 0; static Sound hitSound; enum MenuState { MAIN_MENU, OPTIONS_MENU, CREATE_WORLD_MENU, LOAD_WORLD_MENU, GAMEPLAY, PAUSE_MENU, CRAFTING_GUI, CHECKING_UPDATES, UPDATE_NOTES, UPDATE_FOUND, DOWNLOADING_UPDATE, EXTRACTING_UPDATE, CONNECT_MENU, SKIN_EDITOR, WORLD_CREATION_PROGRESS, CHEST_GUI, CHEAT_GUI }; // Forward Declarations unsigned char GetExposedFaces(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP); void RebuildChunkRenderList(Chunk* chunk, int cx, int cz); void GenerateChunk(int cx, int cz); void SetBlock(int x, int y, int z, int type); void SaveConfig(); void LoadConfig(); std::string GetRemoteVersion(); struct RemotePlayer { Socket sock; // Server only uint32_t id; std::string name; Vector3 position; float yaw; Color shirtColor; Color pantsColor; }; static std::vector remotePlayers; struct ChatMessage { std::string text; float timer; }; static std::vector chatLog; // Global Networking State static Socket clientSocket = INVALID_SOCKET_VAL; static Socket serverSocket = INVALID_SOCKET_VAL; static std::vector clientSockets; static bool isConnecting = false; // ---- Inventory System ---- struct InventorySlot { int blockType = AIR; int count = 0; int durability = 0; int maxDurability = 0; InventorySlot() : blockType(AIR), count(0), durability(0), maxDurability(0) {} InventorySlot(int bt, int c) : blockType(bt), count(c) { maxDurability = GetMaxDurability(bt); durability = maxDurability; } }; static InventorySlot hotbar[9]; // 9 hotbar slots static InventorySlot inventory[27]; // 3x9 main inventory static int activeHotbarSlot = 0; static bool inventoryOpen = false; static bool isFlying = false; static float worldGenProgress = 0.0f; static int chunksGeneratedCount = 0; static const int totalChunksToPreGen = 625; // 25x25 area around spawn static bool isNewWorldGeneration = false; static float spawnSavedX = 0, spawnSavedY = 0, spawnSavedZ = 0; static int loadWorldScrollOffset = 0; static std::map> chestInventories; static Vector3 activeChestPos = {0}; static int cheatScrollOffset = 0; static std::vector torchPositions; static float dayFactor = 1.0f; static bool ignoreUpdateThisSession = false; float GetBlockLight(Vector3 pos) { float bL = 0.0f; for (const auto& tp : torchPositions) { float d = Vector3Distance(pos, tp); if (d < 15.0f) { float intensity = 1.0f - (d / 15.0f); if (intensity > bL) bL = intensity; } } return fmaxf(dayFactor, bL); } uint64_t GetPosKey(int x, int y, int z) { return ((uint64_t)(x + 1000000) << 40) | ((uint64_t)(y + 10000) << 20) | (uint64_t)(z + 1000000); } // Adds one block to inventory: first fills existing stacks, then empty slots. void AddToInventory(int blockType) { // Search hotbar first for (int i = 0; i < 9; i++) { if (hotbar[i].blockType == blockType && hotbar[i].count < 64) { hotbar[i].count++; return; } } // Then main inventory for (int i = 0; i < 27; i++) { if (inventory[i].blockType == blockType && inventory[i].count < 64) { inventory[i].count++; return; } } // Empty hotbar slot for (int i = 0; i < 9; i++) { if (hotbar[i].count == 0) { hotbar[i].blockType = blockType; hotbar[i].count = 1; return; } } // Empty main inventory slot for (int i = 0; i < 27; i++) { if (inventory[i].count == 0) { inventory[i].blockType = blockType; inventory[i].count = 1; return; } } // Inventory full — item is lost for now } // Helper to give multiple items void GiveItems(int type, int count) { for (int i = 0; i < count; i++) AddToInventory(type); } // Camera look angles — global so world load/create can reset them cleanly static float camYaw = 0.0f; static float camPitch = 0.0f; bool LoadChunk(Chunk* chunk, int cx, int cz) { if (currentWorldName.empty()) return false; std::string path = "saves/" + currentWorldName + "/chunk_" + std::to_string(cx) + "_" + std::to_string(cz) + ".dat"; std::ifstream file(path, std::ios::binary); if (file.is_open()) { file.read((char*)chunk->blocks, sizeof(chunk->blocks)); chunk->generated = true; chunk->modified = false; // Calculate maxY for the loaded chunk chunk->maxY = 0; for (int x=0; x=0; y--) if (chunk->blocks[x][y][z] != AIR) { if (y > chunk->maxY) chunk->maxY = y; break; } return true; } return false; } void SaveChunk(Chunk* chunk, int cx, int cz) { if (currentWorldName.empty() || !chunk->modified) return; std::filesystem::create_directories("saves/" + currentWorldName); std::string path = "saves/" + currentWorldName + "/chunk_" + std::to_string(cx) + "_" + std::to_string(cz) + ".dat"; std::ofstream file(path, std::ios::binary); if (file.is_open()) { file.write((char*)chunk->blocks, sizeof(chunk->blocks)); chunk->modified = false; } } int GetBlock(int x, int y, int z) { if (y < 0 || y >= CHUNK_HEIGHT) return 0; int cx = (int)floorf((float)x / CHUNK_SIZE); int cz = (int)floorf((float)z / CHUNK_SIZE); ChunkPos key = { cx, cz }; if (worldChunks.find(key) != worldChunks.end()) { int lx = x - (cx * CHUNK_SIZE); int lz = z - (cz * CHUNK_SIZE); return worldChunks[key]->blocks[lx][y][lz]; } return 0; // Air if chunk not loaded } unsigned char GetExposedFaces(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP) { auto IsTrans = [](int bt) { return bt == AIR || bt == LEAVES; }; unsigned char mask = 0; // Front (+Z) if (lz < CHUNK_SIZE - 1) { if (IsTrans(chunk->blocks[lx][ly][lz+1])) mask |= 1; } else if (nzP) { if (IsTrans(nzP->blocks[lx][ly][0])) mask |= 1; } else mask |= 1; // Back (-Z) if (lz > 0) { if (IsTrans(chunk->blocks[lx][ly][lz-1])) mask |= 2; } else if (nzM) { if (IsTrans(nzM->blocks[lx][ly][CHUNK_SIZE-1])) mask |= 2; } else mask |= 2; // Top (+Y) if (ly < CHUNK_HEIGHT - 1) { if (IsTrans(chunk->blocks[lx][ly+1][lz])) mask |= 4; } else mask |= 4; // Bottom (-Y) if (ly > 0) { if (IsTrans(chunk->blocks[lx][ly-1][lz])) mask |= 8; } else mask |= 8; // Right (+X) if (lx < CHUNK_SIZE - 1) { if (IsTrans(chunk->blocks[lx+1][ly][lz])) mask |= 16; } else if (nxP) { if (IsTrans(nxP->blocks[0][ly][lz])) mask |= 16; } else mask |= 16; // Left (-X) if (lx > 0) { if (IsTrans(chunk->blocks[lx-1][ly][lz])) mask |= 32; } else if (nxM) { if (IsTrans(nxM->blocks[CHUNK_SIZE-1][ly][lz])) mask |= 32; } else mask |= 32; return mask; } void RebuildChunkRenderList(Chunk* chunk, int cx, int cz) { for (int i = 0; i < 64; i++) chunk->renderLists[i].clear(); auto itNM = worldChunks.find({cx-1, cz}); Chunk* nxM = (itNM != worldChunks.end()) ? itNM->second : nullptr; auto itNP = worldChunks.find({cx+1, cz}); Chunk* nxP = (itNP != worldChunks.end()) ? itNP->second : nullptr; auto itZM = worldChunks.find({cx, cz-1}); Chunk* nzM = (itZM != worldChunks.end()) ? itZM->second : nullptr; auto itZP = worldChunks.find({cx, cz+1}); Chunk* nzP = (itZP != worldChunks.end()) ? itZP->second : nullptr; int worldX = cx * CHUNK_SIZE; int worldZ = cz * CHUNK_SIZE; for (int lx = 0; lx < CHUNK_SIZE; lx++) { for (int ly = 0; ly <= chunk->maxY; ly++) { for (int lz = 0; lz < CHUNK_SIZE; lz++) { int bt = chunk->blocks[lx][ly][lz]; if (bt == AIR) continue; unsigned char faces = GetExposedFaces(lx, ly, lz, chunk, nxM, nxP, nzM, nzP); if (faces != 0) { // Simple Torch Lighting (v2.3.3) float blockLight = 0.0f; Vector3 bPos = {(float)(worldX+lx), (float)ly, (float)(worldZ+lz)}; for (const auto& tp : torchPositions) { float dist = Vector3Distance(bPos, tp); if (dist < 15.0f) { float l = 1.0f - (dist / 15.0f); if (l > blockLight) blockLight = l; } } BlockRenderData brd = { (float)(worldX+lx), (float)ly, (float)(worldZ+lz), faces }; // We'll store the light level in a temporary way or just bake it into the tint later. // For now, let's add a light field to BlockRenderData if we can. chunk->renderLists[bt].push_back(brd); } } } } chunk->dirty = false; } int RecvAll(Socket s, char* buf, int len) { int total = 0; while (total < len) { int r = recv(s, buf + total, len - total, 0); if (r <= 0) { #ifdef _WIN32 if (r < 0 && WSAGetLastError() == WSAEWOULDBLOCK) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } #else if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } #endif return r; } total += r; } return total; } int SendAll(Socket s, const char* buf, int len) { int total = 0; while (total < len) { int r = send(s, buf + total, len - total, 0); if (r <= 0) return r; total += r; } return total; } void NetSetBlock(int x, int y, int z, int type) { // Torch tracking int oldBT = GetBlock(x, y, z); if (oldBT == TORCH) { for (auto it = torchPositions.begin(); it != torchPositions.end(); ++it) { if (it->x == x && it->y == y && it->z == z) { torchPositions.erase(it); break; } } } if (type == TORCH) torchPositions.push_back({(float)x, (float)y, (float)z}); SetBlock(x, y, z, type); PacketHeader head = { (uint8_t)PACKET_BLOCK_CHANGE, (uint32_t)sizeof(PacketBlockChange) }; PacketBlockChange bc = { x, y, z, type }; if (clientSocket != INVALID_SOCKET_VAL) { SendAll(clientSocket, (char*)&head, sizeof(head)); SendAll(clientSocket, (char*)&bc, sizeof(bc)); } if (serverSocket != INVALID_SOCKET_VAL) { for (auto& s : clientSockets) { SendAll(s, (char*)&head, sizeof(head)); SendAll(s, (char*)&bc, sizeof(bc)); } } } void SetBlock(int x, int y, int z, int type) { if (y < 0 || y >= CHUNK_HEIGHT) return; int cx = (int)floorf((float)x / CHUNK_SIZE); int cz = (int)floorf((float)z / CHUNK_SIZE); ChunkPos key = { cx, cz }; if (worldChunks.find(key) == worldChunks.end()) { GenerateChunk(cx, cz); } if (worldChunks.find(key) != worldChunks.end()) { int lx = x - (cx * CHUNK_SIZE); int lz = z - (cz * CHUNK_SIZE); worldChunks[key]->blocks[lx][y][lz] = type; worldChunks[key]->modified = true; worldChunks[key]->dirty = true; // Keep maxY up to date if (type != AIR && y > worldChunks[key]->maxY) worldChunks[key]->maxY = y; if (lz == CHUNK_SIZE-1) { auto n = worldChunks.find({cx, cz+1}); if (n != worldChunks.end()) n->second->dirty = true; } // Diagnostic log for block changes (helps debug sync) TraceLog(LOG_INFO, "Block set at %d, %d, %d to %d", x, y, z, type); } } std::string GetRemoteVersion() { std::string url = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/release/version.txt"; char buffer[128]; std::string result = ""; std::string cmd = "curl -s -m 5 " + url; // 5 second timeout FILE* pipe = popen(cmd.c_str(), "r"); if (!pipe) return "error"; while (fgets(buffer, sizeof(buffer), pipe) != NULL) { result += buffer; } pclose(pipe); // Trim result size_t last = result.find_last_not_of(" \n\r\t"); if (last != std::string::npos) result.erase(last + 1); return result; } long GetRemoteFileSize(std::string url) { #ifdef _WIN32 std::string cmd = "powershell -Command \"[long](Invoke-WebRequest -Method Head -Uri '" + url + "').Headers.'Content-Length'\""; #else std::string cmd = "curl -sI -L \"" + url + "\" | grep -i Content-Length | tail -n 1 | awk '{print $2}' | tr -d '\\r'"; #endif FILE* pipe = popen(cmd.c_str(), "r"); if (!pipe) return 0; char buffer[128]; long size = 0; if (fgets(buffer, sizeof(buffer), pipe) != NULL) { size = std::atol(buffer); } pclose(pipe); return size; } long GetLocalFileSize(std::string filename) { std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary); if (!in.is_open()) return 0; return (long)in.tellg(); } bool IsVersionNewer(std::string remote, std::string local) { if (remote.empty() || remote == "error" || remote == local) return false; int r1=0, r2=0, r3=0, l1=0, l2=0, l3=0; // Try to parse vX.Y.Z or X.Y.Z auto parse = [](const char* s, int &v1, int &v2, int &v3) { if (sscanf(s, "v%d.%d.%d", &v1, &v2, &v3) == 3) return true; if (sscanf(s, "%d.%d.%d", &v1, &v2, &v3) == 3) return true; // Fallback for vX.Y or X.Y v3 = 0; if (sscanf(s, "v%d.%d", &v1, &v2) == 2) return true; if (sscanf(s, "%d.%d", &v1, &v2) == 2) return true; return false; }; if (!parse(remote.c_str(), r1, r2, r3)) return false; if (!parse(local.c_str(), l1, l2, l3)) return false; if (r1 > l1) return true; if (r1 < l1) return false; if (r2 > l2) return true; if (r2 < l2) return false; return r3 > l3; } void SaveConfig() { std::ofstream file("config.cfg"); if (file.is_open()) { file << "playerName=" << playerName << "\n"; file << "music=" << masterMusicVolume << "\n"; file << "sound=" << masterSoundVolume << "\n"; file << "shirt=" << (int)myShirtColor.r << "," << (int)myShirtColor.g << "," << (int)myShirtColor.b << "\n"; file << "pants=" << (int)myPantsColor.r << "," << (int)myPantsColor.g << "," << (int)myPantsColor.b << "\n"; file.close(); } } void LoadConfig() { std::ifstream file("config.cfg"); if (file.is_open()) { std::string line; while (std::getline(file, line)) { size_t sep = line.find('='); if (sep != std::string::npos) { std::string key = line.substr(0, sep); std::string val = line.substr(sep + 1); if (key == "playerName") playerName = val; else if (key == "music") masterMusicVolume = std::stof(val); else if (key == "sound") masterSoundVolume = std::stof(val); } } file.close(); } else { playerName = ""; // Trigger name entry popup } } void GenerateTrees(Chunk* chunk, int cx, int cz) { for (int x = 0; x < CHUNK_SIZE; x++) { for (int z = 0; z < CHUNK_SIZE; z++) { // Find the highest block int topY = -1; for (int y = CHUNK_HEIGHT - 1; y >= 0; y--) { if (chunk->blocks[x][y][z] != AIR) { topY = y; break; } } // Trees only spawn on grass if (topY != -1 && chunk->blocks[x][topY][z] == GRASS) { // Pseudo-random chance to spawn a tree unsigned int treeHash = (cx * CHUNK_SIZE + x) * 73856093 ^ (cz * CHUNK_SIZE + z) * 19349663; treeHash ^= globalSeedHash; // ~0.5% chance to spawn a tree, and ensure there's enough height if (treeHash % 1000 < 5 && topY < CHUNK_HEIGHT - 6) { int treeHeight = 4 + (treeHash % 2); // 4 or 5 blocks tall // Wood logs for (int i = 1; i <= treeHeight; i++) { chunk->blocks[x][topY + i][z] = LOG; } // 3x3x3 Leaf cluster at the top for (int lx = x - 1; lx <= x + 1; lx++) { for (int lz = z - 1; lz <= z + 1; lz++) { for (int ly = topY + treeHeight - 1; ly <= topY + treeHeight + 1; ly++) { if (lx >= 0 && lx < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE && ly >= 0 && ly < CHUNK_HEIGHT) { // Don't overwrite the wood logs if (chunk->blocks[lx][ly][lz] == AIR) { chunk->blocks[lx][ly][lz] = LEAVES; } } } } } } } } } } // Fractal Brownian Motion: stacks multiple octaves of Perlin noise for smooth terrain float fbm(float x, float y, unsigned int seed) { float value = 0.0f; float amplitude = 1.0f; float frequency = 1.0f; float maxValue = 0.0f; // 4 octaves: coarse shape + medium bumps + fine details + micro-texture for (int i = 0; i < 4; i++) { value += perlin(x * frequency, y * frequency, seed + i * 7919) * amplitude; maxValue += amplitude; amplitude *= 0.5f; // each octave is half the strength frequency *= 2.0f; // each octave is double the detail } return value / maxValue; // normalize back to -1..1 } // Forward-declare so FindSpawnY can call GenerateChunk void GenerateChunk(int cx, int cz); // Find a spawn point near (0,0) that is far from biome edges Vector2 FindIdealSpawn() { float bestScore = -1.0f; Vector2 bestPos = { 0, 0 }; // Scan a spiral to find the most "stable" biome point for (int r = 0; r < 500; r += 16) { for (float a = 0; a < 2.0f * PI; a += 0.4f) { float x = cosf(a) * r; float z = sinf(a) * r; float bn = fbm(x * 0.002f, z * 0.002f, globalSeedHash + 77777); // Score based on distance from the transition thresholds (-0.3, 0.3) // We want points that are deeply in a biome (e.g. bn = 0.0, 0.7, or -0.7) float distToEdge = fminf(fabsf(bn - 0.3f), fabsf(bn + 0.3f)); // Also prioritize points closer to origin if scores are similar float finalScore = distToEdge - (r * 0.0001f); if (finalScore > bestScore) { bestScore = finalScore; bestPos = { x, z }; } } // If we found a very stable point (dist > 0.2 from edge), stop early if (bestScore > 0.2f) break; } return bestPos; } // Generate the spawn chunk and scan downward to find the first solid surface. // Returns the player eye-level Y (surface block top + 1.6 camera height). float FindSpawnY(int spawnX, int spawnZ) { int cx = (int)floorf((float)spawnX / CHUNK_SIZE); int cz = (int)floorf((float)spawnZ / CHUNK_SIZE); GenerateChunk(cx, cz); for (int y = CHUNK_HEIGHT - 1; y >= 1; y--) { if (GetBlock(spawnX, y, spawnZ) != AIR) { // Feet land at top of block (y + 0.5). // We use +0.6f + 1.6f to ensure we start slightly ABOVE the block // to avoid getting stuck in collision on frame 1. // v2.3.3: Spawn higher (feet at y + 1.5, eyes at y + 3.1) to avoid ground-clipping return (float)y + 1.5f + 1.6f; } } return 64.0f; // Safer fallback (above typical y=32 ground) } void GenerateChunk(int cx, int cz) { ChunkPos key = { cx, cz }; if (worldChunks.find(key) != worldChunks.end()) return; Chunk* newChunk = new Chunk(); if (LoadChunk(newChunk, cx, cz)) { worldChunks[key] = newChunk; auto n1 = worldChunks.find({cx-1, cz}); if (n1 != worldChunks.end()) n1->second->dirty = true; auto n2 = worldChunks.find({cx+1, cz}); if (n2 != worldChunks.end()) n2->second->dirty = true; auto n3 = worldChunks.find({cx, cz-1}); if (n3 != worldChunks.end()) n3->second->dirty = true; auto n4 = worldChunks.find({cx, cz+1}); if (n4 != worldChunks.end()) n4->second->dirty = true; return; } // Initialize with AIR for (int x=0; xblocks[x][y][z] = AIR; for (int x = 0; x < CHUNK_SIZE; x++) { for (int z = 0; z < CHUNK_SIZE; z++) { float worldX = cx * CHUNK_SIZE + x; float worldZ = cz * CHUNK_SIZE + z; // Multi-layer terrain generation for interesting landscapes // Layer 1: Continent-scale rolling hills (broad, smooth features) float continentNoise = fbm(worldX * 0.004f, worldZ * 0.004f, globalSeedHash); // Layer 2: Local detail bumps (smaller, sharper features) float detailNoise = fbm(worldX * 0.015f, worldZ * 0.015f, globalSeedHash + 9999); // Combine: broad hills (±12) + local detail (±4) = up to ±16 around Y=32 int height = 32 + (int)(continentNoise * 12.0f) + (int)(detailNoise * 4.0f); // Spawn Plateau (v2.3.3): Smoothly flatten area around (0,0) float distToSpawn = sqrtf(worldX * worldX + worldZ * worldZ); if (distToSpawn < 12.0f) { float plateauStrength = 1.0f - (distToSpawn / 12.0f); if (plateauStrength > 1.0f) plateauStrength = 1.0f; // Force height towards 32 in the center height = (int)(height * (1.0f - plateauStrength) + 32 * plateauStrength); } if (height < 10) height = 10; if (height >= CHUNK_HEIGHT - 2) height = CHUNK_HEIGHT - 2; // Biome noise: determines surface type (v2.3.3: 4x larger biomes) float biomeNoise = fbm(worldX * 0.002f, worldZ * 0.002f, globalSeedHash + 77777); for (int y = 0; y <= height; y++) { if (y == 0) { newChunk->blocks[x][y][z] = BEDROCK; } else if (y == height) { // Surface block depends on biome and height if (height <= 30) { // Low areas = beach/sand newChunk->blocks[x][y][z] = SAND; } else if (biomeNoise > 0.3f) { // Desert biome newChunk->blocks[x][y][z] = SAND; } else if (biomeNoise < -0.3f) { // Rocky biome newChunk->blocks[x][y][z] = COBBLESTONE; } else { // Normal grassland newChunk->blocks[x][y][z] = GRASS; } } else if (y > height - 4) { // Sub-surface: sand in desert/beach, dirt elsewhere if (height <= 30 || biomeNoise > 0.3f) { newChunk->blocks[x][y][z] = SAND; } else { newChunk->blocks[x][y][z] = DIRT; } } else { newChunk->blocks[x][y][z] = STONE; // Ore generation gated by depth so ores are never at surface. unsigned int oreHash = (cx * CHUNK_SIZE + x) * 73856093 ^ (cz * CHUNK_SIZE + z) * 19349663 ^ y * 83492791; oreHash ^= globalSeedHash; // Diamond only very deep (below Y=16, well below new surface at Y=32) if (y <= 16 && oreHash % 120 < 1) { newChunk->blocks[x][y][z] = DIAMOND_ORE; // Iron ore below Y=24 } else if (y <= 24 && oreHash % 80 < 3) { newChunk->blocks[x][y][z] = IRON_ORE; // Gravel pockets below Y=20 } else if (y <= 20 && oreHash % 60 < 4) { newChunk->blocks[x][y][z] = GRAVEL; } } } } } // Add trees GenerateTrees(newChunk, cx, cz); // Record the highest non-air block so the render loop skips empty space newChunk->maxY = 0; for (int x = 0; x < CHUNK_SIZE; x++) for (int z = 0; z < CHUNK_SIZE; z++) for (int y = CHUNK_HEIGHT - 1; y >= 0; y--) if (newChunk->blocks[x][y][z] != AIR) { if (y > newChunk->maxY) newChunk->maxY = y; break; } newChunk->generated = true; newChunk->modified = true; worldChunks[key] = newChunk; // Mark neighbors as dirty (Bug #2 fix) ChunkPos neighbors[] = { {cx-1, cz}, {cx+1, cz}, {cx, cz-1}, {cx, cz+1} }; for (auto& nPos : neighbors) { auto it = worldChunks.find(nPos); if (it != worldChunks.end()) it->second->dirty = true; } } bool CheckPlayerCollision(Vector3 pos) { BoundingBox playerBox = { (Vector3){ pos.x - 0.25f, pos.y - 1.45f, pos.z - 0.25f }, (Vector3){ pos.x + 0.25f, pos.y + 0.05f, pos.z + 0.25f } }; int minX = (int)floorf(playerBox.min.x + 0.5f); int maxX = (int)floorf(playerBox.max.x + 0.5f); int minY = (int)floorf(playerBox.min.y + 0.5f); int maxY = (int)floorf(playerBox.max.y + 0.5f); int minZ = (int)floorf(playerBox.min.z + 0.5f); int maxZ = (int)floorf(playerBox.max.z + 0.5f); for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { if (GetBlock(x, y, z) != AIR && GetBlock(x, y, z) != TORCH) { BoundingBox blockBox = { (Vector3){ x - 0.5f, y - 0.5f, z - 0.5f }, (Vector3){ x + 0.5f, y + 0.5f, z + 0.5f } }; if (CheckCollisionBoxes(playerBox, blockBox)) { return true; } } } } } return false; } void DrawTexturedCube(Vector3 position, float width, float height, float length, Color color) { float x = position.x; float y = position.y; float z = position.z; rlBegin(RL_QUADS); rlColor4ub(color.r, color.g, color.b, color.a); // Front rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z + length/2); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z + length/2); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z + length/2); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z + length/2); // Back rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z - length/2); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z - length/2); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z - length/2); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z - length/2); // Top rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - width/2, y + height/2, z - length/2); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z + length/2); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z + length/2); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + width/2, y + height/2, z - length/2); // Bottom rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z - length/2); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z - length/2); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + width/2, y - height/2, z + length/2); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - width/2, y - height/2, z + length/2); // Right rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z - length/2); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z - length/2); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z + length/2); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z + length/2); // Left rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z - length/2); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z + length/2); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z + length/2); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z - length/2); rlEnd(); } // Draws a grass block with correct per-face textures: // top=green grass, sides=dirt+grass stripe, bottom=pure dirt. // Caller must NOT have an active rlSetTexture — this function manages its own binds. void DrawCubeVertices(float x, float y, float z, float w, float h, float l, unsigned char mask) { float hw = w/2.0f; float hh = h/2.0f; float hl = l/2.0f; if (mask & 1) { // Front (+Z) rlNormal3f(0, 0, 1); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - hw, y - hh, z + hl); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + hw, y - hh, z + hl); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + hw, y + hh, z + hl); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - hw, y + hh, z + hl); } if (mask & 2) { // Back (-Z) rlNormal3f(0, 0, -1); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - hw, y - hh, z - hl); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - hw, y + hh, z - hl); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + hw, y + hh, z - hl); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + hw, y - hh, z - hl); } if (mask & 4) { // Top (+Y) rlNormal3f(0, 1, 0); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - hw, y + hh, z - hl); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - hw, y + hh, z + hl); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + hw, y + hh, z + hl); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + hw, y + hh, z - hl); } if (mask & 8) { // Bottom (-Y) rlNormal3f(0, -1, 0); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - hw, y - hh, z - hl); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + hw, y - hh, z - hl); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + hw, y - hh, z + hl); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - hw, y - hh, z + hl); } if (mask & 16) { // Right (+X) rlNormal3f(1, 0, 0); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + hw, y - hh, z - hl); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + hw, y + hh, z - hl); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + hw, y + hh, z + hl); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + hw, y - hh, z + hl); } if (mask & 32) { // Left (-X) rlNormal3f(-1, 0, 0); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - hw, y - hh, z - hl); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - hw, y - hh, z + hl); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - hw, y + hh, z + hl); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - hw, y + hh, z - hl); } } void DrawGrassBlock(Vector3 position, unsigned int sideTexId, unsigned int topTexId, unsigned int botTexId, Color tint, unsigned char mask) { float x = position.x; float y = position.y; float z = position.z; if (mask & 51) { // Sides (Front, Back, Right, Left: 1|2|16|32 = 51) rlSetTexture(sideTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); if (mask & 1) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); } if (mask & 2) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); } if (mask & 16) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); } if (mask & 32) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); } rlEnd(); } if (mask & 4) { // Top rlSetTexture(topTexId); rlBegin(RL_QUADS); rlColor4ub((unsigned char)(160 * (tint.r/255.0f)), (unsigned char)(230 * (tint.g/255.0f)), (unsigned char)(140 * (tint.b/255.0f)), 255); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlEnd(); } if (mask & 8) { // Bottom rlSetTexture(botTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlEnd(); } } void DrawLog(Vector3 position, unsigned int sideTexId, unsigned int topTexId, Color tint, unsigned char mask) { float x = position.x; float y = position.y; float z = position.z; if (mask & 51) { rlSetTexture(sideTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); if (mask & 1) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); } if (mask & 2) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); } if (mask & 16) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); } if (mask & 32) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); } rlEnd(); } if (mask & 12) { // Top + Bottom rlSetTexture(topTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); if (mask & 4) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); } if (mask & 8) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); } rlEnd(); } } void DrawCraftingTable(Vector3 position, unsigned int sideTexId, unsigned int topTexId, unsigned int botTexId, Color tint, unsigned char mask) { float x = position.x; float y = position.y; float z = position.z; if (mask & 51) { rlSetTexture(sideTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); if (mask & 1) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); } if (mask & 2) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); } if (mask & 16) { rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); } if (mask & 32) { rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); } rlEnd(); } if (mask & 4) { rlSetTexture(topTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlEnd(); } if (mask & 8) { rlSetTexture(botTexId); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, 255); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlEnd(); } } // Optimized check to see if a block has any exposed faces bool IsExposed(int x, int ly, int z, Chunk* chunk, int lx, int lz) { // Local check (inside same chunk) if (lx > 0 && lx < CHUNK_SIZE - 1 && ly > 0 && ly < CHUNK_HEIGHT - 1 && lz > 0 && lz < CHUNK_SIZE - 1) { if (chunk->blocks[lx-1][ly][lz] == AIR) return true; if (chunk->blocks[lx+1][ly][lz] == AIR) return true; if (chunk->blocks[lx][ly-1][lz] == AIR) return true; if (chunk->blocks[lx][ly+1][lz] == AIR) return true; if (chunk->blocks[lx][ly][lz-1] == AIR) return true; if (chunk->blocks[lx][ly][lz+1] == AIR) return true; return false; } // Global check (at chunk boundary) if (GetBlock(x-1, ly, z) == AIR) return true; if (GetBlock(x+1, ly, z) == AIR) return true; if (GetBlock(x, ly-1, z) == AIR) return true; if (GetBlock(x, ly+1, z) == AIR) return true; if (GetBlock(x, ly, z-1) == AIR) return true; if (GetBlock(x, ly, z+1) == AIR) return true; return false; } int main(void) { SetRandomSeed((unsigned int)time(NULL)); // Initialization //-------------------------------------------------------------------------------------- const int screenWidth = 1280; const int screenHeight = 720; // Set config flags to allow window resizing. // By default, windows have minimize, maximize, and close buttons on the top bar. SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT); InitWindow(screenWidth, screenHeight, "MorriCraft v2.3.3"); LoadConfig(); SetExitKey(KEY_NULL); // Prevent ESC from closing the window // Initialize audio device InitAudioDevice(); hitSound = LoadSound("assets/hit.wav"); if (hitSound.frameCount == 0) { // Procedural fallback if asset missing Wave w = { 0 }; w.frameCount = 4410; w.sampleRate = 44100; w.sampleSize = 16; w.channels = 1; w.data = RL_MALLOC(w.frameCount * 2); short* d = (short*)w.data; for(int i=0; i 4 Planks (single slot, any position) if (filledSlots == 1) { for (int i = 0; i < 4; i++) { if (craftingSlots[i].blockType == LOG) { craftingResult = InventorySlot(PLANK, 4); return; } } } // Recipe: 4 Planks -> 1 Crafting Table if (craftingSlots[0].blockType == PLANK && craftingSlots[1].blockType == PLANK && craftingSlots[2].blockType == PLANK && craftingSlots[3].blockType == PLANK) { craftingResult = InventorySlot(CRAFTING_TABLE, 1); return; } // Recipe: 2 Planks (vertical) -> 4 Sticks if ((craftingSlots[0].blockType == PLANK && craftingSlots[2].blockType == PLANK && craftingSlots[1].blockType == AIR && craftingSlots[3].blockType == AIR) || (craftingSlots[1].blockType == PLANK && craftingSlots[3].blockType == PLANK && craftingSlots[0].blockType == AIR && craftingSlots[2].blockType == AIR)) { craftingResult = InventorySlot(STICK, 4); return; } craftingResult = InventorySlot(AIR, 0); }; auto UpdateTableCrafting = [&]() { // Helper: get block type at grid position, -1 if out of bounds auto T = [&](int i) -> int { return (i >= 0 && i < 9) ? tableSlots[i].blockType : AIR; }; // Count filled slots and find bounding box int filledSlots = 0; for (int i = 0; i < 9; i++) { if (T(i) != AIR) filledSlots++; } // === SINGLE SLOT RECIPES === if (filledSlots == 1) { for (int i = 0; i < 9; i++) { if (T(i) == LOG) { tableResult = InventorySlot(PLANK, 4); return; } } } // === Helper: Check a 3x3 pattern against grid === // Pattern: array of 9 ints, -1 means "must be empty", type means "must match" auto checkPattern = [&](int p[9]) -> bool { for (int i = 0; i < 9; i++) { if (p[i] == -1) { if (T(i) != AIR) return false; } else { if (T(i) != p[i]) return false; } } return true; }; // === TOOLS (check all valid column offsets) === // Wooden Pickaxe: PPP / .S. / .S. for (int c = 0; c <= 0; c++) { int p[9] = {PLANK, PLANK, PLANK, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_PICKAXE, 1); return; } } // Wooden Axe: PP. / PS. / .S. (and mirrored PP. -> .PP) { int p[9] = {PLANK, PLANK, -1, PLANK, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_AXE, 1); return; } } { int p[9] = {-1, PLANK, PLANK, -1, STICK, PLANK, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_AXE, 1); return; } } // Wooden Sword: .P. / .P. / .S. { int p[9] = {-1, PLANK, -1, -1, PLANK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_SWORD, 1); return; } } // Wooden Shovel: .P. / .S. / .S. { int p[9] = {-1, PLANK, -1, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_SHOVEL, 1); return; } } // Wooden Hoe: PP. / .S. / .S. (and mirrored) { int p[9] = {PLANK, PLANK, -1, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_HOE, 1); return; } } { int p[9] = {-1, PLANK, PLANK, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(WOOD_HOE, 1); return; } } // Stone Pickaxe: CCC / .S. / .S. { int p[9] = {COBBLESTONE, COBBLESTONE, COBBLESTONE, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_PICKAXE, 1); return; } } // Stone Axe: CC. / CS. / .S. (and mirrored) { int p[9] = {COBBLESTONE, COBBLESTONE, -1, COBBLESTONE, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_AXE, 1); return; } } { int p[9] = {-1, COBBLESTONE, COBBLESTONE, -1, STICK, COBBLESTONE, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_AXE, 1); return; } } // Stone Sword: .C. / .C. / .S. { int p[9] = {-1, COBBLESTONE, -1, -1, COBBLESTONE, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_SWORD, 1); return; } } // Stone Shovel: .C. / .S. / .S. { int p[9] = {-1, COBBLESTONE, -1, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_SHOVEL, 1); return; } } // Stone Hoe: CC. / .S. / .S. (and mirrored) { int p[9] = {COBBLESTONE, COBBLESTONE, -1, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_HOE, 1); return; } } { int p[9] = {-1, COBBLESTONE, COBBLESTONE, -1, STICK, -1, -1, STICK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_HOE, 1); return; } } // === BLOCKS === // Furnace: CCC / C.C / CCC { int p[9] = {COBBLESTONE, COBBLESTONE, COBBLESTONE, COBBLESTONE, -1, COBBLESTONE, COBBLESTONE, COBBLESTONE, COBBLESTONE}; if (checkPattern(p)) { tableResult = InventorySlot(FURNACE, 1); return; } } // Chest: PPP / P.P / PPP { int p[9] = {PLANK, PLANK, PLANK, PLANK, -1, PLANK, PLANK, PLANK, PLANK}; if (checkPattern(p)) { tableResult = InventorySlot(CHEST, 1); return; } } // Door: PP. / PP. / PP. { int p[9] = {PLANK, PLANK, -1, PLANK, PLANK, -1, PLANK, PLANK, -1}; if (checkPattern(p)) { tableResult = InventorySlot(DOOR, 3); return; } } { int p[9] = {-1, PLANK, PLANK, -1, PLANK, PLANK, -1, PLANK, PLANK}; if (checkPattern(p)) { tableResult = InventorySlot(DOOR, 3); return; } } // Fence: PSP / PSP / ... (bottom row empty) { int p[9] = {PLANK, STICK, PLANK, PLANK, STICK, PLANK, -1, -1, -1}; if (checkPattern(p)) { tableResult = InventorySlot(FENCE, 3); return; } } { int p[9] = {-1, -1, -1, PLANK, STICK, PLANK, PLANK, STICK, PLANK}; if (checkPattern(p)) { tableResult = InventorySlot(FENCE, 3); return; } } // Ladder: S.S / SSS / S.S { int p[9] = {STICK, -1, STICK, STICK, STICK, STICK, STICK, -1, STICK}; if (checkPattern(p)) { tableResult = InventorySlot(LADDER, 3); return; } } // Stone Slab: ... / ... / CCC (bottom row) { int p[9] = {-1, -1, -1, -1, -1, -1, COBBLESTONE, COBBLESTONE, COBBLESTONE}; if (checkPattern(p)) { tableResult = InventorySlot(STONE_SLAB, 6); return; } } // === SIMPLE 2x2 RECIPES (in any 2x2 sub-grid of the 3x3) === // Crafting Table: 2x2 planks (check all four 2x2 positions) for (int r = 0; r <= 1; r++) { for (int c = 0; c <= 1; c++) { int i0 = r*3+c, i1 = r*3+c+1, i2 = (r+1)*3+c, i3 = (r+1)*3+c+1; if (T(i0) == PLANK && T(i1) == PLANK && T(i2) == PLANK && T(i3) == PLANK) { // Make sure other slots are empty bool othersEmpty = true; for (int i = 0; i < 9; i++) { if (i != i0 && i != i1 && i != i2 && i != i3 && T(i) != AIR) othersEmpty = false; } if (othersEmpty) { tableResult = InventorySlot(CRAFTING_TABLE, 1); return; } } } } // Sticks: 2 planks vertical (in any column, any two adjacent rows) for (int c = 0; c < 3; c++) { for (int r = 0; r <= 1; r++) { int top = r*3+c, bot = (r+1)*3+c; if (T(top) == PLANK && T(bot) == PLANK) { bool othersEmpty = true; for (int i = 0; i < 9; i++) { if (i != top && i != bot && T(i) != AIR) othersEmpty = false; } if (othersEmpty) { tableResult = InventorySlot(STICK, 4); return; } } } } tableResult = InventorySlot(AIR, 0); }; // Block Selection State (for wireframe) bool hitBlock = false; int hitX = -1, hitY = -1, hitZ = -1; Vector3 closestNormal = {0}; while (!WindowShouldClose()) // Detect window close button or ESC key { // Update //---------------------------------------------------------------------------------- // Update ONLY active music streams and ensure others are fully stopped bool inGame = (currentState == GAMEPLAY || currentState == PAUSE_MENU || currentState == CRAFTING_GUI || currentState == CHEAT_GUI || currentState == CHEST_GUI || (currentState == OPTIONS_MENU && optionsReturnState != MAIN_MENU)); if (!inGame) { UpdateMusicStream(titleMusic); if (IsMusicStreamPlaying(gameplayMusic)) StopMusicStream(gameplayMusic); if (IsMusicStreamPlaying(nightMusic)) StopMusicStream(nightMusic); } else { UpdateMusicStream(gameplayMusic); UpdateMusicStream(nightMusic); if (IsMusicStreamPlaying(titleMusic)) StopMusicStream(titleMusic); } // --- NETWORK UPDATE --- if (isConnecting) { fd_set writefds; FD_ZERO(&writefds); FD_SET(clientSocket, &writefds); struct timeval tv = {0, 0}; if (select((int)clientSocket + 1, NULL, &writefds, NULL, &tv) > 0) { isConnecting = false; currentState = GAMEPLAY; DisableCursor(); // Send Handshake PacketHeader head = { (uint8_t)PACKET_HANDSHAKE, (uint32_t)sizeof(PacketHandshake) }; PacketHandshake hand; strncpy(hand.name, playerName.c_str(), 31); hand.shirtR = myShirtColor.r; hand.shirtG = myShirtColor.g; hand.shirtB = myShirtColor.b; hand.pantsR = myPantsColor.r; hand.pantsG = myPantsColor.g; hand.pantsB = myPantsColor.b; SendAll(clientSocket, (char*)&head, sizeof(head)); SendAll(clientSocket, (char*)&hand, sizeof(hand)); } } // Handle incoming data auto handleIncoming = [&](Socket sock, bool isServer, int clientIdx = -1) { while (true) { fd_set readfds; FD_ZERO(&readfds); FD_SET(sock, &readfds); struct timeval tv = {0, 0}; if (select((int)sock + 1, &readfds, NULL, NULL, &tv) <= 0) break; PacketHeader head; int bytes = recv(sock, (char*)&head, sizeof(head), 0); if (bytes <= 0) { if (isServer && clientIdx != -1) { std::string leaver = "A player"; for (auto it = remotePlayers.begin(); it != remotePlayers.end(); ++it) { if (it->sock == sock) { leaver = it->name; remotePlayers.erase(it); break; } } chatLog.push_back({ leaver + " left the game", 5.0f }); PacketHeader nHead = { (uint8_t)PACKET_CHAT, (uint32_t)sizeof(PacketChat) }; PacketChat nChat; strncpy(nChat.name, "Server", 31); strncpy(nChat.message, (leaver + " left the game").c_str(), 127); for (auto& s : clientSockets) { if (s != sock) { SendAll(s, (char*)&nHead, sizeof(nHead)); SendAll(s, (char*)&nChat, sizeof(nChat)); } } closesocket(sock); clientSockets.erase(clientSockets.begin() + clientIdx); } break; } if (head.type == PACKET_HANDSHAKE) { PacketHandshake hand; if (RecvAll(sock, (char*)&hand, sizeof(hand)) <= 0) break; if (isServer) { bool duplicate = false; for (auto& rp : remotePlayers) { if (rp.sock == sock) { duplicate = true; break; } } if (!duplicate) { uint32_t newID = (uint32_t)sock; RemotePlayer rp; rp.sock = sock; rp.id = newID; rp.name = hand.name; rp.position = (Vector3){0,0,0}; rp.shirtColor = (Color){ hand.shirtR, hand.shirtG, hand.shirtB, 255 }; rp.pantsColor = (Color){ hand.pantsR, hand.pantsG, hand.pantsB, 255 }; remotePlayers.push_back(rp); chatLog.push_back({ std::string(hand.name) + " joined the game", 5.0f }); PacketHeader nHead = { (uint8_t)PACKET_CHAT, (uint32_t)sizeof(PacketChat) }; PacketChat nChat; strncpy(nChat.name, "Server", 31); strncpy(nChat.message, (std::string(hand.name) + " joined the game").c_str(), 127); for (auto& s : clientSockets) { SendAll(s, (char*)&nHead, sizeof(nHead)); SendAll(s, (char*)&nChat, sizeof(nChat)); } PacketHeader sHead = { (uint8_t)PACKET_SEED_SYNC, (uint32_t)sizeof(PacketSeedSync) }; PacketSeedSync sData = { (int)globalSeedHash }; SendAll(sock, (char*)&sHead, sizeof(sHead)); SendAll(sock, (char*)&sData, sizeof(sData)); PacketHeader tHead = { (uint8_t)PACKET_TIME_SYNC, (uint32_t)sizeof(PacketTimeSync) }; PacketTimeSync tData = { gameTime }; SendAll(sock, (char*)&tHead, sizeof(tHead)); SendAll(sock, (char*)&tData, sizeof(tData)); for (auto& existing : remotePlayers) { if (existing.sock != sock) { PacketHeader pHead = { (uint8_t)PACKET_PLAYER_UPDATE, (uint32_t)sizeof(PacketPlayerUpdate) }; PacketPlayerUpdate pData = { existing.position.x, existing.position.y, existing.position.z, existing.yaw, existing.id }; SendAll(sock, (char*)&pHead, sizeof(pHead)); SendAll(sock, (char*)&pData, sizeof(pData)); } } // Tell the new client its own ID (its socket value) PacketHeader idHead = { (uint8_t)PACKET_PLAYER_UPDATE, (uint32_t)sizeof(PacketPlayerUpdate) }; PacketPlayerUpdate idData = { camera3D.position.x, camera3D.position.y - 1.6f, camera3D.position.z, camYaw, (uint32_t)sock }; SendAll(sock, (char*)&idHead, sizeof(idHead)); PacketHeader hHead = { (uint8_t)PACKET_PLAYER_UPDATE, (uint32_t)sizeof(PacketPlayerUpdate) }; PacketPlayerUpdate hData = { camera3D.position.x, camera3D.position.y - 1.6f, camera3D.position.z, camYaw, 0 }; SendAll(sock, (char*)&hHead, sizeof(hHead)); SendAll(sock, (char*)&hData, sizeof(hData)); } } } else if (head.type == PACKET_PLAYER_UPDATE) { PacketPlayerUpdate pu; if (RecvAll(sock, (char*)&pu, sizeof(pu)) <= 0) break; if (isServer) pu.playerID = (uint32_t)sock; bool found = false; for (auto& rp : remotePlayers) { if (rp.id == pu.playerID) { rp.position = (Vector3){ pu.x, pu.y, pu.z }; rp.yaw = pu.yaw; found = true; break; } } if (pu.playerID != 0 && !isServer && pu.playerID != (uint32_t)clientSocket) { // If it's a new player we haven't seen yet if (!found) { RemotePlayer rp; rp.id = pu.playerID; rp.name = "Remote Player"; rp.position = (Vector3){ pu.x, pu.y, pu.z }; rp.yaw = pu.yaw; rp.shirtColor = BLUE; rp.pantsColor = DARKBLUE; remotePlayers.push_back(rp); } } else if (!isServer && pu.playerID == (uint32_t)clientSocket) { localPlayerID = pu.playerID; } if (isServer) { pu.playerID = (uint32_t)sock; for (auto& other : clientSockets) { if (other != sock) { SendAll(other, (char*)&head, sizeof(head)); SendAll(other, (char*)&pu, sizeof(pu)); } } } } else if (head.type == PACKET_BLOCK_CHANGE) { PacketBlockChange bc; if (RecvAll(sock, (char*)&bc, sizeof(bc)) <= 0) break; SetBlock(bc.x, bc.y, bc.z, bc.blockType); if (isServer) { for (auto& other : clientSockets) { if (other != sock) { SendAll(other, (char*)&head, sizeof(head)); SendAll(other, (char*)&bc, sizeof(bc)); } } } } else if (head.type == PACKET_TIME_SYNC) { PacketTimeSync ts; if (RecvAll(sock, (char*)&ts, sizeof(ts)) <= 0) break; if (!isServer) gameTime = ts.gameTime; } else if (head.type == PACKET_SEED_SYNC) { PacketSeedSync ss; if (RecvAll(sock, (char*)&ss, sizeof(ss)) <= 0) break; if (!isServer) { globalSeedHash = ss.seed; for (auto& pair : worldChunks) delete pair.second; worldChunks.clear(); // Force spawn generation to fix "falling through ground" int scx = (int)floorf(camera3D.position.x / CHUNK_SIZE); int scz = (int)floorf(camera3D.position.z / CHUNK_SIZE); GenerateChunk(scx, scz); float sy = FindSpawnY((int)camera3D.position.x, (int)camera3D.position.z); camera3D.position.y = sy + 1.6f; } } else if (head.type == PACKET_CHAT) { PacketChat pc; if (RecvAll(sock, (char*)&pc, sizeof(pc)) <= 0) break; chatLog.push_back({ std::string(pc.name) + ": " + pc.message, 5.0f }); if (isServer) { for (auto& other : clientSockets) { if (other != sock) { SendAll(other, (char*)&head, sizeof(head)); SendAll(other, (char*)&pc, sizeof(pc)); } } } } else if (head.type == PACKET_PLAYER_HIT) { PacketPlayerHit ph; if (RecvAll(sock, (char*)&ph, sizeof(ph)) <= 0) break; if (ph.targetID == localPlayerID) { // I got hit! playerHealth -= ph.damage; playerVelocityY = 6.0f; // Small jump Vector3 pushDir = Vector3Normalize((Vector3){ camera3D.position.x - ph.attackerX, 0, camera3D.position.z - ph.attackerZ }); camera3D.position.x += pushDir.x * 0.8f; camera3D.position.z += pushDir.z * 0.8f; PlaySound(hitSound); chatLog.push_back({ "You were hit!", 2.0f }); } if (isServer) { // Broadcast hit to all OTHER clients for (auto& other : clientSockets) { if (other != sock) { SendAll(other, (char*)&head, sizeof(head)); SendAll(other, (char*)&ph, sizeof(ph)); } } } } else { if (head.size > 0 && head.size < 2048) { std::vector discard(head.size); RecvAll(sock, discard.data(), head.size); } } } }; if (clientSocket != INVALID_SOCKET_VAL && !isConnecting) { handleIncoming(clientSocket, false); // Send our position static float netTimer = 0.0f; netTimer += GetFrameTime(); if (netTimer > 0.05f) { // 20Hz update PacketHeader head = { (uint8_t)PACKET_PLAYER_UPDATE, (uint32_t)sizeof(PacketPlayerUpdate) }; PacketPlayerUpdate pu = { camera3D.position.x, camera3D.position.y - 1.6f, camera3D.position.z, camYaw, 0 }; SendAll(clientSocket, (char*)&head, sizeof(head)); SendAll(clientSocket, (char*)&pu, sizeof(pu)); netTimer = 0.0f; } } if (serverSocket != INVALID_SOCKET_VAL) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); Socket newClient = accept(serverSocket, (struct sockaddr*)&client_addr, &client_len); if (newClient != INVALID_SOCKET_VAL) { SetNonBlocking(newClient); clientSockets.push_back(newClient); } for (size_t i = 0; i < clientSockets.size(); i++) { handleIncoming(clientSockets[i], true, i); } // Send time sync periodically static float timeSyncTimer = 0.0f; timeSyncTimer += GetFrameTime(); if (timeSyncTimer > 2.0f) { PacketHeader head = { (uint8_t)PACKET_TIME_SYNC, (uint32_t)sizeof(PacketTimeSync) }; PacketTimeSync ts = { gameTime }; for (auto& s : clientSockets) { SendAll(s, (char*)&head, sizeof(head)); SendAll(s, (char*)&ts, sizeof(ts)); } timeSyncTimer = 0.0f; } // Send Host position periodically static float hostNetTimer = 0.0f; hostNetTimer += GetFrameTime(); if (hostNetTimer > 0.05f) { PacketHeader head = { (uint8_t)PACKET_PLAYER_UPDATE, (uint32_t)sizeof(PacketPlayerUpdate) }; PacketPlayerUpdate pu = { camera3D.position.x, camera3D.position.y - 1.6f, camera3D.position.z, camYaw, 0 }; for (auto& s : clientSockets) { SendAll(s, (char*)&head, sizeof(head)); SendAll(s, (char*)&pu, sizeof(pu)); } hostNetTimer = 0.0f; } } // Handle title music loop fading float fadeTime = 2.0f; // 2 seconds fade float timePlayed = GetMusicTimePlayed(titleMusic); float timeLength = GetMusicTimeLength(titleMusic); float titleLoopFade = 1.0f; if (timePlayed < fadeTime) { titleLoopFade = timePlayed / fadeTime; } else if (timeLength > 0.0f && (timeLength - timePlayed) < fadeTime) { titleLoopFade = (timeLength - timePlayed) / fadeTime; } // Handle crossfading based on state static float crossfade = 0.0f; // 0.0 = title, 1.0 = gameplay if (inGame) { crossfade += 0.02f; if (crossfade > 1.0f) crossfade = 1.0f; } else { crossfade -= 0.02f; if (crossfade < 0.0f) crossfade = 0.0f; } // --- GLOBAL TIME & AUDIO MANAGEMENT --- float cycleLength = 600.0f; // Slower day cycle (v2.3.3: cut speed in half) if (currentState == GAMEPLAY) { gameTime += GetFrameTime(); // Hunger Depletion hungerTimer += GetFrameTime(); if (hungerTimer >= 10.0f) { // Deplete every 10 seconds hungerTimer = 0; if (playerHunger > 0) playerHunger -= 0.5f; } // Hunger Effects (Health Regen/Starve) if (playerHunger >= 20.0f) { healthRegenTimer += GetFrameTime(); if (healthRegenTimer >= 3.0f) { healthRegenTimer = 0; if (playerHealth < 16.0f) playerHealth += 1.0f; // Half heart } } else { healthRegenTimer = 0; } if (playerHunger <= 0) { healthStarveTimer += GetFrameTime(); if (healthStarveTimer >= 3.0f) { healthStarveTimer = 0; if (playerHealth > 2.0f) { playerHealth -= 1.0f; // Half heart PlaySound(hitSound); damageFlashTimer = 0.4f; // Knockback Vector3 forward = Vector3Normalize(Vector3Subtract(camera3D.target, camera3D.position)); camera3D.position = Vector3Subtract(camera3D.position, Vector3Scale(forward, 0.5f)); } } } else { healthStarveTimer = 0; } if (damageFlashTimer > 0) damageFlashTimer -= GetFrameTime(); // Item Pickup Logic for (auto& item : droppedItems) { if (item.active) { float dist = Vector3Distance(camera3D.position, item.pos); if (dist < 1.5f) { item.active = false; AddToInventory(item.type); } } } } float timeOfDay = fmodf(gameTime, cycleLength) / cycleLength; float sunAngle = timeOfDay * 2.0f * 3.14159f - 3.14159f/2.0f; dayFactor = (sinf(sunAngle) + 1.0f) / 2.0f; float quickMix = (dayFactor - 0.5f) * 5.0f + 0.5f; if (quickMix > 1.0f) quickMix = 1.0f; if (quickMix < 0.0f) quickMix = 0.0f; SetMusicVolume(titleMusic, titleLoopFade * masterMusicVolume * (1.0f - crossfade)); SetMusicVolume(gameplayMusic, masterMusicVolume * crossfade * quickMix); SetMusicVolume(nightMusic, masterMusicVolume * crossfade * (1.0f - quickMix)); // Ensure gameplay streams are playing when crossfaded in if (crossfade > 0.01f) { if (!IsMusicStreamPlaying(gameplayMusic)) PlayMusicStream(gameplayMusic); if (!IsMusicStreamPlaying(nightMusic)) PlayMusicStream(nightMusic); } // Handle window resize dynamically int currentWidth = GetScreenWidth(); int currentHeight = GetScreenHeight(); camera.offset = (Vector2){ currentWidth/2.0f, currentHeight/2.0f }; camera.target = (Vector2){ currentWidth/2.0f, currentHeight/2.0f }; // Smooth camera zoom currentZoom += (targetZoom - currentZoom) * 0.05f; camera.zoom = currentZoom; // --- GLOBAL INPUTS (Outside gameplay state) --- if (IsKeyPressed(KEY_E) && !isChatting && (currentState == GAMEPLAY || currentState == CRAFTING_GUI || currentState == CHEST_GUI || currentState == CHEAT_GUI)) { if (currentState == CRAFTING_GUI || currentState == CHEST_GUI || currentState == CHEAT_GUI) { if (currentState == CHEST_GUI) PlaySound(chestCloseSound); currentState = GAMEPLAY; inventoryOpen = false; DisableCursor(); } else { inventoryOpen = !inventoryOpen; if (inventoryOpen) EnableCursor(); else DisableCursor(); } } // Gameplay Update if (currentState == GAMEPLAY) { gameTime += GetFrameTime(); // Hotbar slot selection (keys 1-9) if (IsKeyPressed(KEY_ONE)) activeHotbarSlot = 0; if (IsKeyPressed(KEY_TWO)) activeHotbarSlot = 1; if (IsKeyPressed(KEY_THREE)) activeHotbarSlot = 2; if (IsKeyPressed(KEY_FOUR)) activeHotbarSlot = 3; if (IsKeyPressed(KEY_FIVE)) activeHotbarSlot = 4; if (IsKeyPressed(KEY_SIX)) activeHotbarSlot = 5; if (IsKeyPressed(KEY_SEVEN)) activeHotbarSlot = 6; if (IsKeyPressed(KEY_EIGHT)) activeHotbarSlot = 7; if (IsKeyPressed(KEY_NINE)) activeHotbarSlot = 8; // Scroll wheel cycles hotbar float scroll = GetMouseWheelMove(); if (scroll > 0.0f) activeHotbarSlot = (activeHotbarSlot - 1 + 9) % 9; if (scroll < 0.0f) activeHotbarSlot = (activeHotbarSlot + 1) % 9; // Dynamic Chunk Loading int playerCX = (int)floorf(camera3D.position.x / CHUNK_SIZE); int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE); for (int x = playerCX - RENDER_DISTANCE; x <= playerCX + RENDER_DISTANCE; x++) { for (int z = playerCZ - RENDER_DISTANCE; z <= playerCZ + RENDER_DISTANCE; z++) { GenerateChunk(x, z); } } // Handle chat input if (IsKeyPressed(KEY_ENTER)) { if (!isChatting) { isChatting = true; chatInput[0] = '\0'; while (GetCharPressed() != 0); // Clear buffer EnableCursor(); } else { if (strlen(chatInput) > 0) { if (chatInput[0] == '/') { // Command processing - never sent to network std::string cmd(chatInput + 1); // skip the '/' // Trim and lowercase while (!cmd.empty() && cmd.back() == ' ') cmd.pop_back(); if (cmd == "seed") { chatLog.push_back({ "[Server] World seed: " + std::to_string(globalSeedHash), 8.0f }); } else if (cmd == "fly" || cmd == "fly on" || cmd == "fly off") { if (cmd == "fly on") isFlying = true; else if (cmd == "fly off") isFlying = false; else isFlying = !isFlying; if (isFlying) chatLog.push_back({ "[Server] Flight enabled (Noclip ON)", 5.0f }); else chatLog.push_back({ "[Server] Flight disabled", 5.0f }); } else if (cmd == "test") { GiveItems(CRAFTING_TABLE, 1); GiveItems(LOG, 64); chatLog.push_back({"[System] Granted 1x Crafting Table and 64x Oak Logs.", 5.0f}); } else if (cmd == "cheat") { currentState = CHEAT_GUI; EnableCursor(); chatLog.push_back({"[System] Opened Cheat Inventory.", 5.0f}); } else if (cmd == "help") { chatLog.push_back({ "[Server] Commands List:", 8.0f }); chatLog.push_back({ " /help - Shows this list", 8.0f }); chatLog.push_back({ " /seed - Shows the world seed", 8.0f }); chatLog.push_back({ " /fly [on|off] - Toggle flight mode", 8.0f }); chatLog.push_back({ " /test - Give testing materials (1x Table, 64x Logs)", 8.0f }); chatLog.push_back({ " /cheat - Open creative menu", 8.0f }); } else { chatLog.push_back({ "[Server] Unknown command: /" + cmd, 5.0f }); } } else { // Normal chat message - send to network PacketHeader head = { (uint8_t)PACKET_CHAT, (uint32_t)sizeof(PacketChat) }; PacketChat pc; strncpy(pc.name, playerName.c_str(), 31); strncpy(pc.message, chatInput, 127); if (clientSocket != INVALID_SOCKET_VAL) { SendAll(clientSocket, (char*)&head, sizeof(head)); SendAll(clientSocket, (char*)&pc, sizeof(pc)); } if (serverSocket != INVALID_SOCKET_VAL) { chatLog.push_back({ std::string(playerName) + ": " + chatInput, 5.0f }); for (auto& s : clientSockets) { SendAll(s, (char*)&head, sizeof(head)); SendAll(s, (char*)&pc, sizeof(pc)); } } } chatInput[0] = '\0'; isChatting = false; } if (currentState == GAMEPLAY) DisableCursor(); } } if (isChatting) { int c = GetCharPressed(); while (c > 0) { if (c >= 32 && c <= 125 && strlen(chatInput) < 120) { int len = strlen(chatInput); chatInput[len] = (char)c; chatInput[len+1] = '\0'; } c = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE)) { int len = strlen(chatInput); if (len > 0) chatInput[len-1] = '\0'; } } if (!inventoryOpen && !isChatting) { const float MOUSE_SENS = 0.002f; Vector2 md = GetMouseDelta(); camYaw -= md.x * MOUSE_SENS; camPitch -= md.y * MOUSE_SENS; if (camPitch > 1.5f) camPitch = 1.5f; if (camPitch < -1.5f) camPitch = -1.5f; } // Direction vectors (horizontal) Vector3 forward = { sinf(camYaw), 0, cosf(camYaw) }; Vector3 right = { cosf(camYaw), 0, -sinf(camYaw) }; Vector3 oldPos = camera3D.position; Vector3 moveVec = { 0, 0, 0 }; if (!inventoryOpen && !isChatting) { if (IsKeyDown(KEY_W)) moveVec = Vector3Add(moveVec, forward); if (IsKeyDown(KEY_S)) moveVec = Vector3Subtract(moveVec, forward); if (IsKeyDown(KEY_A)) moveVec = Vector3Add(moveVec, right); if (IsKeyDown(KEY_D)) moveVec = Vector3Subtract(moveVec, right); } if (Vector3Length(moveVec) > 0) { moveVec = Vector3Normalize(moveVec); float speed = (isFlying ? 15.0f : 5.0f) * GetFrameTime(); Vector3 tryX = { oldPos.x + moveVec.x * speed, oldPos.y, oldPos.z }; if (isFlying || !CheckPlayerCollision(tryX)) camera3D.position.x = tryX.x; Vector3 tryZ = { camera3D.position.x, oldPos.y, oldPos.z + moveVec.z * speed }; if (isFlying || !CheckPlayerCollision(tryZ)) camera3D.position.z = tryZ.z; } // ---- Vertical Physics (Ground-Lock / Flight System) ---- if (isFlying) { playerVelocityY = 0.0f; isGrounded = false; float flySpeed = 10.0f * GetFrameTime(); if (!inventoryOpen && !isChatting) { if (IsKeyDown(KEY_SPACE)) camera3D.position.y += flySpeed; if (IsKeyDown(KEY_LEFT_SHIFT)) camera3D.position.y -= flySpeed; } } else if (isGrounded) { playerVelocityY = 0.0f; // Keep player exactly on top of the block with a small epsilon float feetY = camera3D.position.y - 1.6f; float expectedFeetY = floorf(feetY + 0.15f) + 0.5f; camera3D.position.y = expectedFeetY + 1.6f + 0.03f; // Jump if (IsKeyPressed(KEY_SPACE) && !inventoryOpen && !isChatting) { playerVelocityY = 8.5f; isGrounded = false; } else { // Check if we walked off an edge (use a generous 0.2f margin) Vector3 checkBelow = camera3D.position; checkBelow.y -= 0.2f; if (!CheckPlayerCollision(checkBelow)) isGrounded = false; } } else { // Falling / Jumping playerVelocityY -= 25.0f * GetFrameTime(); float dy = playerVelocityY * GetFrameTime(); Vector3 nextYPos = camera3D.position; nextYPos.y += dy; if (CheckPlayerCollision(nextYPos)) { if (playerVelocityY < 0.0f) { // Landed: Lock to surface float feetY = nextYPos.y - 1.6f; camera3D.position.y = floorf(feetY + 0.15f) + 0.5f + 1.6f + 0.03f; playerVelocityY = 0.0f; isGrounded = true; } else { // Hit ceiling camera3D.position.y = ceilf(nextYPos.y + 0.1f) - 0.5f - 0.41f; playerVelocityY = 0.0f; } } else { camera3D.position.y = nextYPos.y; isGrounded = false; } } // Resolve any lingering overlap to prevent getting stuck (Iterative push-out) if (!isFlying) { for (int i = 0; i < 5 && CheckPlayerCollision(camera3D.position); i++) { camera3D.position.y += 0.05f; } } // Final Camera state camera3D.target.x = camera3D.position.x + sinf(camYaw) * cosf(camPitch); camera3D.target.y = camera3D.position.y + sinf(camPitch); camera3D.target.z = camera3D.position.z + cosf(camYaw) * cosf(camPitch); camera3D.up = (Vector3){ 0, 1, 0 }; // Block Raycasting (moved outside to update every frame for wireframe) hitBlock = false; if (!inventoryOpen && !isChatting) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { isSwinging = true; swingTime = 0.0f; // PvP Attack Raycast Ray ray = GetMouseRay((Vector2){ (float)currentWidth / 2, (float)currentHeight / 2 }, camera3D); for (auto& rp : remotePlayers) { BoundingBox pBox = { (Vector3){ rp.position.x - 0.4f, rp.position.y, rp.position.z - 0.4f }, (Vector3){ rp.position.x + 0.4f, rp.position.y + 1.8f, rp.position.z + 0.4f } }; RayCollision rCol = GetRayCollisionBox(ray, pBox); if (rCol.hit && rCol.distance < 4.5f) { PacketHeader hitH = { (uint8_t)PACKET_PLAYER_HIT, (uint32_t)sizeof(PacketPlayerHit) }; PacketPlayerHit hitD = { rp.id, 1.0f, camera3D.position.x, camera3D.position.z }; if (clientSocket != INVALID_SOCKET_VAL) { SendAll(clientSocket, (char*)&hitH, sizeof(hitH)); SendAll(clientSocket, (char*)&hitD, sizeof(hitD)); } else if (serverMode) { for (auto& s : clientSockets) { if (s == (Socket)rp.id) { SendAll(s, (char*)&hitH, sizeof(hitH)); SendAll(s, (char*)&hitD, sizeof(hitD)); } } } break; } } } Ray ray = GetMouseRay((Vector2){ (float)currentWidth / 2, (float)currentHeight / 2 }, camera3D); float closestDist = 8.0f; int startX = (int)floorf(camera3D.position.x - 8); int endX = (int)floorf(camera3D.position.x + 8); int startY = (int)floorf(camera3D.position.y - 8); int endY = (int)floorf(camera3D.position.y + 8); int startZ = (int)floorf(camera3D.position.z - 8); int endZ = (int)floorf(camera3D.position.z + 8); for (int x = startX; x <= endX; x++) { for (int y = startY; y <= endY; y++) { for (int z = startZ; z <= endZ; z++) { if (GetBlock(x, y, z) != AIR) { BoundingBox box = { (Vector3){ x - 0.5f, y - 0.5f, z - 0.5f }, (Vector3){ x + 0.5f, y + 0.5f, z + 0.5f } }; RayCollision collision = GetRayCollisionBox(ray, box); if (collision.hit && collision.distance < closestDist) { closestDist = collision.distance; closestNormal = collision.normal; hitBlock = true; hitX = x; hitY = y; hitZ = z; } } } } } if (hitBlock) { if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { if (hitX != lastHitX || hitY != lastHitY || hitZ != lastHitZ) { breakProgress = 0.0f; lastHitX = hitX; lastHitY = hitY; lastHitZ = hitZ; } int targetBlock = GetBlock(hitX, hitY, hitZ); if (targetBlock != AIR && targetBlock != BEDROCK) { float breakSpeed = 2.0f; // Seconds to break if (targetBlock == LOG || targetBlock == PLANK || targetBlock == CRAFTING_TABLE) { if (hotbar[activeHotbarSlot].blockType == WOOD_AXE) breakSpeed = 0.5f; // Fast else breakSpeed = 1.5f; // Normal } else if (targetBlock == LEAVES) { if (hotbar[activeHotbarSlot].blockType == WOOD_SWORD) breakSpeed = 1.4f; // 4 hits (0.35s per swing) else breakSpeed = 2.1f; // 6 hits } breakProgress += GetFrameTime(); // Digging sound timer digSoundTimer += GetFrameTime(); if (digSoundTimer >= 0.35f) { Sound* s = &digGrass; if (targetBlock == LOG || targetBlock == PLANK) s = &digWood; else if (targetBlock == STONE || targetBlock == COBBLESTONE) s = &digStone; else if (targetBlock == SAND) s = &digSand; PlaySound(*s); digSoundTimer = 0.0f; isSwinging = true; } if (breakProgress >= breakSpeed) { if (targetBlock == LEAVES) { // 1 in 20 chance for an apple if (GetRandomValue(0, 19) == 0) { droppedItems.push_back({(Vector3){(float)hitX, (float)hitY, (float)hitZ}, APPLE, true}); } } else { AddToInventory(targetBlock); } NetSetBlock(hitX, hitY, hitZ, AIR); // Tool Durability Logic (v2.3.3) InventorySlot* slot = &hotbar[activeHotbarSlot]; if (slot->maxDurability > 0) { slot->durability--; if (slot->durability <= 0) { PlaySound(toolBreakSound); *slot = InventorySlot(AIR, 0); } } breakProgress = 0.0f; isSwinging = true; // Swing when finishing } // Visual swing while mining if (fmodf(breakProgress, 0.4f) < 0.1f) isSwinging = true; } } else { breakProgress = 0.0f; } if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { // NEW: Eating Logic if (hotbar[activeHotbarSlot].blockType == APPLE && playerHunger < 20.0f) { playerHunger += 4.0f; if (playerHunger > 20.0f) playerHunger = 20.0f; hotbar[activeHotbarSlot].count--; if (hotbar[activeHotbarSlot].count <= 0) hotbar[activeHotbarSlot].blockType = AIR; PlaySound(digGrass); } else if (hitBlock) { int targetBlock = GetBlock(hitX, hitY, hitZ); if (targetBlock == CRAFTING_TABLE) { currentState = CRAFTING_GUI; EnableCursor(); } else if (targetBlock == CHEST) { activeChestPos = (Vector3){ (float)hitX, (float)hitY, (float)hitZ }; uint64_t key = GetPosKey(hitX, hitY, hitZ); if (chestInventories.find(key) == chestInventories.end()) { chestInventories[key] = std::vector(27, InventorySlot(AIR, 0)); } currentState = CHEST_GUI; PlaySound(chestOpenSound); EnableCursor(); } else if (hotbar[activeHotbarSlot].count > 0) { int placeX = hitX + (int)closestNormal.x; int placeY = hitY + (int)closestNormal.y; int placeZ = hitZ + (int)closestNormal.z; BoundingBox playerBB = { (Vector3){camera3D.position.x - 0.3f, camera3D.position.y - 1.5f, camera3D.position.z - 0.3f}, (Vector3){camera3D.position.x + 0.3f, camera3D.position.y + 0.3f, camera3D.position.z + 0.3f} }; BoundingBox blockBB = { (Vector3){(float)placeX - 0.5f, (float)placeY - 0.5f, (float)placeZ - 0.5f}, (Vector3){(float)placeX + 0.5f, (float)placeY + 0.5f, (float)placeZ + 0.5f} }; if (!CheckCollisionBoxes(playerBB, blockBB) || hotbar[activeHotbarSlot].blockType == TORCH) { NetSetBlock(placeX, placeY, placeZ, hotbar[activeHotbarSlot].blockType); hotbar[activeHotbarSlot].count--; if (hotbar[activeHotbarSlot].count <= 0) hotbar[activeHotbarSlot].blockType = AIR; } isSwinging = true; } } } } } if (IsKeyPressed(KEY_ESCAPE)) { if (inventoryOpen) { inventoryOpen = false; DisableCursor(); } else { currentState = PAUSE_MENU; EnableCursor(); } } } // Text Input Logic if (currentState == CREATE_WORLD_MENU) { if (activeTextBox > 0) { int key = GetCharPressed(); while (key > 0) { if ((key >= 32) && (key <= 125)) { // printable characters if (activeTextBox == 1 && worldNameLen < 63) { worldName[worldNameLen] = (char)key; worldName[worldNameLen + 1] = '\0'; worldNameLen++; } else if (activeTextBox == 2 && worldSeedLen < 63) { worldSeed[worldSeedLen] = (char)key; worldSeed[worldSeedLen + 1] = '\0'; worldSeedLen++; } } key = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE)) { if (activeTextBox == 1 && worldNameLen > 0) { worldNameLen--; worldName[worldNameLen] = '\0'; } else if (activeTextBox == 2 && worldSeedLen > 0) { worldSeedLen--; worldSeed[worldSeedLen] = '\0'; } } } } //---------------------------------------------------------------------------------- // Draw //---------------------------------------------------------------------------------- BeginDrawing(); ClearBackground(BLACK); Vector2 mousePos = GetMousePosition(); if (currentState == CHECKING_UPDATES) { updateTimer += GetFrameTime(); DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 20, 20, 20, 255 }); DrawTextEx(customFont, "CHECKING FOR UPDATES...", (Vector2){ (float)currentWidth/2 - 150, (float)currentHeight/2 - 20 }, 24, 1.0f, WHITE); // Perform real check after 1 second if (updateTimer > 1.0f && latestVersion == "") { latestVersion = GetRemoteVersion(); if (IsVersionNewer(latestVersion, localVersion)) { updateReady = true; } } if (updateTimer > 2.0f) { if (updateReady && !ignoreUpdateThisSession) currentState = UPDATE_FOUND; else currentState = MAIN_MENU; } } else if (currentState == UPDATE_FOUND) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 20, 20, 20, 255 }); DrawTextEx(customFont, "AN UPDATE IS AVAILABLE!", (Vector2){ (float)currentWidth/2 - 180, (float)currentHeight/2 - 80 }, 28, 1.0f, YELLOW); DrawTextEx(customFont, TextFormat("Latest: %s", latestVersion.c_str()), (Vector2){ (float)currentWidth/2 - 100, (float)currentHeight/2 - 40 }, 24, 1.0f, WHITE); Rectangle updateBtn = { (float)currentWidth/2 - 210, (float)currentHeight/2 + 20, 200, 50 }; Rectangle ignoreBtn = { (float)currentWidth/2 + 10, (float)currentHeight/2 + 20, 200, 50 }; bool hUpdate = CheckCollisionPointRec(mousePos, updateBtn); bool hIgnore = CheckCollisionPointRec(mousePos, ignoreBtn); DrawRectangleRec(updateBtn, hUpdate ? GRAY : DARKGRAY); DrawRectangleRec(ignoreBtn, hIgnore ? GRAY : DARKGRAY); DrawRectangleLinesEx(updateBtn, 2.0f, WHITE); DrawRectangleLinesEx(ignoreBtn, 2.0f, WHITE); DrawTextEx(customFont, "UPDATE NOW", (Vector2){ updateBtn.x + 35, updateBtn.y + 15 }, 20, 1.0f, WHITE); DrawTextEx(customFont, "IGNORE", (Vector2){ ignoreBtn.x + 65, ignoreBtn.y + 15 }, 20, 1.0f, WHITE); if (hUpdate && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) currentState = DOWNLOADING_UPDATE; if (hIgnore && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { ignoreUpdateThisSession = true; currentState = MAIN_MENU; } } else if (currentState == DOWNLOADING_UPDATE) { if (!startedDownload) { startedDownload = true; currentProgress = 0.0f; std::thread([&]() { std::string binaryUrl, binaryName; #ifdef _WIN32 binaryUrl = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/release/MorriCraft-Windows.zip"; binaryName = "MorriCraft-Windows.zip"; #else binaryUrl = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/release/MorriCraft-Linux.zip"; binaryName = "MorriCraft-Linux.zip"; #endif std::string versionUrl = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/release/version.txt"; long remoteSize = GetRemoteFileSize(binaryUrl); bool binaryDone = false; // Start download in background std::thread([&, binaryName, binaryUrl, &binaryDone]() { system(("curl -L -s -o " + binaryName + " \"" + binaryUrl + "\"").c_str()); binaryDone = true; }).detach(); // Monitor progress while (!binaryDone) { if (remoteSize > 0) { long localSize = GetLocalFileSize(binaryName); currentProgress = (float)localSize / (float)remoteSize * 0.95f; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } currentProgress = 0.96f; // Download version int r2 = system(("curl -L -s -o version.txt.new \"" + versionUrl + "\"").c_str()); currentProgress = 1.0f; if (r2 == 0) { // Switch to extraction phase currentState = EXTRACTING_UPDATE; isInstalling = true; currentProgress = 0.0f; // Reset for extraction phase #ifdef _WIN32 // Windows Update: Move exe, unzip assets system("move MorriCraft.exe MorriCraft.old >nul 2>&1"); currentProgress = 0.3f; system(("powershell -Command \"Expand-Archive -Path " + binaryName + " -DestinationPath . -Force\"").c_str()); currentProgress = 0.8f; system("move version.txt.new version.txt >nul 2>&1"); system("copy version.txt assets\\version.txt >nul 2>&1"); system(("del " + binaryName).c_str()); #else // Linux Update: Move binary, unzip assets system("mv MorriCraft MorriCraft.old 2>/dev/null"); currentProgress = 0.3f; system(("unzip -o " + binaryName).c_str()); currentProgress = 0.8f; system("chmod +x MorriCraft"); system("mv version.txt.new version.txt 2>/dev/null"); system("cp version.txt assets/version.txt 2>/dev/null"); system(("rm " + binaryName).c_str()); #endif currentProgress = 1.0f; downloadFinished = true; } else { downloadFailed = true; } }).detach(); } DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 20, 20, 20, 255 }); if (downloadFailed) { DrawTextEx(customFont, "DOWNLOAD FAILED! Check internet connection.", (Vector2){ (float)currentWidth/2 - 250, (float)currentHeight/2 - 40 }, 24, 1.0f, RED); Rectangle backBtn = { (float)currentWidth/2 - 100, (float)currentHeight/2 + 20, 200, 40 }; if (CheckCollisionPointRec(mousePos, backBtn) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentState = MAIN_MENU; startedDownload = false; downloadFailed = false; } DrawRectangleRec(backBtn, DARKGRAY); DrawTextEx(customFont, "BACK", (Vector2){ backBtn.x + 70, backBtn.y + 10 }, 20, 1.0f, WHITE); } else { DrawTextEx(customFont, "Downloading update...", (Vector2){ (float)currentWidth/2 - 90, (float)currentHeight/2 - 60 }, 24, 1.0f, YELLOW); // v2.3.3: Removed dynamic pulse to fix jitter int bw = 500, bh = 35; Rectangle barBg = { (float)currentWidth/2 - bw/2, (float)currentHeight/2 - bh/2, (float)bw, (float)bh }; DrawRectangleRec(barBg, BLACK); DrawRectangle(barBg.x, barBg.y, (int)(bw * currentProgress), bh, GREEN); DrawRectangleLinesEx(barBg, 2.0f, GRAY); DrawTextEx(customFont, TextFormat("%i%%", (int)(currentProgress * 100)), (Vector2){ barBg.x + bw + 15, barBg.y + 5 }, 20, 1.0f, WHITE); } } else if (currentState == EXTRACTING_UPDATE) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 20, 20, 20, 255 }); DrawTextEx(customFont, "Extracting and Installing Update...", (Vector2){ (float)currentWidth/2 - 150, (float)currentHeight/2 - 60 }, 24, 1.0f, GREEN); int bw = 500, bh = 35; Rectangle barBg = { (float)currentWidth/2 - bw/2, (float)currentHeight/2 - bh/2, (float)bw, (float)bh }; DrawRectangleRec(barBg, BLACK); // We use the thread's progress for the extraction bar too DrawRectangle(barBg.x, barBg.y, (int)(bw * 1.0f), bh, (Color){0, 228, 48, 150}); // Faded green background DrawRectangle(barBg.x, barBg.y, (int)(bw * 1.0f), bh, GREEN); DrawRectangleLinesEx(barBg, 2.0f, GRAY); DrawTextEx(customFont, "Applying files...", (Vector2){ barBg.x, barBg.y + bh + 10 }, 18, 1.0f, GRAY); if (downloadFinished) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 230 }); int pw = 500, ph = 220; Rectangle pBox = { (float)currentWidth/2 - pw/2, (float)currentHeight/2 - ph/2, (float)pw, (float)ph }; DrawRectangleRec(pBox, (Color){ 30, 30, 30, 255 }); DrawRectangleLinesEx(pBox, 4.0f, GREEN); DrawTextEx(customFont, "UPDATE SUCCESSFUL!", (Vector2){ pBox.x + 80, pBox.y + 40 }, 28, 1.0f, GREEN); DrawTextEx(customFont, "THE GAME NEEDS TO RESTART TO APPLY.", (Vector2){ pBox.x + 60, pBox.y + 90 }, 20, 1.0f, WHITE); Rectangle restartBtn = { pBox.x + pw/2 - 90, pBox.y + 145, 180, 45 }; bool hRestart = CheckCollisionPointRec(mousePos, restartBtn); DrawRectangleRec(restartBtn, hRestart ? GREEN : DARKGREEN); DrawTextEx(customFont, "RESTART", (Vector2){ restartBtn.x + 45, restartBtn.y + 12 }, 20, 1.0f, WHITE); if (hRestart && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { #ifdef _WIN32 system("start MorriCraft.exe"); #else system("./MorriCraft &"); #endif exit(0); } } } else if (currentState == CHEAT_GUI) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){0, 0, 0, 200}); DrawTextEx(customFont, "CHEAT MENU (Press E to Close)", (Vector2){20, 20}, 24, 1.0f, WHITE); // Grid of all blocks for(int i = 0; i < 64; i++) { if (blockTextures[i].id == 0) continue; int px = 50 + (i % 10) * 60; int py = 80 + (i / 10) * 60; Rectangle r = {(float)px, (float)py, 50, 50}; DrawRectangleRec(r, GRAY); DrawTexturePro(blockTextures[i], (Rectangle){0,0,(float)blockTextures[i].width, (float)blockTextures[i].height}, (Rectangle){(float)px+5,(float)py+5,40,40}, (Vector2){0,0}, 0.0f, WHITE); if (CheckCollisionPointRec(mousePos, r) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { AddToInventory(i); } } } else if (currentState == SKIN_EDITOR) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 20, 20, 20, 255 }); int panelWidth = 700; int panelHeight = 500; int panelX = (currentWidth / 2) - (panelWidth / 2); int panelY = (currentHeight / 2) - (panelHeight / 2); DrawRectangle(panelX, panelY, panelWidth, panelHeight, (Color){ 40, 40, 40, 240 }); DrawRectangleLinesEx((Rectangle){ (float)panelX, (float)panelY, (float)panelWidth, (float)panelHeight }, 4.0f, BLUE); DrawTextEx(customFont, "Skin Editor", (Vector2){ (float)panelX + 20, (float)panelY + 20 }, 32, 1.0f, WHITE); // Preview Model (3D) static float previewRot = 0.0f; previewRot += GetFrameTime() * 30.0f; Camera3D previewCam = { 0 }; previewCam.position = (Vector3){ 3.0f, 2.0f, 3.0f }; previewCam.target = (Vector3){ 0.0f, 1.0f, 0.0f }; previewCam.up = (Vector3){ 0.0f, 1.0f, 0.0f }; previewCam.fovy = 45.0f; previewCam.projection = CAMERA_PERSPECTIVE; BeginMode3D(previewCam); rlPushMatrix(); rlRotatef(previewRot, 0, 1, 0); // Render Humanoid DrawCube((Vector3){0, 0.7f, 0}, 0.6f, 0.9f, 0.3f, myShirtColor); DrawCube((Vector3){0, 1.4f, 0}, 0.45f, 0.45f, 0.45f, (Color){220, 180, 150, 255}); DrawCube((Vector3){-0.45f, 0.7f, 0}, 0.2f, 0.8f, 0.2f, (Color){200, 160, 130, 255}); DrawCube((Vector3){0.45f, 0.7f, 0}, 0.2f, 0.8f, 0.2f, (Color){200, 160, 130, 255}); DrawCube((Vector3){-0.15f, 0.15f, 0}, 0.25f, 0.4f, 0.25f, myPantsColor); DrawCube((Vector3){0.15f, 0.15f, 0}, 0.25f, 0.4f, 0.25f, myPantsColor); rlPopMatrix(); EndMode3D(); // Color Pickers Color shirtPresets[] = { RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, WHITE, BLACK }; Color pantsPresets[] = { DARKBLUE, BROWN, DARKGRAY, BLACK, DARKGREEN, MAROON }; DrawTextEx(customFont, "Shirt Color", (Vector2){ (float)panelX + 400, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY); for (int i = 0; i < 8; i++) { Rectangle r = { (float)panelX + 400 + (i % 4) * 50, (float)panelY + 130 + (i / 4) * 50, 40, 40 }; DrawRectangleRec(r, shirtPresets[i]); if (CheckCollisionPointRec(mousePos, r) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) myShirtColor = shirtPresets[i]; if (myShirtColor.r == shirtPresets[i].r && myShirtColor.g == shirtPresets[i].g && myShirtColor.b == shirtPresets[i].b) DrawRectangleLinesEx(r, 2.0f, WHITE); } DrawTextEx(customFont, "Pants Color", (Vector2){ (float)panelX + 400, (float)panelY + 250 }, 20, 1.0f, LIGHTGRAY); for (int i = 0; i < 6; i++) { Rectangle r = { (float)panelX + 400 + (i % 3) * 50, (float)panelY + 280 + (i / 3) * 50, 40, 40 }; DrawRectangleRec(r, pantsPresets[i]); if (CheckCollisionPointRec(mousePos, r) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) myPantsColor = pantsPresets[i]; if (myPantsColor.r == pantsPresets[i].r && myPantsColor.g == pantsPresets[i].g && myPantsColor.b == pantsPresets[i].b) DrawRectangleLinesEx(r, 2.0f, WHITE); } Rectangle doneBtn = { (float)panelX + panelWidth - 150, (float)panelY + panelHeight - 60, 120, 40 }; if (CheckCollisionPointRec(mousePos, doneBtn) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { SaveConfig(); currentState = OPTIONS_MENU; } DrawRectangleRec(doneBtn, GREEN); DrawTextEx(customFont, "SAVE", (Vector2){ doneBtn.x + 35, doneBtn.y + 10 }, 20, 1.0f, WHITE); } else if (currentState == CONNECT_MENU) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 220 }); int cw = 500, ch = 350; Rectangle cBox = { (float)currentWidth/2 - cw/2, (float)currentHeight/2 - ch/2, (float)cw, (float)ch }; DrawRectangleRec(cBox, (Color){ 30, 30, 30, 255 }); DrawRectangleLinesEx(cBox, 4.0f, DARKGRAY); DrawTextEx(customFont, "DIRECT CONNECT", (Vector2){ cBox.x + 110, cBox.y + 30 }, 28, 1.0f, WHITE); // IP Address Input DrawTextEx(customFont, "Server IP Address:", (Vector2){ cBox.x + 50, cBox.y + 90 }, 20, 1.0f, LIGHTGRAY); Rectangle ipRect = { cBox.x + 50, cBox.y + 120, 400, 40 }; bool isIPHovered = CheckCollisionPointRec(mousePos, ipRect); DrawRectangleRec(ipRect, (activeNetField == 1) ? BLACK : (isIPHovered ? (Color){ 50, 50, 50, 255 } : (Color){ 40, 40, 40, 255 })); DrawRectangleLinesEx(ipRect, 2, (activeNetField == 1) ? GREEN : GRAY); DrawTextEx(customFont, targetIP, (Vector2){ ipRect.x + 10, ipRect.y + 10 }, 20, 1.0f, WHITE); if (isIPHovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) activeNetField = 1; // Port Input DrawTextEx(customFont, "Server Port:", (Vector2){ cBox.x + 50, cBox.y + 180 }, 20, 1.0f, LIGHTGRAY); Rectangle portRect = { cBox.x + 50, cBox.y + 210, 150, 40 }; bool isPortHovered = CheckCollisionPointRec(mousePos, portRect); DrawRectangleRec(portRect, (activeNetField == 2) ? BLACK : (isPortHovered ? (Color){ 50, 50, 50, 255 } : (Color){ 40, 40, 40, 255 })); DrawRectangleLinesEx(portRect, 2, (activeNetField == 2) ? GREEN : GRAY); DrawTextEx(customFont, targetPort, (Vector2){ portRect.x + 10, portRect.y + 10 }, 20, 1.0f, WHITE); if (isPortHovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) activeNetField = 2; // Input handling for IP/Port int key = GetCharPressed(); while (key > 0) { if (activeNetField == 1 && strlen(targetIP) < 127) { int len = strlen(targetIP); targetIP[len] = (char)key; targetIP[len+1] = '\0'; } else if (activeNetField == 2 && strlen(targetPort) < 15) { int len = strlen(targetPort); targetPort[len] = (char)key; targetPort[len+1] = '\0'; } key = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE)) { if (activeNetField == 1) { int len = strlen(targetIP); if (len > 0) targetIP[len-1] = '\0'; } else if (activeNetField == 2) { int len = strlen(targetPort); if (len > 0) targetPort[len-1] = '\0'; } } // Server Mode Toggle (Moved from Options) Rectangle serverCheck = { cBox.x + 50, cBox.y + 260, 20, 20 }; bool isServerHovered = CheckCollisionPointRec(mousePos, serverCheck); DrawRectangleRec(serverCheck, serverMode ? GREEN : DARKGRAY); DrawRectangleLinesEx(serverCheck, 2.0f, isServerHovered ? WHITE : GRAY); DrawTextEx(customFont, "HOST SERVER MODE", (Vector2){ serverCheck.x + 35, serverCheck.y }, 18, 1.0f, serverMode ? GREEN : WHITE); if (isServerHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { serverMode = !serverMode; SaveConfig(); } // Join Button Logic Rectangle joinBtn = { cBox.x + 50, cBox.y + 300, 180, 45 }; bool isJoinHovered = CheckCollisionPointRec(mousePos, joinBtn); DrawRectangleRec(joinBtn, isJoinHovered ? GREEN : (Color){ 0, 120, 0, 255 }); DrawTextEx(customFont, "JOIN SERVER", (Vector2){ joinBtn.x + 25, joinBtn.y + 12 }, 20, 1.0f, WHITE); if (isJoinHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { clientSocket = socket(AF_INET, SOCK_STREAM, 0); if (clientSocket != INVALID_SOCKET_VAL) { SetNonBlocking(clientSocket); struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(atoi(targetPort)); inet_pton(AF_INET, targetIP, &serv_addr.sin_addr); connect(clientSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); isConnecting = true; } } // Cancel Button Rectangle cancelBtn = { cBox.x + 270, cBox.y + 300, 180, 45 }; bool isCancelHovered = CheckCollisionPointRec(mousePos, cancelBtn); DrawRectangleRec(cancelBtn, isCancelHovered ? RED : (Color){ 120, 0, 0, 255 }); DrawTextEx(customFont, "CANCEL", (Vector2){ cancelBtn.x + 55, cancelBtn.y + 12 }, 20, 1.0f, WHITE); if (isCancelHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentState = MAIN_MENU; activeNetField = 0; } } else if (currentState == WORLD_CREATION_PROGRESS) { // UPDATE: Generate 10 chunks per frame (faster for the larger area) int spiralSize = 25; int spawnCX = (int)floorf(spawnSavedX / CHUNK_SIZE); int spawnCZ = (int)floorf(spawnSavedZ / CHUNK_SIZE); for (int i = 0; i < 20 && chunksGeneratedCount < totalChunksToPreGen; i++) { int cx_off = (chunksGeneratedCount % spiralSize) - 12; int cz_off = (chunksGeneratedCount / spiralSize) - 12; GenerateChunk(spawnCX + cx_off, spawnCZ + cz_off); chunksGeneratedCount++; } worldGenProgress = (float)chunksGeneratedCount / (float)totalChunksToPreGen; if (chunksGeneratedCount >= totalChunksToPreGen) { // FINALIZE: Place player and save world data if (isNewWorldGeneration) { float spawnY = FindSpawnY((int)camera3D.position.x, (int)camera3D.position.z); camera3D.position.y = spawnY; camera3D.target = (Vector3){ camera3D.position.x, spawnY, camera3D.position.z + 1.0f }; std::ofstream worldFile2("saves/" + currentWorldName + "/world.dat"); if (worldFile2.is_open()) { worldFile2 << globalSeedHash << " " << camera3D.position.x << " " << camera3D.position.y << " " << camera3D.position.z; worldFile2.close(); } for (int i = 0; i < 9; i++) hotbar[i] = InventorySlot(AIR, 0); for (int i = 0; i < 27; i++) inventory[i] = InventorySlot(AIR, 0); playerHunger = 20.0f; // Start with full hunger } else { // Restoration logic for existing worlds float spawnY = (spawnSavedY > 0) ? spawnSavedY : FindSpawnY((int)spawnSavedX, (int)spawnSavedZ); camera3D.position = (Vector3){ spawnSavedX, spawnY, spawnSavedZ }; camera3D.target = (Vector3){ spawnSavedX, spawnY, spawnSavedZ + 1.0f }; std::ifstream invf("saves/" + currentWorldName + "/inventory.dat", std::ios::binary); if (invf.is_open()) { invf.read((char*)hotbar, sizeof(hotbar)); invf.read((char*)inventory, sizeof(inventory)); invf.read((char*)&activeHotbarSlot, sizeof(activeHotbarSlot)); invf.close(); } } currentState = GAMEPLAY; DisableCursor(); } // DRAW: Progress UI DrawRectangle(0, 0, currentWidth, currentHeight, BLACK); int barWidth = 400; int barHeight = 40; int barX = currentWidth / 2 - barWidth / 2; int barY = currentHeight / 2 - barHeight / 2; DrawTextEx(customFont, isNewWorldGeneration ? "Creating World..." : "Loading World...", (Vector2){ (float)barX, (float)barY - 40 }, 24, 1.0f, WHITE); // Background bar DrawRectangle(barX, barY, barWidth, barHeight, DARKGRAY); // Progress bar DrawRectangle(barX, barY, (int)(barWidth * worldGenProgress), barHeight, GREEN); // Border DrawRectangleLinesEx((Rectangle){ (float)barX, (float)barY, (float)barWidth, (float)barHeight }, 2, WHITE); char progText[32]; snprintf(progText, 32, "%d%%", (int)(worldGenProgress * 100)); Vector2 textSize = MeasureTextEx(customFont, progText, 20, 1.0f); DrawTextEx(customFont, progText, (Vector2){ (float)(barX + barWidth / 2 - textSize.x / 2), (float)(barY + barHeight / 2 - textSize.y / 2) }, 20, 1.0f, WHITE); } else if (currentState != GAMEPLAY) { BeginMode2D(camera); // Draw the texture, scaling it to fit the current window size exactly Rectangle sourceRec = { 0.0f, 0.0f, (float)titleTexture.width, (float)titleTexture.height }; Rectangle destRec = { (float)currentWidth/2.0f, (float)currentHeight/2.0f, (float)currentWidth, (float)currentHeight }; Vector2 origin = { (float)currentWidth/2.0f, (float)currentHeight/2.0f }; DrawTexturePro(titleTexture, sourceRec, destRec, origin, 0.0f, WHITE); EndMode2D(); // Show Version Number (v2.3.3) in Red DrawTextEx(customFont, "v2.3.3", (Vector2){ 20, (float)currentHeight - 30 }, 22, 1.0f, RED); // --- PLAYER NAME POPUP (IF MISSING) --- if (playerName == "") { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 200 }); int nw = 400, nh = 200; Rectangle nBox = { (float)currentWidth/2 - nw/2, (float)currentHeight/2 - nh/2, (float)nw, (float)nh }; DrawRectangleRec(nBox, (Color){ 40, 40, 40, 255 }); DrawRectangleLinesEx(nBox, 2.0f, WHITE); DrawTextEx(customFont, "Enter Your Name", (Vector2){ nBox.x + 20, nBox.y + 20 }, 24, 1.0f, WHITE); static char inputName[32] = {0}; int c = GetCharPressed(); while (c > 0) { if (c >= 32 && c <= 125 && strlen(inputName) < 30) { int len = strlen(inputName); inputName[len] = (char)c; inputName[len+1] = '\0'; } c = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE)) { int len = strlen(inputName); if (len > 0) inputName[len-1] = '\0'; } DrawRectangle(nBox.x + 20, nBox.y + 60, nw - 40, 40, BLACK); DrawTextEx(customFont, inputName, (Vector2){ nBox.x + 30, nBox.y + 70 }, 20, 1.0f, WHITE); if (IsKeyPressed(KEY_ENTER) && strlen(inputName) > 0) { playerName = inputName; SaveConfig(); } } } if (currentState == MAIN_MENU) { targetZoom = 1.1f; // ONLY show buttons if name is entered if (playerName != "") { // UI Buttons const char* buttons[] = { "Create World", "Load World", "Connect", "Options" }; int numButtons = 4; int buttonWidth = 400; int buttonHeight = 60; int buttonSpacing = 15; int fontSize = 24; // Calculate starting Y position to center buttons in the lower half of the screen int totalHeight = numButtons * buttonHeight + (numButtons - 1) * buttonSpacing; int startY = (currentHeight * 0.75f) - (totalHeight / 2); for (int i = 0; i < numButtons; i++) { int posX = (currentWidth / 2) - (buttonWidth / 2); int posY = startY + i * (buttonHeight + buttonSpacing); Rectangle btnBounds = { (float)posX, (float)posY, (float)buttonWidth, (float)buttonHeight }; bool isHovered = CheckCollisionPointRec(mousePos, btnBounds); // Draw Button Box Color baseColor = isHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 }; Color borderColor = isHovered ? (Color){ 200, 200, 200, 255 } : (Color){ 20, 20, 20, 255 }; DrawRectangleRec(btnBounds, baseColor); DrawRectangleLinesEx(btnBounds, 3.0f, borderColor); if (isHovered) { DrawRectangleLinesEx((Rectangle){btnBounds.x - 2, btnBounds.y - 2, btnBounds.width + 4, btnBounds.height + 4}, 2.0f, (Color){255, 255, 255, 50}); // Handle click if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { if (i == 0) { // Create World button currentState = CREATE_WORLD_MENU; } else if (i == 1) { // Load World button currentState = LOAD_WORLD_MENU; } else if (i == 2) { // Connect button currentState = CONNECT_MENU; } else if (i == 3) { // Options button optionsReturnState = MAIN_MENU; currentState = OPTIONS_MENU; } } } Vector2 textSize = MeasureTextEx(customFont, buttons[i], fontSize, 1.0f); int textPosX = posX + (buttonWidth / 2) - (textSize.x / 2); int textPosY = posY + (buttonHeight / 2) - (textSize.y / 2); DrawTextEx(customFont, buttons[i], (Vector2){ (float)textPosX + 2, (float)textPosY + 2 }, fontSize, 1.0f, (Color){ 30, 30, 30, 255 }); Color textColor = isHovered ? (Color){ 255, 255, 160, 255 } : (Color){ 220, 220, 220, 255 }; DrawTextEx(customFont, buttons[i], (Vector2){ (float)textPosX, (float)textPosY }, fontSize, 1.0f, textColor); } } } else if (currentState == LOAD_WORLD_MENU) { targetZoom = 1.0f; DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 }); // Scroll handling float scroll = GetMouseWheelMove(); loadWorldScrollOffset -= (int)scroll; if (loadWorldScrollOffset < 0) loadWorldScrollOffset = 0; int panelWidth = 640; // Wider int panelHeight = 520; // Taller int panelX = (currentWidth / 2) - (panelWidth / 2); int panelY = (currentHeight / 2) - (panelHeight / 2); DrawRectangle(panelX, panelY, panelWidth, panelHeight, (Color){ 40, 40, 40, 240 }); DrawRectangleLinesEx((Rectangle){ (float)panelX, (float)panelY, (float)panelWidth, (float)panelHeight }, 4.0f, (Color){ 100, 100, 100, 255 }); DrawTextEx(customFont, "Load World", (Vector2){ (float)panelX + 30, (float)panelY + 30 }, 32, 1.0f, WHITE); // List saves std::vector savedWorlds; if (std::filesystem::exists("saves")) { for (const auto& entry : std::filesystem::directory_iterator("saves")) { if (entry.is_directory()) { savedWorlds.push_back(entry.path().filename().string()); } } } if (savedWorlds.size() > 6) { if (loadWorldScrollOffset > (int)savedWorlds.size() - 6) loadWorldScrollOffset = (int)savedWorlds.size() - 6; } else { loadWorldScrollOffset = 0; } if (savedWorlds.empty()) { DrawTextEx(customFont, "No saved worlds found.", (Vector2){ (float)panelX + 40, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY); } else { // Draw scroll bar if (savedWorlds.size() > 6) { float barHeightFull = 360.0f; float scrollRatio = 6.0f / (float)savedWorlds.size(); float handleHeight = barHeightFull * scrollRatio; float scrollProgress = (float)loadWorldScrollOffset / (float)(savedWorlds.size() - 6); float handleY = panelY + 100 + (barHeightFull - handleHeight) * scrollProgress; DrawRectangle(panelX + panelWidth - 30, panelY + 100, 10, (int)barHeightFull, DARKGRAY); DrawRectangle(panelX + panelWidth - 30, (int)handleY, 10, (int)handleHeight, LIGHTGRAY); } for (size_t i = 0; i < 6 && (i + loadWorldScrollOffset) < savedWorlds.size(); i++) { size_t idx = i + loadWorldScrollOffset; Rectangle worldBtn = { (float)panelX + 40, (float)panelY + 100 + (float)(i * 65), 550, 55 }; Rectangle deleteBtn = { worldBtn.x + worldBtn.width - 50, worldBtn.y + 7, 40, 40 }; bool isDeleteHovered = CheckCollisionPointRec(mousePos, deleteBtn); bool isHovered = CheckCollisionPointRec(mousePos, worldBtn) && !isDeleteHovered; DrawRectangleRec(worldBtn, isHovered ? (Color){ 80, 80, 80, 255 } : DARKGRAY); DrawRectangleLinesEx(worldBtn, 2.0f, isHovered ? WHITE : GRAY); DrawTextEx(customFont, savedWorlds[idx].c_str(), (Vector2){ worldBtn.x + 15, worldBtn.y + 17 }, 20, 1.0f, WHITE); // Draw delete button DrawRectangleRec(deleteBtn, isDeleteHovered ? RED : MAROON); DrawRectangleLinesEx(deleteBtn, 2.0f, isDeleteHovered ? WHITE : GRAY); DrawTextEx(customFont, "X", (Vector2){ deleteBtn.x + 13, deleteBtn.y + 10 }, 20, 1.0f, WHITE); if (!showDeleteConfirm) { if (isDeleteHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { deletingWorldName = savedWorlds[idx]; showDeleteConfirm = true; } else if (isHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { // Load this world currentWorldName = savedWorlds[idx]; // Load seed and player position from world.dat std::ifstream worldFile("saves/" + currentWorldName + "/world.dat"); if (worldFile.is_open()) { worldFile >> globalSeedHash >> spawnSavedX >> spawnSavedY >> spawnSavedZ >> gameTime; worldFile.close(); } else { globalSeedHash = 12345; spawnSavedX = 0; spawnSavedY = -1; spawnSavedZ = 0; } // Load Chests (v2.3.3) chestInventories.clear(); std::ifstream cf("saves/" + currentWorldName + "/chests.dat", std::ios::binary); if (cf.is_open()) { uint32_t count; cf.read((char*)&count, 4); for (uint32_t i=0; i inv(27, InventorySlot(AIR, 0)); for (int j=0; j<27; j++) { cf.read((char*)&inv[j].blockType, 4); cf.read((char*)&inv[j].count, 4); cf.read((char*)&inv[j].durability, 4); } chestInventories[key] = inv; } cf.close(); } // Load Torches (v2.3.3) torchPositions.clear(); std::ifstream tf("saves/" + currentWorldName + "/torches.dat", std::ios::binary); if (tf.is_open()) { uint32_t count; tf.read((char*)&count, 4); torchPositions.resize(count); tf.read((char*)torchPositions.data(), count * sizeof(Vector3)); tf.close(); } // Clear old chunks for (auto& pair : worldChunks) delete pair.second; worldChunks.clear(); if (serverMode) { serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket != INVALID_SOCKET_VAL) { SetNonBlocking(serverSocket); struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(12345); bind(serverSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serverSocket, 5); } } // Start progress screen isNewWorldGeneration = false; chunksGeneratedCount = 0; worldGenProgress = 0.0f; currentState = WORLD_CREATION_PROGRESS; } } } } // Back Button int btnWidth = 220; // Slightly wider int btnHeight = 50; int backBtnX = panelX + (panelWidth / 2) - (btnWidth / 2); int btnY = panelY + panelHeight - 80; Rectangle backBtnBounds = { (float)backBtnX, (float)btnY, (float)btnWidth, (float)btnHeight }; bool isBackHovered = CheckCollisionPointRec(mousePos, backBtnBounds); DrawRectangleRec(backBtnBounds, isBackHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 }); DrawRectangleLinesEx(backBtnBounds, 3.0f, isBackHovered ? WHITE : GRAY); Vector2 backTextSize = MeasureTextEx(customFont, "Back", 20, 1.0f); DrawTextEx(customFont, "Back", (Vector2){ backBtnX + (btnWidth/2) - (backTextSize.x/2), btnY + (btnHeight/2) - (backTextSize.y/2) }, 20, 1.0f, WHITE); if (!showDeleteConfirm) { if (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentState = MAIN_MENU; } } // Draw Confirmation Overlay if (showDeleteConfirm) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 200 }); int confWidth = 650; int confHeight = 300; int confX = (currentWidth / 2) - (confWidth / 2); int confY = (currentHeight / 2) - (confHeight / 2); DrawRectangle(confX, confY, confWidth, confHeight, (Color){ 50, 20, 20, 255 }); Vector2 warnSize = MeasureTextEx(customFont, "Delete World?", 28, 1.0f); DrawTextEx(customFont, "Delete World?", (Vector2){ (float)(confX + (confWidth/2) - (warnSize.x/2)), (float)(confY + 30) }, 28, 1.0f, WHITE); std::string warnText = "Are you sure you want to delete '" + deletingWorldName + "'?"; Vector2 nameSize = MeasureTextEx(customFont, warnText.c_str(), 22, 1.0f); DrawTextEx(customFont, warnText.c_str(), (Vector2){ (float)(confX + (confWidth/2) - (nameSize.x/2)), (float)(confY + 85) }, 22, 1.0f, LIGHTGRAY); // Yes Button Rectangle yesBtn = { (float)confX + 80, (float)confY + 150, 140, 40 }; bool isYesHovered = CheckCollisionPointRec(mousePos, yesBtn); DrawRectangleRec(yesBtn, isYesHovered ? RED : MAROON); DrawRectangleLinesEx(yesBtn, 2.0f, isYesHovered ? WHITE : GRAY); DrawTextEx(customFont, "YES, DELETE", (Vector2){ yesBtn.x + 10, yesBtn.y + 10 }, 20, 1.0f, WHITE); // No Button Rectangle noBtn = { (float)confX + confWidth - 220, (float)confY + 150, 140, 40 }; bool isNoHovered = CheckCollisionPointRec(mousePos, noBtn); DrawRectangleRec(noBtn, isNoHovered ? (Color){ 100, 100, 100, 255 } : DARKGRAY); DrawRectangleLinesEx(noBtn, 2.0f, isNoHovered ? WHITE : GRAY); DrawTextEx(customFont, "NO", (Vector2){ noBtn.x + 45, noBtn.y + 10 }, 20, 1.0f, WHITE); if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { if (isYesHovered) { std::filesystem::remove_all("saves/" + deletingWorldName); showDeleteConfirm = false; } else if (isNoHovered) { showDeleteConfirm = false; } } } } else if (currentState == OPTIONS_MENU) { targetZoom = 1.0f; // Draw dark overlay DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 }); // Draw options panel int panelWidth = 600; int panelHeight = 480; // Increased from 400 int panelX = (currentWidth / 2) - (panelWidth / 2); int panelY = (currentHeight / 2) - (panelHeight / 2); DrawRectangle(panelX, panelY, panelWidth, panelHeight, (Color){ 40, 40, 40, 240 }); DrawRectangleLinesEx((Rectangle){ (float)panelX, (float)panelY, (float)panelWidth, (float)panelHeight }, 4.0f, (Color){ 100, 100, 100, 255 }); // Title DrawTextEx(customFont, "Options", (Vector2){ (float)panelX + 20, (float)panelY + 20 }, 32, 1.0f, WHITE); // Music Volume Slider DrawTextEx(customFont, "Music Volume", (Vector2){ (float)panelX + 40, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY); Rectangle musicSliderBar = { (float)panelX + 40, (float)panelY + 140, 520, 20 }; DrawRectangleRec(musicSliderBar, DARKGRAY); Rectangle musicHandle = { musicSliderBar.x + (masterMusicVolume * (musicSliderBar.width - 20)), musicSliderBar.y - 10, 20, 40 }; if (CheckCollisionPointRec(mousePos, (Rectangle){musicSliderBar.x, musicSliderBar.y - 20, musicSliderBar.width, 60})) { if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { masterMusicVolume = (mousePos.x - musicSliderBar.x) / (musicSliderBar.width - 20); if (masterMusicVolume < 0.0f) masterMusicVolume = 0.0f; if (masterMusicVolume > 1.0f) masterMusicVolume = 1.0f; } } DrawRectangleRec(musicHandle, LIGHTGRAY); DrawRectangleLinesEx(musicHandle, 2.0f, WHITE); // Sound Volume Slider DrawTextEx(customFont, "Sound Volume", (Vector2){ (float)panelX + 40, (float)panelY + 200 }, 20, 1.0f, LIGHTGRAY); Rectangle soundSliderBar = { (float)panelX + 40, (float)panelY + 240, 520, 20 }; DrawRectangleRec(soundSliderBar, DARKGRAY); Rectangle soundHandle = { soundSliderBar.x + (masterSoundVolume * (soundSliderBar.width - 20)), soundSliderBar.y - 10, 20, 40 }; if (CheckCollisionPointRec(mousePos, (Rectangle){soundSliderBar.x, soundSliderBar.y - 20, soundSliderBar.width, 60})) { if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { masterSoundVolume = (mousePos.x - soundSliderBar.x) / (soundSliderBar.width - 20); if (masterSoundVolume < 0.0f) masterSoundVolume = 0.0f; if (masterSoundVolume > 1.0f) masterSoundVolume = 1.0f; } } DrawRectangleRec(soundHandle, LIGHTGRAY); DrawRectangleLinesEx(soundHandle, 2.0f, WHITE); // Edit Skin Button Rectangle skinBtn = { (float)panelX + panelWidth/2 - 100, (float)panelY + 310, 200, 40 }; bool isSkinHovered = CheckCollisionPointRec(mousePos, skinBtn); DrawRectangleRec(skinBtn, isSkinHovered ? BLUE : DARKBLUE); DrawRectangleLinesEx(skinBtn, 2.0f, isSkinHovered ? WHITE : GRAY); DrawTextEx(customFont, "EDIT SKIN", (Vector2){ skinBtn.x + 55, skinBtn.y + 10 }, 20, 1.0f, WHITE); if (isSkinHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentState = SKIN_EDITOR; } // No Multiplayer options here anymore, moved to Connect Menu // Done Button int backBtnWidth = 200; int backBtnHeight = 50; int backBtnX = panelX + (panelWidth / 2) - (backBtnWidth / 2); int backBtnY = panelY + panelHeight - 80; Rectangle backBtnBounds = { (float)backBtnX, (float)backBtnY, (float)backBtnWidth, (float)backBtnHeight }; bool isBackHovered = CheckCollisionPointRec(mousePos, backBtnBounds); Color backBaseColor = isBackHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 }; DrawRectangleRec(backBtnBounds, backBaseColor); DrawRectangleLinesEx(backBtnBounds, 3.0f, isBackHovered ? WHITE : GRAY); Vector2 backTextSize = MeasureTextEx(customFont, "Done", 20, 1.0f); DrawTextEx(customFont, "Done", (Vector2){ backBtnX + (backBtnWidth/2) - (backTextSize.x/2), backBtnY + (backBtnHeight/2) - (backTextSize.y/2) }, 20, 1.0f, WHITE); if (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentState = optionsReturnState; } } else if (currentState == UPDATE_NOTES) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 200 }); int pw = 700, ph = 500; Rectangle pBox = { (float)currentWidth/2 - pw/2, (float)currentHeight/2 - ph/2, (float)pw, (float)ph }; DrawRectangleRec(pBox, (Color){ 40, 40, 40, 255 }); DrawRectangleLinesEx(pBox, 4.0f, DARKGRAY); DrawTextEx(customFont, "Update Notes", (Vector2){ pBox.x + 20, pBox.y + 20 }, 28, 1.0f, YELLOW); // Close Button (X) Rectangle xBtn = { pBox.x + pBox.width - 40, pBox.y + 10, 30, 30 }; bool isXHovered = CheckCollisionPointRec(mousePos, xBtn); DrawRectangleRec(xBtn, isXHovered ? RED : MAROON); DrawTextEx(customFont, "X", (Vector2){ xBtn.x + 8, xBtn.y + 5 }, 20, 1.0f, WHITE); if (isXHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) currentState = MAIN_MENU; // Mock Markdown content DrawTextEx(customFont, "v1.9.0 Release Notes:", (Vector2){ pBox.x + 30, pBox.y + 80 }, 20, 1.0f, WHITE); DrawTextEx(customFont, "- Fixed Frustum Culling gaps", (Vector2){ pBox.x + 40, pBox.y + 110 }, 18, 1.0f, LIGHTGRAY); DrawTextEx(customFont, "- Added Auto-Update System", (Vector2){ pBox.x + 40, pBox.y + 140 }, 18, 1.0f, LIGHTGRAY); DrawTextEx(customFont, "- Added Player Identity requirement", (Vector2){ pBox.x + 40, pBox.y + 170 }, 18, 1.0f, LIGHTGRAY); DrawTextEx(customFont, "- Improved audio stability", (Vector2){ pBox.x + 40, pBox.y + 200 }, 18, 1.0f, LIGHTGRAY); } else if (currentState == CREATE_WORLD_MENU) { targetZoom = 1.0f; DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 }); int panelWidth = 640; int panelHeight = 520; int panelX = (currentWidth / 2) - (panelWidth / 2); int panelY = (currentHeight / 2) - (panelHeight / 2); DrawRectangle(panelX, panelY, panelWidth, panelHeight, (Color){ 40, 40, 40, 240 }); DrawRectangleLinesEx((Rectangle){ (float)panelX, (float)panelY, (float)panelWidth, (float)panelHeight }, 4.0f, (Color){ 100, 100, 100, 255 }); DrawTextEx(customFont, "Create New World", (Vector2){ (float)panelX + 30, (float)panelY + 30 }, 32, 1.0f, WHITE); // World Name DrawTextEx(customFont, "World Name", (Vector2){ (float)panelX + 50, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY); Rectangle nameBox = { (float)panelX + 50, (float)panelY + 130, 540, 45 }; if (CheckCollisionPointRec(mousePos, nameBox) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) activeTextBox = 1; DrawRectangleRec(nameBox, DARKGRAY); DrawRectangleLinesEx(nameBox, 2.0f, activeTextBox == 1 ? WHITE : GRAY); DrawTextEx(customFont, worldName, (Vector2){ nameBox.x + 15, nameBox.y + 12 }, 20, 1.0f, WHITE); if (activeTextBox == 1 && ((int)(GetTime() * 2) % 2 == 0)) { int textW = MeasureTextEx(customFont, worldName, 20, 1.0f).x; DrawRectangle(nameBox.x + 17 + textW, nameBox.y + 12, 12, 20, LIGHTGRAY); } // World Seed DrawTextEx(customFont, "Seed (Optional)", (Vector2){ (float)panelX + 50, (float)panelY + 200 }, 20, 1.0f, LIGHTGRAY); Rectangle seedBox = { (float)panelX + 50, (float)panelY + 230, 540, 45 }; if (CheckCollisionPointRec(mousePos, seedBox) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) activeTextBox = 2; if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(mousePos, nameBox) && !CheckCollisionPointRec(mousePos, seedBox)) activeTextBox = 0; DrawRectangleRec(seedBox, DARKGRAY); DrawRectangleLinesEx(seedBox, 2.0f, activeTextBox == 2 ? WHITE : GRAY); DrawTextEx(customFont, worldSeed, (Vector2){ seedBox.x + 15, seedBox.y + 12 }, 20, 1.0f, WHITE); if (activeTextBox == 2 && ((int)(GetTime() * 2) % 2 == 0)) { int textW = MeasureTextEx(customFont, worldSeed, 20, 1.0f).x; DrawRectangle(seedBox.x + 17 + textW, seedBox.y + 12, 12, 20, LIGHTGRAY); } // Buttons int btnWidth = 200; int btnHeight = 50; int btnY = panelY + panelHeight - 90; // Back Button int backBtnX = panelX + 50; Rectangle backBtnBounds = { (float)backBtnX, (float)btnY, (float)btnWidth, (float)btnHeight }; bool isBackHovered = CheckCollisionPointRec(mousePos, backBtnBounds); DrawRectangleRec(backBtnBounds, isBackHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 }); DrawRectangleLinesEx(backBtnBounds, 3.0f, isBackHovered ? WHITE : GRAY); Vector2 backTextSize = MeasureTextEx(customFont, "Back", 20, 1.0f); DrawTextEx(customFont, "Back", (Vector2){ backBtnX + (btnWidth/2) - (backTextSize.x/2), btnY + (btnHeight/2) - (backTextSize.y/2) }, 20, 1.0f, WHITE); if (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) currentState = MAIN_MENU; // Create Button int createBtnX = panelX + panelWidth - 250; Rectangle createBtnBounds = { (float)createBtnX, (float)btnY, (float)btnWidth, (float)btnHeight }; bool isCreateHovered = CheckCollisionPointRec(mousePos, createBtnBounds); DrawRectangleRec(createBtnBounds, isCreateHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 }); DrawRectangleLinesEx(createBtnBounds, 3.0f, isCreateHovered ? WHITE : GRAY); Vector2 createTextSize = MeasureTextEx(customFont, "Create", 20, 1.0f); DrawTextEx(customFont, "Create", (Vector2){ createBtnX + (btnWidth/2) - (createTextSize.x/2), btnY + (btnHeight/2) - (createTextSize.y/2) }, 20, 1.0f, WHITE); if (isCreateHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentWorldName = worldName; std::string baseName = worldName; int nameCounter = 1; while (std::filesystem::exists("saves/" + currentWorldName)) { currentWorldName = baseName + " " + std::to_string(nameCounter); nameCounter++; } // Update the worldName string so the HUD shows it correctly snprintf(worldName, sizeof(worldName), "%s", currentWorldName.c_str()); worldNameLen = strlen(worldName); gameTime = 150.0f; // Start new world at 7:00 AM (v2.3.3 adjusted for slower cycle) if (serverMode) { serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket != INVALID_SOCKET_VAL) { SetNonBlocking(serverSocket); struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(12345); bind(serverSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serverSocket, 5); } } std::filesystem::create_directories("saves/" + currentWorldName); // Simple hash for seed string globalSeedHash = 0; for (int i = 0; worldSeed[i] != '\0'; i++) { globalSeedHash = globalSeedHash * 31 + worldSeed[i]; } if (worldSeed[0] == '\0') globalSeedHash = GetRandomValue(0, 100000); // Clear old chunks for (auto& pair : worldChunks) { delete pair.second; } worldChunks.clear(); // Start progress screen isNewWorldGeneration = true; chunksGeneratedCount = 0; worldGenProgress = 0.0f; // NEW: Find the ideal spawn BEFORE generating chunks to fix "missing terrain" bug Vector2 idealSpawn = FindIdealSpawn(); spawnSavedX = idealSpawn.x; spawnSavedZ = idealSpawn.y; spawnSavedY = -1; camera3D.position = (Vector3){ idealSpawn.x, 60.0f, idealSpawn.y }; currentState = WORLD_CREATION_PROGRESS; } } // Draw Gameplay overlay if we entered gameplay if (currentState == GAMEPLAY || currentState == PAUSE_MENU || currentState == CRAFTING_GUI || currentState == CHEST_GUI || currentState == CHEAT_GUI) { hoveredItemName = ""; // ---- Atmospheric Calculations (Previously global) ---- // lightLevel and blockTint are derived from the global dayFactor float lightLevel = 0.2f + 0.8f * dayFactor; // Ambient light multiplier Color blockTint = { (unsigned char)(255 * lightLevel), (unsigned char)(255 * lightLevel), (unsigned char)(255 * lightLevel), 255 }; Color skyBlue = { 135, 206, 235, 255 }; Color nightBlue = { 10, 10, 25, 255 }; Color currentSky = { (unsigned char)(nightBlue.r + dayFactor * (skyBlue.r - nightBlue.r)), (unsigned char)(nightBlue.g + dayFactor * (skyBlue.g - nightBlue.g)), (unsigned char)(nightBlue.b + dayFactor * (skyBlue.b - nightBlue.b)), 255 }; ClearBackground(currentSky); BeginMode3D(camera3D); // Draw Sun and Moon (simple billboards or spheres far away) Vector3 sunPos = { camera3D.position.x + cosf(sunAngle) * 100, camera3D.position.y + sinf(sunAngle) * 100, camera3D.position.z }; Vector3 moonPos = { camera3D.position.x + cosf(sunAngle + 3.14159f) * 100, camera3D.position.y + sinf(sunAngle + 3.14159f) * 100, camera3D.position.z }; DrawSphere(sunPos, 5.0f, YELLOW); DrawSphere(moonPos, 4.0f, LIGHTGRAY); // --- DRAW REMOTE PLAYERS --- for (const auto& rp : remotePlayers) { rlPushMatrix(); rlTranslatef(rp.position.x, rp.position.y, rp.position.z); rlRotatef(rp.yaw * 180.0f/PI, 0, 1, 0); // Simple Humanoid Shape (Cube-based "Skin") // Body DrawCube((Vector3){0, 0.7f, 0}, 0.6f, 0.9f, 0.3f, rp.shirtColor); // Head DrawCube((Vector3){0, 1.4f, 0}, 0.45f, 0.45f, 0.45f, (Color){220, 180, 150, 255}); // Arms DrawCube((Vector3){-0.45f, 0.7f, 0}, 0.2f, 0.8f, 0.2f, (Color){200, 160, 130, 255}); DrawCube((Vector3){0.45f, 0.7f, 0}, 0.2f, 0.8f, 0.2f, (Color){200, 160, 130, 255}); // Legs DrawCube((Vector3){-0.15f, 0.15f, 0}, 0.25f, 0.4f, 0.25f, rp.pantsColor); DrawCube((Vector3){0.15f, 0.15f, 0}, 0.25f, 0.4f, 0.25f, rp.pantsColor); rlPopMatrix(); // Draw Nameplate Vector2 namePos = GetWorldToScreen((Vector3){ rp.position.x, rp.position.y + 2.1f, rp.position.z }, camera3D); DrawTextEx(customFont, rp.name.c_str(), (Vector2){ namePos.x - MeasureTextEx(customFont, rp.name.c_str(), 20, 1.0f).x/2, namePos.y }, 20, 1.0f, WHITE); } int playerCX = (int)floorf(camera3D.position.x / CHUNK_SIZE); int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE); Vector3 camForward = Vector3Normalize(Vector3Subtract(camera3D.target, camera3D.position)); // --- PERFORMANCE OPTIMIZED BATCHED RENDER LOOP --- // 1. Identify chunks to render and rebuild dirty ones std::vector visibleChunks; for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { // OPTIMIZED FRUSTUM CULLING (Bug #1 Fix: Don't cull the player's current chunk) if (abs(cx - playerCX) > 1 || abs(cz - playerCZ) > 1) { Vector3 chunkCenter = { (float)(cx * CHUNK_SIZE + 16), 64.0f, (float)(cz * CHUNK_SIZE + 16) }; float distSqr = Vector3LengthSqr(Vector3Subtract(chunkCenter, camera3D.position)); if (distSqr > 160.0f * 160.0f) continue; Vector3 toChunk = Vector3Normalize(Vector3Subtract(chunkCenter, camera3D.position)); if (distSqr > 32.0f * 32.0f && Vector3DotProduct(toChunk, camForward) < -0.4f) continue; } auto it = worldChunks.find({cx, cz}); if (it == worldChunks.end()) continue; Chunk* chunk = it->second; if (chunk->dirty) RebuildChunkRenderList(chunk, cx, cz); visibleChunks.push_back(chunk); } } // Render Dropped Items for (const auto& item : droppedItems) { if (item.active) { Texture2D tex = (item.type == GRASS) ? grassTopTexture : blockTextures[item.type]; // Floating animation float bounce = sinf(GetTime() * 3.0f) * 0.1f; Vector3 renderPos = { item.pos.x, item.pos.y + bounce, item.pos.z }; DrawBillboard(camera3D, tex, renderPos, 0.4f, WHITE); } } for (int renderType = 1; renderType < 64; renderType++) { if (renderType == GRASS) { for (Chunk* chunk : visibleChunks) { for (auto& data : chunk->renderLists[GRASS]) { float light = GetBlockLight((Vector3){data.x, data.y, data.z}); Color tint = {(unsigned char)(255*light), (unsigned char)(255*light), (unsigned char)(255*light), 255}; DrawGrassBlock((Vector3){data.x, data.y, data.z}, blockTextures[GRASS].id, grassTopTexture.id, blockTextures[DIRT].id, tint, data.faces); } } } else if (renderType == LOG) { for (Chunk* chunk : visibleChunks) { for (auto& data : chunk->renderLists[LOG]) { float light = GetBlockLight((Vector3){data.x, data.y, data.z}); Color tint = {(unsigned char)(255*light), (unsigned char)(255*light), (unsigned char)(255*light), 255}; DrawLog((Vector3){data.x, data.y, data.z}, logSideTexture.id, logTopTexture.id, tint, data.faces); } } } else if (renderType == CRAFTING_TABLE) { for (Chunk* chunk : visibleChunks) { for (auto& data : chunk->renderLists[CRAFTING_TABLE]) { float light = GetBlockLight((Vector3){data.x, data.y, data.z}); Color tint = {(unsigned char)(255*light), (unsigned char)(255*light), (unsigned char)(255*light), 255}; DrawCraftingTable((Vector3){data.x, data.y, data.z}, craftingSideTexture.id, craftingTopTexture.id, blockTextures[DIRT].id, tint, data.faces); } } } else if (renderType == TORCH) { for (Chunk* chunk : visibleChunks) { for (auto& data : chunk->renderLists[TORCH]) { // 3D Flickering Torch (v2.3.3) float flicker = sinf(GetTime() * 12.0f) * 0.03f; float h = 0.6f + flicker; float w = 0.12f; Texture2D tex = blockTextures[TORCH]; rlSetTexture(tex.id); rlPushMatrix(); rlTranslatef(data.x, data.y - 0.3f, data.z); rlBegin(RL_QUADS); rlColor4ub(255, 255, 255, 255); // Front rlTexCoord2f(0,0); rlVertex3f(-w/2, h, w/2); rlTexCoord2f(0,1); rlVertex3f(-w/2, 0, w/2); rlTexCoord2f(1,1); rlVertex3f(w/2, 0, w/2); rlTexCoord2f(1,0); rlVertex3f(w/2, h, w/2); // Back rlTexCoord2f(0,0); rlVertex3f(w/2, h, -w/2); rlTexCoord2f(0,1); rlVertex3f(w/2, 0, -w/2); rlTexCoord2f(1,1); rlVertex3f(-w/2, 0, -w/2); rlTexCoord2f(1,0); rlVertex3f(-w/2, h, -w/2); // Right rlTexCoord2f(0,0); rlVertex3f(w/2, h, w/2); rlTexCoord2f(0,1); rlVertex3f(w/2, 0, w/2); rlTexCoord2f(1,1); rlVertex3f(w/2, 0, -w/2); rlTexCoord2f(1,0); rlVertex3f(w/2, h, -w/2); // Left rlTexCoord2f(0,0); rlVertex3f(-w/2, h, -w/2); rlTexCoord2f(0,1); rlVertex3f(-w/2, 0, -w/2); rlTexCoord2f(1,1); rlVertex3f(-w/2, 0, w/2); rlTexCoord2f(1,0); rlVertex3f(-w/2, h, w/2); // Top rlTexCoord2f(0,0); rlVertex3f(-w/2, h, -w/2); rlTexCoord2f(0,1); rlVertex3f(-w/2, h, w/2); rlTexCoord2f(1,1); rlVertex3f(w/2, h, w/2); rlTexCoord2f(1,0); rlVertex3f(w/2, h, -w/2); rlEnd(); rlPopMatrix(); } } } else { rlSetTexture(blockTextures[renderType].id); rlBegin(RL_QUADS); for (Chunk* chunk : visibleChunks) { for (auto& data : chunk->renderLists[renderType]) { float light = GetBlockLight((Vector3){data.x, data.y, data.z}); Color c = {(unsigned char)(255*light), (unsigned char)(255*light), (unsigned char)(255*light), 255}; if (renderType == LEAVES) { c.r *= 0.6f; c.g *= 0.9f; c.b *= 0.6f; } rlColor4ub(c.r, c.g, c.b, 255); DrawCubeVertices(data.x, data.y, data.z, 1.0f, 1.0f, 1.0f, data.faces); } } rlEnd(); } } // Draw Block Selection Wireframe if (hitBlock && !inventoryOpen && currentState != CRAFTING_GUI) { float size = 1.02f; DrawCubeWires((Vector3){(float)hitX, (float)hitY, (float)hitZ}, size, size, size, BLACK); // Draw breaking progress indicator if (breakProgress > 0.0f) { float pSize = (breakProgress / 1.5f) * 1.05f; // Scale visual based on progress if (pSize > 1.05f) pSize = 1.05f; DrawCubeWires((Vector3){(float)hitX, (float)hitY, (float)hitZ}, pSize, pSize, pSize, RED); } } // --- DRAW VIEWMODEL (ARM/HELD ITEM) --- if (!inventoryOpen && currentState != CRAFTING_GUI) { if (isSwinging) { swingTime += GetFrameTime() * 8.0f; if (swingTime >= 1.0f) { swingTime = 0.0f; isSwinging = false; } } float swingVal = sinf(swingTime * PI); rlDisableDepthTest(); // Draw on top rlPushMatrix(); // Build a stable viewmodel matrix // We use the camera's right/up/forward but dampen the vertical tilt Vector3 forwardVM = Vector3Subtract(camera3D.target, camera3D.position); forwardVM = Vector3Normalize(forwardVM); Vector3 rightVM = Vector3CrossProduct(forwardVM, camera3D.up); rightVM = Vector3Normalize(rightVM); Vector3 upVM = Vector3CrossProduct(rightVM, forwardVM); upVM = Vector3Normalize(upVM); // Horizontal-only forward for arm orientation Vector3 hForward = { forwardVM.x, 0, forwardVM.z }; hForward = Vector3Normalize(hForward); // Hand position relative to camera Vector3 handPos = Vector3Add(camera3D.position, Vector3Scale(forwardVM, 0.45f)); handPos = Vector3Add(handPos, Vector3Scale(rightVM, 0.28f)); handPos = Vector3Add(handPos, Vector3Scale(upVM, -0.25f - swingVal * 0.15f)); // Viewmodel logic int heldBT = hotbar[activeHotbarSlot].blockType; if (heldBT == AIR) { // Draw Arm (rotated cube) rlPushMatrix(); rlTranslatef(handPos.x, handPos.y, handPos.z); rlRotatef(camYaw * 180.0f/PI, 0, 1, 0); rlRotatef(camPitch * 180.0f/PI - 15.0f - swingVal * 20.0f, 1, 0, 0); DrawCube((Vector3){0,0,0}, 0.1f, 0.12f, 0.4f, (Color){220, 180, 150, 255}); rlPopMatrix(); } else { // Draw Held Item Vector3 itemPos = Vector3Add(handPos, Vector3Scale(forwardVM, 0.25f)); itemPos = Vector3Add(itemPos, Vector3Scale(upVM, 0.05f)); itemPos = Vector3Add(itemPos, Vector3Scale(rightVM, -0.05f)); if (heldBT < STICK) { // 3D Block in hand rlPushMatrix(); rlTranslatef(itemPos.x, itemPos.y, itemPos.z); rlRotatef(camYaw * 180.0f/PI + 45.0f, 0, 1, 0); rlRotatef(20.0f, 1, 0, 1); rlScalef(0.15f, 0.15f, 0.15f); if (heldBT == GRASS) { DrawGrassBlock((Vector3){0,0,0}, blockTextures[GRASS].id, grassTopTexture.id, blockTextures[DIRT].id, WHITE, 63); } else if (heldBT == LOG) { DrawLog((Vector3){0,0,0}, logSideTexture.id, logTopTexture.id, WHITE, 63); } else if (heldBT == CRAFTING_TABLE) { DrawCraftingTable((Vector3){0,0,0}, craftingSideTexture.id, craftingTopTexture.id, blockTextures[DIRT].id, WHITE, 63); } else { rlSetTexture(blockTextures[heldBT].id); rlBegin(RL_QUADS); rlColor4ub(255, 255, 255, 255); DrawCubeVertices(0,0,0, 1.0f, 1.0f, 1.0f, 63); rlEnd(); } rlPopMatrix(); rlSetTexture(0); } else { // Item (Stick/Axe) as Billboard Texture2D itemTex = blockTextures[heldBT]; if (itemTex.id > 0) { Rectangle src = { 0, 0, (float)itemTex.width, (float)itemTex.height }; Vector2 itemSize = { 0.35f, 0.35f }; Vector2 pivot = { -itemSize.x/2.0f, -itemSize.y/2.0f }; DrawBillboardPro(camera3D, itemTex, src, itemPos, camera3D.up, itemSize, pivot, 45.0f + swingVal * 40.0f, WHITE); } } } rlPopMatrix(); rlEnableDepthTest(); } EndMode3D(); if (damageFlashTimer > 0) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 255, 0, 0, (unsigned char)(120 * (damageFlashTimer/0.4f)) }); } // Draw 2D Crosshair overlay (hide when inventory open) if (!inventoryOpen) { DrawRectangle(currentWidth/2 - 2, currentHeight/2 - 10, 4, 20, (Color){255, 255, 255, 150}); DrawRectangle(currentWidth/2 - 10, currentHeight/2 - 2, 20, 4, (Color){255, 255, 255, 150}); } // Draw debug info (top-left) DrawTextEx(customFont, "MorriCraft Pre-Alpha", (Vector2){ 10, 10 }, 20, 1.0f, BLACK); DrawTextEx(customFont, TextFormat("World: %s", worldName), (Vector2){ 10, 40 }, 16, 1.0f, BLACK); DrawTextEx(customFont, TextFormat("FPS: %i", GetFPS()), (Vector2){ 10, 60 }, 16, 1.0f, RED); DrawTextEx(customFont, TextFormat("XYZ: %.1f / %.1f / %.1f", camera3D.position.x, camera3D.position.y - 1.6f, camera3D.position.z), (Vector2){ 10, 80 }, 16, 1.0f, DARKGRAY); // Draw Chat Log (Bottom Left, above inventory) int chatY = currentHeight - 120; for (auto it = chatLog.begin(); it != chatLog.end();) { it->timer -= GetFrameTime(); if (it->timer <= 0) { it = chatLog.erase(it); } else { float alpha = (it->timer > 1.0f) ? 1.0f : it->timer; DrawTextEx(customFont, it->text.c_str(), (Vector2){ 20, (float)chatY }, 20, 1.0f, Fade(WHITE, alpha)); chatY -= 25; ++it; } } if (isChatting) { DrawRectangle(10, currentHeight - 100, 400, 40, (Color){ 0, 0, 0, 180 }); DrawRectangleLines(10, currentHeight - 100, 400, 40, DARKGRAY); DrawTextEx(customFont, TextFormat("> %s", chatInput), (Vector2){ 20, (float)currentHeight - 90 }, 20, 1.0f, WHITE); } // ---- HOTBAR ---- const int slotSize = 50; const int slotGap = 4; int hotbarW = 9 * slotSize + 8 * slotGap; int hotbarX = currentWidth / 2 - hotbarW / 2; int hotbarY = currentHeight - slotSize - 12; // --- Health Hearts --- int heartX = currentWidth / 2 - 225; int heartY = hotbarY - 35; for (int i = 0; i < 8; i++) { Rectangle heartRect = { (float)heartX + i * 28, (float)heartY, 24, 24 }; float h = playerHealth - (i * 2); if (h >= 2.0f) DrawRectangleRec(heartRect, RED); // Full heart else if (h >= 1.0f) { DrawRectangle((int)heartRect.x, (int)heartRect.y, 12, 24, RED); // Half heart DrawRectangleLinesEx(heartRect, 2.0f, DARKGRAY); } else { DrawRectangleLinesEx(heartRect, 2.0f, GRAY); // Empty heart } } // --- Hunger Bar --- int hungerX = currentWidth / 2 + 5; int hungerY = hotbarY - 35; for (int i = 0; i < 10; i++) { Rectangle hungerRect = { (float)hungerX + i * 22, (float)hungerY, 18, 18 }; float h = playerHunger - (i * 2); Color hungerColor = { 180, 100, 40, 255 }; // Brownish orange if (h >= 2.0f) DrawRectangleRec(hungerRect, hungerColor); else if (h >= 1.0f) { DrawRectangle((int)hungerRect.x, (int)hungerRect.y, 9, 18, hungerColor); DrawRectangleLinesEx(hungerRect, 2.0f, DARKGRAY); } else { DrawRectangleLinesEx(hungerRect, 2.0f, (Color){ 60, 60, 60, 255 }); } } // Hotbar background panel DrawRectangle(hotbarX - 6, hotbarY - 6, hotbarW + 12, slotSize + 12, (Color){ 30, 30, 30, 180 }); DrawRectangleLinesEx((Rectangle){ (float)(hotbarX-6), (float)(hotbarY-6), (float)(hotbarW+12), (float)(slotSize+12) }, 2.0f, (Color){ 80, 80, 80, 220 }); for (int s = 0; s < 9; s++) { int sx = hotbarX + s * (slotSize + slotGap); bool selected = (s == activeHotbarSlot); // Slot background DrawRectangle(sx, hotbarY, slotSize, slotSize, (Color){ 60, 60, 60, 220 }); // Slot border (gold for selected, gray otherwise) Color borderCol = selected ? (Color){255, 215, 0, 255} : (Color){100, 100, 100, 255}; float borderW = selected ? 3.0f : 1.5f; DrawRectangleLinesEx((Rectangle){ (float)sx, (float)hotbarY, (float)slotSize, (float)slotSize }, borderW, borderCol); // Block texture preview if (hotbar[s].count > 0 && hotbar[s].blockType != AIR) { int bt = hotbar[s].blockType; Texture2D tex = (bt == GRASS) ? grassTopTexture : blockTextures[bt]; if (tex.id > 0) { Rectangle src = { 0, 0, (float)tex.width, (float)tex.height }; Rectangle dst = { (float)(sx+4), (float)(hotbarY+4), (float)(slotSize-8), (float)(slotSize-8) }; Color tint = (bt == GRASS) ? (Color){124,189,107,255} : WHITE; DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint); } // Item count centered bottom Vector2 countSize = MeasureTextEx(customFont, TextFormat("%i", hotbar[s].count), 12, 1.0f); DrawTextEx(customFont, TextFormat("%i", hotbar[s].count), (Vector2){ (float)(sx + (slotSize/2) - (countSize.x/2)), (float)(hotbarY + slotSize - 14) }, 12, 1.0f, WHITE); // Draw Hotbar Durability Bar (v2.3.3) if (hotbar[s].maxDurability > 0 && hotbar[s].durability < hotbar[s].maxDurability) { float durP = (float)hotbar[s].durability / hotbar[s].maxDurability; int barW = slotSize - 10; int barH = 3; DrawRectangle(sx + 5, hotbarY + slotSize - 8, barW, barH, BLACK); DrawRectangle(sx + 5, hotbarY + slotSize - 8, (int)(barW * durP), barH, (durP < 0.25f ? RED : (durP < 0.5f ? ORANGE : GREEN))); } } // Slot number label DrawTextEx(customFont, TextFormat("%i", s+1), (Vector2){ (float)(sx+3), (float)(hotbarY+2) }, 12, 1.0f, (Color){180,180,180,200}); } // ---- UI HELPERS & STATE ---- const int invSlotSize = 52; const int invGap = 4; const int invCols = 9; const int invRows = 3; const int invPanelW = 550; const int invPanelH = 500; auto drawSlotEx = [&](InventorySlot& slot, int px, int py, bool isResultSlot = false, bool isTableResult = false) { Rectangle slotRect = { (float)px, (float)py, (float)invSlotSize, (float)invSlotSize }; bool hov = CheckCollisionPointRec(mousePos, slotRect); DrawRectangleRec(slotRect, hov ? (Color){90,90,90,255} : (Color){60,60,60,255}); DrawRectangleLinesEx(slotRect, 2.0f, hov ? WHITE : (Color){100,100,100,200}); // Draw Durability Bar (v2.3.3) if (slot.maxDurability > 0 && slot.durability < slot.maxDurability) { float durPercent = (float)slot.durability / slot.maxDurability; int barW = slotRect.width - 10; int barH = 3; DrawRectangle(slotRect.x + 5, slotRect.y + slotRect.height - 8, barW, barH, BLACK); DrawRectangle(slotRect.x + 5, slotRect.y + slotRect.height - 8, (int)(barW * durPercent), barH, (durPercent < 0.25f) ? RED : (durPercent < 0.5f ? ORANGE : GREEN)); } if (hov && slot.blockType != AIR) { hoveredItemName = GetBlockName(slot.blockType); } if (slot.count > 0 && slot.blockType != AIR) { int bt = slot.blockType; Texture2D tex = (bt == GRASS) ? grassTopTexture : (bt == CRAFTING_TABLE ? craftingSideTexture : blockTextures[bt]); if (tex.id > 0 && tex.width > 0 && tex.height > 0) { Rectangle src = { 0,0,(float)tex.width,(float)tex.height }; Rectangle dst = { (float)(px+5),(float)(py+5), (float)(invSlotSize-10),(float)(invSlotSize-10) }; Color tint = (bt == GRASS) ? (Color){124,189,107,255} : WHITE; DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint); } else if (bt >= STICK) { // Fallback rendering for items if texture failed Color itemCol = (bt == STICK) ? BROWN : DARKGRAY; DrawRectangle(px + 10, py + 10, invSlotSize - 20, invSlotSize - 20, itemCol); } Vector2 cSize = MeasureTextEx(customFont, TextFormat("%i", slot.count), 12, 1.0f); DrawTextEx(customFont, TextFormat("%i", slot.count), (Vector2){ (float)(px + (invSlotSize/2) - (cSize.x/2)), (float)(py + invSlotSize - 14) }, 12, 1.0f, WHITE); } if (hov && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (IsKeyDown(KEY_LEFT_SHIFT) && isResultSlot && slot.blockType != AIR) { // Take everything possible from the result box while (slot.blockType != AIR) { GiveItems(slot.blockType, slot.count); // Decrease ingredients if (isTableResult) { for(int i=0; i<9; i++) { if (tableSlots[i].count > 0) tableSlots[i].count--; if (tableSlots[i].count == 0) tableSlots[i].blockType = AIR; } UpdateTableCrafting(); slot = tableResult; // Refresh from local reference } else { for(int i=0; i<4; i++) { if (craftingSlots[i].count > 0) craftingSlots[i].count--; if (craftingSlots[i].count == 0) craftingSlots[i].blockType = AIR; } UpdateCrafting(); slot = craftingResult; } // Safety: if we can't craft anymore, break if (slot.blockType == AIR) break; } } else if (IsKeyDown(KEY_LEFT_SHIFT) && !isResultSlot && slot.blockType != AIR) { // Pick up entire stack into hand immediately if (mouseHeldItem.blockType == AIR) { mouseHeldItem = slot; slot = InventorySlot(AIR, 0); } else if (mouseHeldItem.blockType == slot.blockType) { int space = 64 - mouseHeldItem.count; int toAdd = (slot.count < space) ? slot.count : space; mouseHeldItem.count += toAdd; slot.count -= toAdd; if (slot.count <= 0) slot = InventorySlot(AIR, 0); } UpdateCrafting(); UpdateTableCrafting(); } else if (isResultSlot) { if (mouseHeldItem.blockType == AIR && slot.blockType != AIR) { mouseHeldItem = slot; slot = InventorySlot(AIR, 0); if (isTableResult) { for(int i=0; i<9; i++) { if (tableSlots[i].count > 0) tableSlots[i].count--; if (tableSlots[i].count == 0) tableSlots[i].blockType = AIR; } UpdateTableCrafting(); } else { for(int i=0; i<4; i++) { if (craftingSlots[i].count > 0) craftingSlots[i].count--; if (craftingSlots[i].count == 0) craftingSlots[i].blockType = AIR; } UpdateCrafting(); } } } else { // Left click: stack if same type, swap if different if (mouseHeldItem.blockType != AIR && slot.blockType == mouseHeldItem.blockType) { // Same type: merge stacks int space = 64 - slot.count; int toAdd = (mouseHeldItem.count < space) ? mouseHeldItem.count : space; slot.count += toAdd; mouseHeldItem.count -= toAdd; if (mouseHeldItem.count <= 0) mouseHeldItem = InventorySlot(AIR, 0); } else { // Different type or one is empty: swap InventorySlot tmp = slot; slot = mouseHeldItem; mouseHeldItem = tmp; } UpdateCrafting(); UpdateTableCrafting(); } } if (hov && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { if (!isResultSlot) { if (mouseHeldItem.blockType == AIR) { // Empty hand: pick up one item from the slot if (slot.blockType != AIR && slot.count > 0) { mouseHeldItem = InventorySlot(slot.blockType, 1); slot.count--; if (slot.count <= 0) slot = InventorySlot(AIR, 0); UpdateCrafting(); UpdateTableCrafting(); } } else { // Holding something: place one into the slot if (slot.blockType == AIR) { slot = InventorySlot(mouseHeldItem.blockType, 1); mouseHeldItem.count--; } else if (slot.blockType == mouseHeldItem.blockType && slot.count < 64) { slot.count++; mouseHeldItem.count--; } if (mouseHeldItem.count <= 0) mouseHeldItem = InventorySlot(AIR, 0); UpdateCrafting(); UpdateTableCrafting(); } } } }; auto drawInvGrid = [&](int startX, int startY) { for (int row = 0; row < invRows; row++) { for (int col = 0; col < invCols; col++) { int si = row * invCols + col; int px = startX + col * (invSlotSize + invGap); int py = startY + row * (invSlotSize + invGap); drawSlotEx(inventory[si], px, py); } } int hotY = startY + invRows * (invSlotSize + invGap) + 12; for (int col = 0; col < invCols; col++) { int px = startX + col * (invSlotSize + invGap); drawSlotEx(hotbar[col], px, hotY); } }; // ---- INVENTORY SCREEN ---- if (inventoryOpen) { // Dim world DrawRectangle(0, 0, currentWidth, currentHeight, (Color){0,0,0,160}); int invPanelX = currentWidth / 2 - invPanelW / 2; int invPanelY = currentHeight / 2 - invPanelH / 2; DrawRectangle(invPanelX, invPanelY, invPanelW, invPanelH, (Color){ 40, 40, 40, 240 }); DrawRectangleLinesEx((Rectangle){ (float)invPanelX, (float)invPanelY, (float)invPanelW, (float)invPanelH }, 3.0f, (Color){ 180, 180, 180, 255 }); DrawTextEx(customFont, "Inventory", (Vector2){ (float)(invPanelX + 12), (float)(invPanelY + 10) }, 20, 1.0f, WHITE); // --- Player Character Placeholder --- int charW = 100; int charH = 160; int charX = invPanelX + 32; int charY = invPanelY + 40; DrawRectangle(charX, charY, charW, charH, (Color){30, 30, 30, 255}); DrawRectangleLinesEx((Rectangle){(float)charX, (float)charY, (float)charW, (float)charH}, 2.0f, DARKGRAY); DrawTextEx(customFont, "PLAYER", (Vector2){(float)charX + 22, (float)charY + 70}, 16, 1.0f, GRAY); // --- 2x2 Crafting Grid --- int craftX = charX + charW + 40; int craftY = charY + 20; DrawTextEx(customFont, "Crafting", (Vector2){(float)craftX, (float)craftY - 18}, 14, 1.0f, LIGHTGRAY); drawSlotEx(craftingSlots[0], craftX, craftY); drawSlotEx(craftingSlots[1], craftX + invSlotSize + invGap, craftY); drawSlotEx(craftingSlots[2], craftX, craftY + invSlotSize + invGap); drawSlotEx(craftingSlots[3], craftX + invSlotSize + invGap, craftY + invSlotSize + invGap); // Arrow and Result int arrowX = craftX + 2*invSlotSize + invGap + 15; DrawTextEx(customFont, "->", (Vector2){(float)arrowX, (float)craftY + invSlotSize/2 + 10}, 24, 1.0f, WHITE); drawSlotEx(craftingResult, arrowX + 40, craftY + invSlotSize/2, true); // --- Main Inventory (3x9) --- int gridStartX = invPanelX + (invPanelW - (invCols * invSlotSize + (invCols-1) * invGap)) / 2; int gridStartY = invPanelY + charH + 60; drawInvGrid(gridStartX, gridStartY); // Close hint DrawTextEx(customFont, "Press E or ESC to close", (Vector2){ (float)(invPanelX + 12), (float)(invPanelY + invPanelH - 22) }, 14, 1.0f, (Color){160, 160, 160, 200}); if (IsKeyPressed(KEY_ESCAPE)) { inventoryOpen = false; DisableCursor(); } } if (currentState == CRAFTING_GUI) { DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 150 }); int guiX = currentWidth/2 - invPanelW/2; int guiY = currentHeight/2 - (invPanelH + 50)/2; DrawRectangle(guiX, guiY, invPanelW, invPanelH + 50, (Color){40, 40, 40, 240}); DrawRectangleLinesEx((Rectangle){(float)guiX, (float)guiY, (float)invPanelW, (float)invPanelH + 50}, 3.0f, LIGHTGRAY); DrawTextEx(customFont, "Crafting Table", (Vector2){(float)guiX + 20, (float)guiY + 20}, 24, 1.0f, WHITE); int tableStartX = guiX + 50; int tableStartY = guiY + 80; for(int i=0; i<9; i++) { int px = tableStartX + (i % 3) * (invSlotSize + invGap); int py = tableStartY + (i / 3) * (invSlotSize + invGap); drawSlotEx(tableSlots[i], px, py, false, true); } int arrowX = tableStartX + 3*invSlotSize + 30; DrawTextEx(customFont, "----->", (Vector2){(float)arrowX, (float)tableStartY + invSlotSize + 10}, 24, 1.0f, WHITE); drawSlotEx(tableResult, arrowX + 80, tableStartY + invSlotSize, true, true); drawInvGrid(guiX + 16, tableStartY + 3*(invSlotSize+invGap) + 40); if (IsKeyPressed(KEY_ESCAPE)) { currentState = GAMEPLAY; inventoryOpen = false; DisableCursor(); } } if (currentState == CHEST_GUI) { // --- CHEST GUI (3x9 Chest + 3x9 Player Inventory) --- int invPanelW = 600, invPanelH = 500; int guiX = currentWidth/2 - invPanelW/2; int guiY = currentHeight/2 - invPanelH/2; DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 150 }); DrawRectangleRec((Rectangle){(float)guiX, (float)guiY, (float)invPanelW, (float)invPanelH}, (Color){ 40, 40, 40, 255 }); DrawRectangleLinesEx((Rectangle){(float)guiX, (float)guiY, (float)invPanelW, (float)invPanelH}, 3.0f, LIGHTGRAY); DrawTextEx(customFont, "Chest", (Vector2){ (float)guiX + 20, (float)guiY + 15 }, 24, 1.0f, WHITE); DrawTextEx(customFont, "Inventory", (Vector2){ (float)guiX + 20, (float)guiY + 240 }, 24, 1.0f, WHITE); uint64_t key = GetPosKey((int)activeChestPos.x, (int)activeChestPos.y, (int)activeChestPos.z); auto& chestInv = chestInventories[key]; // Chest Slots (3x9) for (int i = 0; i < 27; i++) { int row = i / 9; int col = i % 9; Rectangle slotRect = { (float)guiX + 20 + col * 64, (float)guiY + 50 + row * 64, 58, 58 }; bool hov = CheckCollisionPointRec(mousePos, slotRect); DrawRectangleRec(slotRect, hov ? (Color){90,90,90,255} : (Color){60,60,60,255}); DrawRectangleLinesEx(slotRect, 2.0f, hov ? WHITE : (Color){100,100,100,200}); if (chestInv[i].blockType != AIR) { Texture2D tex = blockTextures[chestInv[i].blockType]; DrawTexturePro(tex, (Rectangle){0,0,(float)tex.width,(float)tex.height}, (Rectangle){slotRect.x+5, slotRect.y+5, 48, 48}, (Vector2){0,0}, 0, WHITE); DrawTextEx(customFont, TextFormat("%d", chestInv[i].count), (Vector2){slotRect.x+40, slotRect.y+40}, 16, 1.0f, WHITE); if (hov) hoveredItemName = GetBlockName(chestInv[i].blockType); } if (hov && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { // Left click: stack if same type, swap if different if (mouseHeldItem.blockType != AIR && chestInv[i].blockType == mouseHeldItem.blockType) { int space = 64 - chestInv[i].count; int toAdd = (mouseHeldItem.count < space) ? mouseHeldItem.count : space; chestInv[i].count += toAdd; mouseHeldItem.count -= toAdd; if (mouseHeldItem.count <= 0) mouseHeldItem = InventorySlot(AIR, 0); } else { InventorySlot temp = chestInv[i]; chestInv[i] = mouseHeldItem; mouseHeldItem = temp; } } if (hov && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { // Right click: pick up/place one (v2.3.3) if (mouseHeldItem.blockType == AIR) { if (chestInv[i].blockType != AIR && chestInv[i].count > 0) { mouseHeldItem = InventorySlot(chestInv[i].blockType, 1); chestInv[i].count--; if (chestInv[i].count <= 0) chestInv[i] = InventorySlot(AIR, 0); } } else { if (chestInv[i].blockType == AIR) { chestInv[i] = InventorySlot(mouseHeldItem.blockType, 1); mouseHeldItem.count--; } else if (chestInv[i].blockType == mouseHeldItem.blockType && chestInv[i].count < 64) { chestInv[i].count++; mouseHeldItem.count--; } if (mouseHeldItem.count <= 0) mouseHeldItem = InventorySlot(AIR, 0); } } } // Player Inventory Slots (3x9) for (int i = 0; i < 27; i++) { int row = i / 9; int col = i % 9; Rectangle slotRect = { (float)guiX + 20 + col * 64, (float)guiY + 280 + row * 64, 58, 58 }; bool hov = CheckCollisionPointRec(mousePos, slotRect); DrawRectangleRec(slotRect, hov ? (Color){90,90,90,255} : (Color){60,60,60,255}); DrawRectangleLinesEx(slotRect, 2.0f, hov ? WHITE : (Color){100,100,100,200}); if (inventory[i].blockType != AIR) { Texture2D tex = blockTextures[inventory[i].blockType]; DrawTexturePro(tex, (Rectangle){0,0,(float)tex.width,(float)tex.height}, (Rectangle){slotRect.x+5, slotRect.y+5, 48, 48}, (Vector2){0,0}, 0, WHITE); DrawTextEx(customFont, TextFormat("%d", inventory[i].count), (Vector2){slotRect.x+40, slotRect.y+40}, 16, 1.0f, WHITE); if (hov) hoveredItemName = GetBlockName(inventory[i].blockType); } if (hov && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (mouseHeldItem.blockType != AIR && inventory[i].blockType == mouseHeldItem.blockType) { int space = 64 - inventory[i].count; int toAdd = (mouseHeldItem.count < space) ? mouseHeldItem.count : space; inventory[i].count += toAdd; mouseHeldItem.count -= toAdd; if (mouseHeldItem.count <= 0) mouseHeldItem = InventorySlot(AIR, 0); } else { InventorySlot temp = inventory[i]; inventory[i] = mouseHeldItem; mouseHeldItem = temp; } } if (hov && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { // Right click logic for player inv in chest GUI (v2.3.3) if (mouseHeldItem.blockType == AIR) { if (inventory[i].blockType != AIR && inventory[i].count > 0) { mouseHeldItem = InventorySlot(inventory[i].blockType, 1); inventory[i].count--; if (inventory[i].count <= 0) inventory[i] = InventorySlot(AIR, 0); } } else { if (inventory[i].blockType == AIR) { inventory[i] = InventorySlot(mouseHeldItem.blockType, 1); mouseHeldItem.count--; } else if (inventory[i].blockType == mouseHeldItem.blockType && inventory[i].count < 64) { inventory[i].count++; mouseHeldItem.count--; } if (mouseHeldItem.count <= 0) mouseHeldItem = InventorySlot(AIR, 0); } } } if (IsKeyPressed(KEY_ESCAPE)) { currentState = GAMEPLAY; inventoryOpen = false; DisableCursor(); PlaySound(chestCloseSound); } } if (currentState == CHEAT_GUI) { // --- CHEAT / CREATIVE INVENTORY GUI --- int panelW = 700, panelH = 500; int guiX = currentWidth/2 - panelW/2; int guiY = currentHeight/2 - panelH/2; DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 150 }); DrawRectangleRec((Rectangle){(float)guiX, (float)guiY, (float)panelW, (float)panelH}, (Color){ 30, 30, 30, 255 }); DrawRectangleLinesEx((Rectangle){(float)guiX, (float)guiY, (float)panelW, (float)panelH}, 3.0f, LIGHTGRAY); DrawTextEx(customFont, "Cheat Inventory (Scroll to see more)", (Vector2){ (float)guiX + 20, (float)guiY + 15 }, 24, 1.0f, GOLD); // Handle Scroll cheatScrollOffset -= GetMouseWheelMove() * 40; if (cheatScrollOffset < 0) cheatScrollOffset = 0; BeginScissorMode(guiX + 10, guiY + 60, panelW - 20, panelH - 100); int cols = 9; int slotS = 64; for (int i = 1; i < 64; i++) { // Show all slots to ensure nothing is missed if (i == 14 || i == 15 || i == 16 || (i >= 17 && i <= 24)) continue; // Skip untextured/meta items if any int row = (i-1) / cols; int col = (i-1) % cols; Rectangle r = { (float)guiX + 30 + col * (slotS + 5), (float)guiY + 70 + row * (slotS + 5) - cheatScrollOffset, (float)slotS, (float)slotS }; bool hov = CheckCollisionPointRec(mousePos, r); DrawRectangleRec(r, hov ? GRAY : DARKGRAY); DrawRectangleLinesEx(r, 1.5f, hov ? WHITE : (Color){80,80,80,255}); Texture2D tex = (i == GRASS) ? grassTopTexture : blockTextures[i]; if (tex.id > 0) { DrawTexturePro(tex, (Rectangle){0,0,(float)tex.width,(float)tex.height}, (Rectangle){r.x+8, r.y+8, 48, 48}, (Vector2){0,0}, 0, WHITE); } else if (i == APPLE) { // Emergency fallback for Apple if texture failed DrawCircle(r.x + 32, r.y + 32, 20, RED); } if (hov) { hoveredItemName = GetBlockName(i); if (hoveredItemName == "Unknown" && i == APPLE) hoveredItemName = "Apple"; // Double check } if (hov && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { // Give 64 of selected item to hotbar hotbar[activeHotbarSlot] = InventorySlot(i, 64); } } EndScissorMode(); DrawTextEx(customFont, "Click item to set to active hotbar slot", (Vector2){ (float)guiX + 20, (float)guiY + panelH - 30 }, 18, 1.0f, LIGHTGRAY); if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_E)) { currentState = GAMEPLAY; inventoryOpen = false; DisableCursor(); } } // --- Draw Held Item Last --- if (mouseHeldItem.blockType != AIR && mouseHeldItem.count > 0) { int bt = mouseHeldItem.blockType; Texture2D tex = (bt == GRASS) ? grassTopTexture : (bt == CRAFTING_TABLE ? craftingSideTexture : blockTextures[bt]); if (tex.id > 0) { Rectangle src = { 0,0,(float)tex.width,(float)tex.height }; Rectangle dst = { mousePos.x - (invSlotSize/2), mousePos.y - (invSlotSize/2), (float)invSlotSize, (float)invSlotSize }; Color tint = (bt == GRASS) ? (Color){124,189,107,255} : WHITE; DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint); Vector2 hSize = MeasureTextEx(customFont, TextFormat("%i", mouseHeldItem.count), 12, 1.0f); DrawTextEx(customFont, TextFormat("%i", mouseHeldItem.count), (Vector2){ dst.x + invSlotSize/2 - hSize.x/2, dst.y + invSlotSize - 14 }, 12, 1.0f, WHITE); } } // --- Draw Hover Tooltip --- if (hoveredItemName != "" && mouseHeldItem.blockType == AIR) { Vector2 tSize = MeasureTextEx(customFont, hoveredItemName.c_str(), 18, 1.0f); int tx = mousePos.x + 15; int ty = mousePos.y - 25; // Keep tooltip on screen if (tx + tSize.x + 10 > currentWidth) tx = mousePos.x - tSize.x - 15; if (ty < 0) ty = mousePos.y + 15; DrawRectangle(tx - 5, ty - 5, tSize.x + 10, tSize.y + 10, (Color){20, 20, 20, 240}); DrawRectangleLines(tx - 5, ty - 5, tSize.x + 10, tSize.y + 10, (Color){100, 100, 100, 255}); DrawTextEx(customFont, hoveredItemName.c_str(), (Vector2){(float)tx, (float)ty}, 18, 1.0f, WHITE); } if (currentState == PAUSE_MENU) { // Draw dark overlay DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 150 }); const char* pButtons[] = { "Resume", "Open to LAN", "Options", "Save & Exit" }; int pNumButtons = 4; int pBtnWidth = 400; int pBtnHeight = 60; int pBtnSpacing = 15; int pStartY = (currentHeight / 2) - ((pNumButtons * pBtnHeight + (pNumButtons - 1) * pBtnSpacing) / 2); for (int i = 0; i < pNumButtons; i++) { int pPosX = (currentWidth / 2) - (pBtnWidth / 2); int pPosY = pStartY + i * (pBtnHeight + pBtnSpacing); Rectangle btnBounds = { (float)pPosX, (float)pPosY, (float)pBtnWidth, (float)pBtnHeight }; bool isHovered = CheckCollisionPointRec(mousePos, btnBounds); Color baseColor = isHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 }; Color borderColor = isHovered ? (Color){ 200, 200, 200, 255 } : (Color){ 20, 20, 20, 255 }; DrawRectangleRec(btnBounds, baseColor); DrawRectangleLinesEx(btnBounds, 3.0f, borderColor); if (isHovered) { DrawRectangleLinesEx((Rectangle){btnBounds.x - 2, btnBounds.y - 2, btnBounds.width + 4, btnBounds.height + 4}, 2.0f, (Color){255, 255, 255, 50}); if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { if (i == 0) { // Resume currentState = GAMEPLAY; inventoryOpen = false; DisableCursor(); } else if (i == 1) { // Open to LAN if (serverSocket == INVALID_SOCKET_VAL) { serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket != INVALID_SOCKET_VAL) { SetNonBlocking(serverSocket); struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(12345); bind(serverSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serverSocket, 5); serverMode = true; chatLog.push_back({ "World opened to LAN on port 12345", 5.0f }); currentState = GAMEPLAY; DisableCursor(); } } } else if (i == 2) { // Options optionsReturnState = PAUSE_MENU; currentState = OPTIONS_MENU; } else if (i == 3) { // Save & Exit currentState = MAIN_MENU; inventoryOpen = false; // Save player position and gameTime to world.dat std::ofstream wf("saves/" + currentWorldName + "/world.dat"); if (wf.is_open()) { wf << globalSeedHash << " " << camera3D.position.x << " " << camera3D.position.y << " " << camera3D.position.z << " " << gameTime; wf.close(); } // Save inventory std::ofstream invf("saves/" + currentWorldName + "/inventory.dat", std::ios::binary); if (invf.is_open()) { invf.write((char*)hotbar, sizeof(hotbar)); invf.write((char*)inventory, sizeof(inventory)); invf.write((char*)&activeHotbarSlot, sizeof(activeHotbarSlot)); invf.close(); } // Save Chests (v2.3.3) std::ofstream cf("saves/" + currentWorldName + "/chests.dat", std::ios::binary); if (cf.is_open()) { uint32_t count = (uint32_t)chestInventories.size(); cf.write((char*)&count, 4); for (auto const& [key, inv] : chestInventories) { cf.write((char*)&key, 8); for (int j=0; j<27; j++) { cf.write((char*)&inv[j].blockType, 4); cf.write((char*)&inv[j].count, 4); cf.write((char*)&inv[j].durability, 4); } } cf.close(); } // Save Torches (v2.3.3) std::ofstream tf("saves/" + currentWorldName + "/torches.dat", std::ios::binary); if (tf.is_open()) { uint32_t count = (uint32_t)torchPositions.size(); tf.write((char*)&count, 4); if (count > 0) tf.write((char*)torchPositions.data(), count * sizeof(Vector3)); tf.close(); } // Save chunks and clear memory for (auto& pair : worldChunks) { SaveChunk(pair.second, pair.first.x, pair.first.z); delete pair.second; } worldChunks.clear(); currentWorldName = ""; } } } Vector2 textSize = MeasureTextEx(customFont, pButtons[i], 24, 1.0f); DrawTextEx(customFont, pButtons[i], (Vector2){ (float)pPosX + (pBtnWidth / 2) - (textSize.x / 2) + 2, (float)pPosY + (pBtnHeight / 2) - (textSize.y / 2) + 2 }, 24, 1.0f, (Color){ 30, 30, 30, 255 }); Color textColor = isHovered ? (Color){ 255, 255, 160, 255 } : (Color){ 220, 220, 220, 255 }; DrawTextEx(customFont, pButtons[i], (Vector2){ (float)pPosX + (pBtnWidth / 2) - (textSize.x / 2), (float)pPosY + (pBtnHeight / 2) - (textSize.y / 2) }, 24, 1.0f, textColor); } } } EndDrawing(); //---------------------------------------------------------------------------------- } // De-Initialization //-------------------------------------------------------------------------------------- for (int i = 1; i <= 5; i++) UnloadTexture(blockTextures[i]); for (int i = 7; i <= 11; i++) UnloadTexture(blockTextures[i]); UnloadTexture(grassTopTexture); UnloadTexture(titleTexture); // Texture unloading for (auto& pair : worldChunks) { delete pair.second; } UnloadFont(customFont); // Font unloading UnloadMusicStream(titleMusic); // Music unloading UnloadMusicStream(gameplayMusic); CloseAudioDevice(); // Close audio device CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; }