diff --git a/MorriCraft-Windows.zip b/MorriCraft-Windows.zip index 17f3bd7..ce04d5a 100644 Binary files a/MorriCraft-Windows.zip and b/MorriCraft-Windows.zip differ diff --git a/README.md b/README.md index 1dd6e7c..53cff03 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,38 @@ A pre-built `MorriCraft-Windows.zip` is available in the repository root. ## 📜 Version History -### v2.1.9 - Crafting Overhaul (Latest) +### v2.2.6 - Physics & World Management (Current) +- **7:00 AM Spawn**: New worlds now start in the morning for immediate daylight. +- **Naming System**: Cleaner sequential naming (`World`, `World 1`, `World 2`) instead of nested parentheses. +- **Anti-Stuck Physics**: Active collision resolution pushes players out of blocks if they overlap. +- **Build Path**: Windows artifacts are now stored in `build-windows/` for easier deployment. + +### v2.2.5 - UX & Inventory Polish +- **Shift-Click**: Holding Shift and clicking picks up entire item stacks instantly. +- **Scrollable Menus**: Added scroll bars and mouse-wheel support for the "Load World" menu. +- **UI Padding**: Increased padding across all panels to ensure text never overlaps borders. + +### v2.2.4 - Visual & Update Stability +- **X-Ray Fix**: Neighborhood chunk dirtying ensures internal faces are culled immediately. +- **Smart Updates**: Client only prompts for updates if the remote version is strictly higher. +- **15x15 Pre-Gen**: Expanded spawn area pre-generation (225 chunks) to eliminate horizon drop-offs. + +### v2.2.3 - Async Generation & Help +- **Loading Screen**: Added a progress bar for asynchronous world generation/loading. +- **Help Command**: Added `/help` to chat to display all available console commands. +- **Spawn Surface**: Guaranteed surface placement after full chunk pre-generation. + +### v2.2.1 - World & Seed Polish +- **Cliff Fix**: Resolved issues where new worlds would spawn with missing chunks. +- **Render Distance**: Increased default render distance to 4 for a more expansive view. +- **Auto-Updater**: Platform-aware updates target Windows vs Linux binaries. + +### v2.2.0 - Biome & Generation Update +- **Seed Fix**: Proper avalanche-hash for seeds, making every world truly unique. +- **Biomes**: Added Grassland, Desert, and Rocky biomes with sand beaches at sea level. +- **Commands**: Added `/seed` command to view the world seed in chat. + +### v2.1.9 - Crafting Overhaul - **20+ Recipes**: Added Log→Planks and full wooden/stone tool tier crafting. - **Smart Inventory**: Left-click stacks same items, right-click picks up or places one. - **Future Item IDs**: Added furnace, chest, ladder, fence, torch, door, and stone slab types. diff --git a/build-linux/MorriCraft b/build-linux/MorriCraft index 2d4a41c..1e6cb8f 100755 Binary files a/build-linux/MorriCraft and b/build-linux/MorriCraft differ diff --git a/build-linux/assets/version.txt b/build-linux/assets/version.txt index 9600f9b..437a354 100644 --- a/build-linux/assets/version.txt +++ b/build-linux/assets/version.txt @@ -1 +1 @@ -v2.1.9 +v2.2.6 diff --git a/build-windows/MorriCraft.exe b/build-windows/MorriCraft.exe index dc87386..b1702d3 100755 Binary files a/build-windows/MorriCraft.exe and b/build-windows/MorriCraft.exe differ diff --git a/build-windows/assets/version.txt b/build-windows/assets/version.txt index 9600f9b..437a354 100644 --- a/build-windows/assets/version.txt +++ b/build-windows/assets/version.txt @@ -1 +1 @@ -v2.1.9 +v2.2.6 diff --git a/src/main.cpp b/src/main.cpp index 4ce1526..23c233c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,7 @@ #define PI 3.1415926535f #endif #define CHUNK_HEIGHT 128 -#define RENDER_DISTANCE 2 +#define RENDER_DISTANCE 4 enum BlockType { AIR = 0, GRASS = 1, DIRT = 2, COBBLESTONE = 3, LOG = 4, LEAVES = 5, PLANK = 6, @@ -71,8 +71,18 @@ std::string GetBlockName(int type) { // Simple 2D Perlin Noise implementation float dotGridGradient(int ix, int iy, float x, float y, unsigned int seed) { - unsigned int hash = ix * 3284157443 ^ iy * 1911520717; - hash ^= 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); @@ -137,7 +147,7 @@ static float playerHealth = 16.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, CONNECT_MENU, SKIN_EDITOR }; +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, CONNECT_MENU, SKIN_EDITOR, WORLD_CREATION_PROGRESS }; // Forward Declarations bool IsExposedOptimized(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP); @@ -183,6 +193,13 @@ 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 = 225; // 15x15 area around spawn +static bool isNewWorldGeneration = false; +static float spawnSavedX = 0, spawnSavedY = 0, spawnSavedZ = 0; +static int loadWorldScrollOffset = 0; // Adds one block to inventory: first fills existing stacks, then empty slots. void AddToInventory(int blockType) { @@ -382,10 +399,20 @@ std::string GetRemoteVersion() { pclose(pipe); // Trim result size_t last = result.find_last_not_of(" \n\r\t"); - if (last != std::string::npos) result = result.substr(0, last + 1); + if (last != std::string::npos) result.erase(last + 1); return result; } +bool IsVersionNewer(std::string remote, std::string local) { + if (remote.empty() || remote == "error" || remote == local) return false; + auto strip = [](std::string s) { + if (!s.empty() && s[0] == 'v') return s.substr(1); + return s; + }; + // Simple lexicographical comparison for v2.x.x format + return strip(remote) > strip(local); +} + void SaveConfig() { std::ofstream file("config.cfg"); if (file.is_open()) { @@ -491,11 +518,13 @@ float FindSpawnY(int spawnX, int spawnZ) { 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), eye is 1.6 above feet - return (float)y + 0.5f + 1.6f; + // 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. + return (float)y + 0.6f + 1.6f; } } - return 40.0f; // Fallback + return 64.0f; // Safer fallback (above typical y=32 ground) } void GenerateChunk(int cx, int cz) { @@ -520,24 +549,44 @@ void GenerateChunk(int cx, int cz) { float worldX = cx * CHUNK_SIZE + x; float worldZ = cz * CHUNK_SIZE + z; - // Multi-octave fbm noise at a much lower frequency for broad, smooth hills. - // Base freq 0.006 means features span ~160 blocks, preventing steep cliffs. - float noiseVal = fbm(worldX * 0.006f, worldZ * 0.006f, globalSeedHash); - // Terrain sits around Y=32 with only ±6 blocks of variation, - // so the minimum ground level is 26 -- no sudden deep holes. - int height = 32 + (int)(noiseVal * 6.0f); + // 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); if (height < 10) height = 10; if (height >= CHUNK_HEIGHT - 2) height = CHUNK_HEIGHT - 2; + // Biome noise: determines surface type + float biomeNoise = fbm(worldX * 0.008f, worldZ * 0.008f, globalSeedHash + 77777); + for (int y = 0; y <= height; y++) { if (y == 0) { newChunk->blocks[x][y][z] = BEDROCK; } else if (y == height) { - newChunk->blocks[x][y][z] = GRASS; + // 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) { - // 4 blocks of dirt under the surface — thick enough that ores - // can never be exposed by normal terrain features. - newChunk->blocks[x][y][z] = DIRT; + // 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; @@ -576,12 +625,19 @@ void GenerateChunk(int cx, int cz) { newChunk->generated = true; newChunk->modified = true; // Newly generated, so we should save it worldChunks[key] = newChunk; + + // Mark neighbors as dirty so they rebuild their render lists with new occlusion data + 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.3f, pos.y - 1.5f, pos.z - 0.3f }, - (Vector3){ pos.x + 0.3f, pos.y + 0.1f, pos.z + 0.3f } + (Vector3){ pos.x - 0.26f, pos.y - 1.45f, pos.z - 0.26f }, + (Vector3){ pos.x + 0.26f, pos.y + 0.05f, pos.z + 0.26f } }; int minX = (int)floorf(playerBox.min.x + 0.5f); @@ -843,7 +899,7 @@ int main(void) // 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.1.9"); + InitWindow(screenWidth, screenHeight, "MorriCraft v2.2.6"); LoadConfig(); SetExitKey(KEY_NULL); // Prevent ESC from closing the window @@ -925,7 +981,7 @@ int main(void) bool isChatting = false; char chatInput[128] = {0}; uint32_t myNetID = 0; // Assigned by server if client - float gameTime = 75.0f; // Start at 6:00 AM + float gameTime = 87.5f; // Start at 7:00 AM float breakProgress = 0.0f; int lastHitX = -1, lastHitY = -1, lastHitZ = -1; float swingTime = 0.0f; @@ -1575,20 +1631,44 @@ int main(void) if (IsKeyPressed(KEY_ENTER)) { if (isChatting) { if (strlen(chatInput) > 0) { - // Send Chat Packet - 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)); + 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 on") { + isFlying = true; + chatLog.push_back({ "[Server] Flight enabled (Noclip ON)", 5.0f }); + } else if (cmd == "fly off") { + isFlying = false; + chatLog.push_back({ "[Server] Flight disabled", 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 }); + } 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'; @@ -1642,23 +1722,31 @@ int main(void) if (Vector3Length(moveVec) > 0) { moveVec = Vector3Normalize(moveVec); - float speed = 5.0f * GetFrameTime(); + float speed = (isFlying ? 15.0f : 5.0f) * GetFrameTime(); Vector3 tryX = { oldPos.x + moveVec.x * speed, oldPos.y, oldPos.z }; - if (!CheckPlayerCollision(tryX)) camera3D.position.x = tryX.x; + if (isFlying || !CheckPlayerCollision(tryX)) camera3D.position.x = tryX.x; Vector3 tryZ = { camera3D.position.x, oldPos.y, oldPos.z + moveVec.z * speed }; - if (!CheckPlayerCollision(tryZ)) camera3D.position.z = tryZ.z; + if (isFlying || !CheckPlayerCollision(tryZ)) camera3D.position.z = tryZ.z; } - // ---- Vertical Physics (Ground-Lock System) ---- - if (isGrounded) { + // ---- 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.1f) + 0.5f; - camera3D.position.y = expectedFeetY + 1.6f + 0.01f; + float expectedFeetY = floorf(feetY + 0.15f) + 0.5f; + camera3D.position.y = expectedFeetY + 1.6f + 0.03f; // Jump if (IsKeyPressed(KEY_SPACE) && !inventoryOpen && !isChatting) { @@ -1682,7 +1770,7 @@ int main(void) if (playerVelocityY < 0.0f) { // Landed: Lock to surface float feetY = nextYPos.y - 1.6f; - camera3D.position.y = floorf(feetY + 0.1f) + 0.5f + 1.6f + 0.01f; + camera3D.position.y = floorf(feetY + 0.15f) + 0.5f + 1.6f + 0.03f; playerVelocityY = 0.0f; isGrounded = true; } else { @@ -1692,8 +1780,14 @@ int main(void) } } else { camera3D.position.y = nextYPos.y; + isGrounded = false; } } + + // Resolve any lingering overlap to prevent getting stuck + if (!isFlying && CheckPlayerCollision(camera3D.position)) { + camera3D.position.y += 0.05f; + } // Final Camera state camera3D.target.x = camera3D.position.x + sinf(camYaw) * cosf(camPitch); @@ -1887,7 +1981,7 @@ int main(void) // Perform real check after 1 second if (updateTimer > 1.0f && latestVersion == "") { latestVersion = GetRemoteVersion(); - if (latestVersion != "error" && latestVersion != localVersion) { + if (IsVersionNewer(latestVersion, localVersion)) { updateReady = true; } } @@ -1929,20 +2023,36 @@ int main(void) if (!startedDownload) { startedDownload = true; - // REAL DOWNLOAD ATTEMPT + // REAL DOWNLOAD ATTEMPT - Platform-aware URLs + #ifdef _WIN32 + std::string binaryUrl = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/build-windows/MorriCraft.exe"; + std::string binaryName = "MorriCraft.exe"; + #else std::string binaryUrl = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/build-linux/MorriCraft"; + std::string binaryName = "MorriCraft"; + #endif std::string versionUrl = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/version.txt"; - // We run these in background-ish or just block for a bit since it's a small app - int r1 = system(("curl -L -s -o MorriCraft.new " + binaryUrl).c_str()); - int r2 = system(("curl -L -s -o version.txt.new " + versionUrl).c_str()); + // Download new binary and version file + int r1 = system(("curl -L -s -o " + binaryName + ".new \"" + binaryUrl + "\"").c_str()); + int r2 = system(("curl -L -s -o version.txt.new \"" + versionUrl + "\"").c_str()); if (r1 == 0 && r2 == 0) { - system("chmod +x MorriCraft.new"); - system("mv MorriCraft MorriCraft.old 2>/dev/null"); - system("mv MorriCraft.new MorriCraft"); + #ifdef _WIN32 + // Windows: rename old, move new into place + system(("rename " + binaryName + " " + binaryName + ".old").c_str()); + system(("rename " + binaryName + ".new " + binaryName).c_str()); + system("rename version.txt version.txt.old"); + system("rename version.txt.new version.txt"); + system("copy version.txt assets\\version.txt >nul 2>&1"); + #else + // Linux: chmod, move + system(("chmod +x " + binaryName + ".new").c_str()); + system(("mv " + binaryName + " " + binaryName + ".old 2>/dev/null").c_str()); + system(("mv " + binaryName + ".new " + binaryName).c_str()); system("mv version.txt.new version.txt"); system("cp version.txt assets/version.txt 2>/dev/null"); + #endif fakeProgress = 1.0f; } else { downloadFailed = true; @@ -2139,6 +2249,76 @@ int main(void) activeNetField = 0; } + } else if (currentState == WORLD_CREATION_PROGRESS) { + // UPDATE: Generate 10 chunks per frame (faster for the larger area) + int spiralSize = 15; + int spawnCX = (int)floorf(spawnSavedX / CHUNK_SIZE); + int spawnCZ = (int)floorf(spawnSavedZ / CHUNK_SIZE); + + for (int i = 0; i < 10 && chunksGeneratedCount < totalChunksToPreGen; i++) { + int cx_off = (chunksGeneratedCount % spiralSize) - 7; + int cz_off = (chunksGeneratedCount / spiralSize) - 7; + 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(0, 0); + camera3D.position = (Vector3){ 0.0f, spawnY, 0.0f }; + camera3D.target = (Vector3){ 0.0f, spawnY, 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); + } 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(); + } + } + playerVelocityY = 0.0f; + camYaw = 0.0f; camPitch = 0.0f; + 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 @@ -2149,8 +2329,8 @@ int main(void) DrawTexturePro(titleTexture, sourceRec, destRec, origin, 0.0f, WHITE); EndMode2D(); - // Show Version Number (v2.1.9) in Red - DrawTextEx(customFont, "v2.1.9", (Vector2){ (float)currentWidth - 140, (float)currentHeight - 40 }, 22, 1.0f, RED); + // Show Version Number (v2.2.6) in Red + DrawTextEx(customFont, "v2.2.6", (Vector2){ (float)currentWidth - 140, (float)currentHeight - 40 }, 22, 1.0f, RED); // --- PLAYER NAME POPUP (IF MISSING) --- if (playerName == "") { @@ -2249,15 +2429,19 @@ int main(void) } 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 = 600; - int panelHeight = 500; + 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 + 20, (float)panelY + 20 }, 32, 1.0f, WHITE); + DrawTextEx(customFont, "Load World", (Vector2){ (float)panelX + 30, (float)panelY + 30 }, 32, 1.0f, WHITE); // List saves std::vector savedWorlds; @@ -2268,20 +2452,38 @@ int main(void) } } } + + 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 { - for (size_t i = 0; i < savedWorlds.size() && i < 5; i++) { - Rectangle worldBtn = { (float)panelX + 40, (float)panelY + 100 + (float)(i * 60), 520, 50 }; + // 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 + 5, 40, 40 }; + 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[i].c_str(), (Vector2){ worldBtn.x + 10, worldBtn.y + 15 }, 20, 1.0f, WHITE); + 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); @@ -2290,79 +2492,51 @@ int main(void) if (!showDeleteConfirm) { if (isDeleteHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { - deletingWorldName = savedWorlds[i]; + deletingWorldName = savedWorlds[idx]; showDeleteConfirm = true; } else if (isHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { // Load this world - currentWorldName = savedWorlds[i]; - - // Load seed and player position from world.dat - std::ifstream worldFile("saves/" + currentWorldName + "/world.dat"); - float savedX = 0.0f, savedY = -1.0f, savedZ = 0.0f; - if (worldFile.is_open()) { - worldFile >> globalSeedHash >> savedX >> savedY >> savedZ; - worldFile.close(); - } else { - globalSeedHash = 12345; - } - - // Clear old chunks just in case - 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); + 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; + worldFile.close(); + } else { + globalSeedHash = 12345; + spawnSavedX = 0; spawnSavedY = -1; spawnSavedZ = 0; } + + // 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; } - - currentState = GAMEPLAY; - DisableCursor(); - inventoryOpen = false; - - // Restore saved position, or find surface if no save - float spawnY; - if (savedY > 0.0f) { - spawnY = savedY; - } else { - spawnY = FindSpawnY((int)savedX, (int)savedZ); - } - camera3D.position = (Vector3){ savedX, spawnY, savedZ }; - camera3D.target = (Vector3){ savedX, spawnY, savedZ + 1.0f }; - playerVelocityY = 0.0f; - camYaw = 0.0f; camPitch = 0.0f; - - // Load inventory from disk - 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(); - } - - // Pre-generate all chunks around spawn so world appears immediately - int spawnCX = (int)floorf(savedX / CHUNK_SIZE); - int spawnCZ = (int)floorf(savedZ / CHUNK_SIZE); - for (int cx = spawnCX - RENDER_DISTANCE; cx <= spawnCX + RENDER_DISTANCE; cx++) - for (int cz = spawnCZ - RENDER_DISTANCE; cz <= spawnCZ + RENDER_DISTANCE; cz++) - GenerateChunk(cx, cz); - } } } } // Back Button - int btnWidth = 200; + int btnWidth = 220; // Slightly wider int btnHeight = 50; int backBtnX = panelX + (panelWidth / 2) - (btnWidth / 2); int btnY = panelY + panelHeight - 80; @@ -2531,76 +2705,65 @@ int main(void) } else if (currentState == CREATE_WORLD_MENU) { targetZoom = 1.0f; - - // Draw dark overlay DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 }); - // Draw Create World panel - int panelWidth = 600; - int panelHeight = 500; + 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 }); - // Title - DrawTextEx(customFont, "Create New World", (Vector2){ (float)panelX + 20, (float)panelY + 20 }, 32, 1.0f, WHITE); + 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 + 40, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY); - Rectangle nameBox = { (float)panelX + 40, (float)panelY + 130, 520, 40 }; + 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 + 10, nameBox.y + 10 }, 20, 1.0f, WHITE); + DrawTextEx(customFont, worldName, (Vector2){ nameBox.x + 15, nameBox.y + 12 }, 20, 1.0f, WHITE); - // Blinking cursor if (activeTextBox == 1 && ((int)(GetTime() * 2) % 2 == 0)) { int textW = MeasureTextEx(customFont, worldName, 20, 1.0f).x; - DrawRectangle(nameBox.x + 12 + textW, nameBox.y + 10, 12, 20, LIGHTGRAY); + DrawRectangle(nameBox.x + 17 + textW, nameBox.y + 12, 12, 20, LIGHTGRAY); } // World Seed - DrawTextEx(customFont, "Seed (Optional)", (Vector2){ (float)panelX + 40, (float)panelY + 200 }, 20, 1.0f, LIGHTGRAY); - Rectangle seedBox = { (float)panelX + 40, (float)panelY + 230, 520, 40 }; + 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; - // Click outside to unfocus - if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(mousePos, nameBox) && !CheckCollisionPointRec(mousePos, seedBox)) { - activeTextBox = 0; - } + 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 + 10, seedBox.y + 10 }, 20, 1.0f, WHITE); + 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 + 12 + textW, seedBox.y + 10, 12, 20, LIGHTGRAY); + DrawRectangle(seedBox.x + 17 + textW, seedBox.y + 12, 12, 20, LIGHTGRAY); } - - // Back Button + + // Buttons int btnWidth = 200; int btnHeight = 50; - int backBtnX = panelX + 40; - int btnY = panelY + panelHeight - 80; + 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; - } - + if (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) currentState = MAIN_MENU; + // Create Button - int createBtnX = panelX + panelWidth - 40 - btnWidth; + 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 }); @@ -2610,15 +2773,18 @@ int main(void) if (isCreateHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { currentWorldName = worldName; + std::string baseName = worldName; int nameCounter = 1; while (std::filesystem::exists("saves/" + currentWorldName)) { - currentWorldName = std::string(worldName) + " (" + std::to_string(nameCounter) + ")"; + 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 = 87.5f; // Start new world at 7:00 AM + if (serverMode) { serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket != INVALID_SOCKET_VAL) { @@ -2632,9 +2798,6 @@ int main(void) } } - currentState = GAMEPLAY; - DisableCursor(); // Hide and lock cursor for first-person control - std::filesystem::create_directories("saves/" + currentWorldName); // Simple hash for seed string @@ -2650,27 +2813,12 @@ int main(void) } worldChunks.clear(); - // Generate spawn chunk and place player on surface - float spawnY = FindSpawnY(0, 0); - camera3D.position = (Vector3){ 0.0f, spawnY, 0.0f }; - camera3D.target = (Vector3){ 0.0f, spawnY, 1.0f }; - playerVelocityY = 0.0f; - camYaw = 0.0f; camPitch = 0.0f; // Reset look so WASD matches view - - // Re-write world.dat with correct spawnY now that we know it - std::ofstream worldFile2("saves/" + currentWorldName + "/world.dat"); - if (worldFile2.is_open()) { - worldFile2 << globalSeedHash << " " - << camera3D.position.x << " " - << camera3D.position.y << " " - << camera3D.position.z; - worldFile2.close(); - } - - // New player starts with nothing - for (int i = 0; i < 9; i++) { hotbar[i] = InventorySlot(AIR, 0); } - for (int i = 0; i < 27; i++) { inventory[i] = InventorySlot(AIR, 0); } - activeHotbarSlot = 0; + // Start progress screen + isNewWorldGeneration = true; + chunksGeneratedCount = 0; + worldGenProgress = 0.0f; + spawnSavedX = 0; spawnSavedY = -1; spawnSavedZ = 0; // Fresh world start at origin + currentState = WORLD_CREATION_PROGRESS; } } @@ -3017,7 +3165,21 @@ int main(void) } if (hov && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - if (isResultSlot) { + 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); diff --git a/version.txt b/version.txt index 9600f9b..437a354 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.1.9 +v2.2.6