MorriCraft v2.2.6: 7AM Spawn, Naming Refinements, and Anti-Stuck Physics

This commit is contained in:
Michael Howard 2026-04-24 17:43:12 -05:00
parent 1a1cf3cc5f
commit 091cfab86f
8 changed files with 378 additions and 185 deletions

Binary file not shown.

View File

@ -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.

Binary file not shown.

View File

@ -1 +1 @@
v2.1.9
v2.2.6

Binary file not shown.

View File

@ -1 +1 @@
v2.1.9
v2.2.6

View File

@ -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,9 +1780,15 @@ 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);
camera3D.target.y = camera3D.position.y + sinf(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<std::string> savedWorlds;
@ -2269,19 +2453,37 @@ 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);
}
Rectangle deleteBtn = { worldBtn.x + worldBtn.width - 50, worldBtn.y + 5, 40, 40 };
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[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];
currentWorldName = savedWorlds[idx];
// 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);
// 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);

View File

@ -1 +1 @@
v2.1.9
v2.2.6