commit e5d99193ab029bf017988a82ea6703ebf66c401c Author: Michael Howard Date: Thu Apr 23 14:32:53 2026 -0500 Initial commit - v1.4.2 stable diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0645bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build-linux/ +build-windows/ +saves/ +.vscode/ +.DS_Store +*.o +*.exe +MorriCraft diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..aa7f734 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.20) +project(MorriCraft VERSION 0.1.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) + +# Fetch Raylib +FetchContent_Declare( + raylib + URL https://github.com/raysan5/raylib/archive/refs/tags/5.0.tar.gz +) +FetchContent_MakeAvailable(raylib) + +# Create executable +add_executable(MorriCraft src/main.cpp) + +# Link Raylib +target_link_libraries(MorriCraft PRIVATE raylib) + +# Copy assets to build directory +add_custom_command(TARGET MorriCraft POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/assets + $/assets +) diff --git a/MCSTYLE/MCCoalOre.scn b/MCSTYLE/MCCoalOre.scn new file mode 100644 index 0000000..f929211 Binary files /dev/null and b/MCSTYLE/MCCoalOre.scn differ diff --git a/MCSTYLE/MCIronOre.scn b/MCSTYLE/MCIronOre.scn new file mode 100644 index 0000000..fe7678f Binary files /dev/null and b/MCSTYLE/MCIronOre.scn differ diff --git a/MCSTYLE/MCRedCarpet.scn b/MCSTYLE/MCRedCarpet.scn new file mode 100644 index 0000000..1e05b31 Binary files /dev/null and b/MCSTYLE/MCRedCarpet.scn differ diff --git a/MCSTYLE/MCRedStone.scn b/MCSTYLE/MCRedStone.scn new file mode 100644 index 0000000..7591d76 Binary files /dev/null and b/MCSTYLE/MCRedStone.scn differ diff --git a/MCSTYLE/MCSign.scn b/MCSTYLE/MCSign.scn new file mode 100644 index 0000000..e1afd4b Binary files /dev/null and b/MCSTYLE/MCSign.scn differ diff --git a/MCSTYLE/MCWallSign.scn b/MCSTYLE/MCWallSign.scn new file mode 100644 index 0000000..f7e2eb8 Binary files /dev/null and b/MCSTYLE/MCWallSign.scn differ diff --git a/MCSTYLE/MCbirchlog.scn b/MCSTYLE/MCbirchlog.scn new file mode 100644 index 0000000..a44afd1 Binary files /dev/null and b/MCSTYLE/MCbirchlog.scn differ diff --git a/MCSTYLE/MCbirchplank.scn b/MCSTYLE/MCbirchplank.scn new file mode 100644 index 0000000..dab69f3 Binary files /dev/null and b/MCSTYLE/MCbirchplank.scn differ diff --git a/MCSTYLE/MCbookshelf.scn b/MCSTYLE/MCbookshelf.scn new file mode 100644 index 0000000..e14acf6 Binary files /dev/null and b/MCSTYLE/MCbookshelf.scn differ diff --git a/MCSTYLE/MCbricks.scn b/MCSTYLE/MCbricks.scn new file mode 100644 index 0000000..d39a6a8 Binary files /dev/null and b/MCSTYLE/MCbricks.scn differ diff --git a/MCSTYLE/MCcake.scn b/MCSTYLE/MCcake.scn new file mode 100644 index 0000000..c2e1a5a Binary files /dev/null and b/MCSTYLE/MCcake.scn differ diff --git a/MCSTYLE/MCchest.scn b/MCSTYLE/MCchest.scn new file mode 100644 index 0000000..34152dc Binary files /dev/null and b/MCSTYLE/MCchest.scn differ diff --git a/MCSTYLE/MCcornflower.scn b/MCSTYLE/MCcornflower.scn new file mode 100644 index 0000000..60d89db Binary files /dev/null and b/MCSTYLE/MCcornflower.scn differ diff --git a/MCSTYLE/MCdandelion.scn b/MCSTYLE/MCdandelion.scn new file mode 100644 index 0000000..187826f Binary files /dev/null and b/MCSTYLE/MCdandelion.scn differ diff --git a/MCSTYLE/MCdiamondblock.scn b/MCSTYLE/MCdiamondblock.scn new file mode 100644 index 0000000..0d284b4 Binary files /dev/null and b/MCSTYLE/MCdiamondblock.scn differ diff --git a/MCSTYLE/MCdiamondore.scn b/MCSTYLE/MCdiamondore.scn new file mode 100644 index 0000000..565faed Binary files /dev/null and b/MCSTYLE/MCdiamondore.scn differ diff --git a/MCSTYLE/MCdiamondsword.scn b/MCSTYLE/MCdiamondsword.scn new file mode 100644 index 0000000..4999a48 Binary files /dev/null and b/MCSTYLE/MCdiamondsword.scn differ diff --git a/MCSTYLE/MCdirt.scn b/MCSTYLE/MCdirt.scn new file mode 100644 index 0000000..c89ba5b Binary files /dev/null and b/MCSTYLE/MCdirt.scn differ diff --git a/MCSTYLE/MCfarm.scn b/MCSTYLE/MCfarm.scn new file mode 100644 index 0000000..a29ffad Binary files /dev/null and b/MCSTYLE/MCfarm.scn differ diff --git a/MCSTYLE/MCfurnace.scn b/MCSTYLE/MCfurnace.scn new file mode 100644 index 0000000..e9de4ff Binary files /dev/null and b/MCSTYLE/MCfurnace.scn differ diff --git a/MCSTYLE/MCglass.scn b/MCSTYLE/MCglass.scn new file mode 100644 index 0000000..096d17c Binary files /dev/null and b/MCSTYLE/MCglass.scn differ diff --git a/MCSTYLE/MCglasspane.scn b/MCSTYLE/MCglasspane.scn new file mode 100644 index 0000000..2ef2588 Binary files /dev/null and b/MCSTYLE/MCglasspane.scn differ diff --git a/MCSTYLE/MCgoldore.scn b/MCSTYLE/MCgoldore.scn new file mode 100644 index 0000000..971fb23 Binary files /dev/null and b/MCSTYLE/MCgoldore.scn differ diff --git a/MCSTYLE/MCgrass.scn b/MCSTYLE/MCgrass.scn new file mode 100644 index 0000000..0907e79 Binary files /dev/null and b/MCSTYLE/MCgrass.scn differ diff --git a/MCSTYLE/MCgravel.scn b/MCSTYLE/MCgravel.scn new file mode 100644 index 0000000..b3c8936 Binary files /dev/null and b/MCSTYLE/MCgravel.scn differ diff --git a/MCSTYLE/MClanternGROUND.scn b/MCSTYLE/MClanternGROUND.scn new file mode 100644 index 0000000..c5eb8ca Binary files /dev/null and b/MCSTYLE/MClanternGROUND.scn differ diff --git a/MCSTYLE/MClava.scn b/MCSTYLE/MClava.scn new file mode 100644 index 0000000..8e7263d Binary files /dev/null and b/MCSTYLE/MClava.scn differ diff --git a/MCSTYLE/MCleaves.scn b/MCSTYLE/MCleaves.scn new file mode 100644 index 0000000..8586d99 Binary files /dev/null and b/MCSTYLE/MCleaves.scn differ diff --git a/MCSTYLE/MCoakdoor.scn b/MCSTYLE/MCoakdoor.scn new file mode 100644 index 0000000..2cc85f2 Binary files /dev/null and b/MCSTYLE/MCoakdoor.scn differ diff --git a/MCSTYLE/MCoaklog.scn b/MCSTYLE/MCoaklog.scn new file mode 100644 index 0000000..1f8fa24 Binary files /dev/null and b/MCSTYLE/MCoaklog.scn differ diff --git a/MCSTYLE/MCoakplank.scn b/MCSTYLE/MCoakplank.scn new file mode 100644 index 0000000..df5635a Binary files /dev/null and b/MCSTYLE/MCoakplank.scn differ diff --git a/MCSTYLE/MCoakstairs.scn b/MCSTYLE/MCoakstairs.scn new file mode 100644 index 0000000..d784a2e Binary files /dev/null and b/MCSTYLE/MCoakstairs.scn differ diff --git a/MCSTYLE/MCobsidian.scn b/MCSTYLE/MCobsidian.scn new file mode 100644 index 0000000..d573539 Binary files /dev/null and b/MCSTYLE/MCobsidian.scn differ diff --git a/MCSTYLE/MCpath.scn b/MCSTYLE/MCpath.scn new file mode 100644 index 0000000..bf6a289 Binary files /dev/null and b/MCSTYLE/MCpath.scn differ diff --git a/MCSTYLE/MCpinktulip.scn b/MCSTYLE/MCpinktulip.scn new file mode 100644 index 0000000..8505a52 Binary files /dev/null and b/MCSTYLE/MCpinktulip.scn differ diff --git a/MCSTYLE/MCportal.scn b/MCSTYLE/MCportal.scn new file mode 100644 index 0000000..127389f Binary files /dev/null and b/MCSTYLE/MCportal.scn differ diff --git a/MCSTYLE/MCsand.scn b/MCSTYLE/MCsand.scn new file mode 100644 index 0000000..3dc5b80 Binary files /dev/null and b/MCSTYLE/MCsand.scn differ diff --git a/MCSTYLE/MCsnow.scn b/MCSTYLE/MCsnow.scn new file mode 100644 index 0000000..bf2409b Binary files /dev/null and b/MCSTYLE/MCsnow.scn differ diff --git a/MCSTYLE/MCsoulsand.scn b/MCSTYLE/MCsoulsand.scn new file mode 100644 index 0000000..13931ef Binary files /dev/null and b/MCSTYLE/MCsoulsand.scn differ diff --git a/MCSTYLE/MCsugarcane.scn b/MCSTYLE/MCsugarcane.scn new file mode 100644 index 0000000..4cd392a Binary files /dev/null and b/MCSTYLE/MCsugarcane.scn differ diff --git a/MCSTYLE/MCtallgrass.scn b/MCSTYLE/MCtallgrass.scn new file mode 100644 index 0000000..f2f52b7 Binary files /dev/null and b/MCSTYLE/MCtallgrass.scn differ diff --git a/MCSTYLE/MCtnt.scn b/MCSTYLE/MCtnt.scn new file mode 100644 index 0000000..c18024c Binary files /dev/null and b/MCSTYLE/MCtnt.scn differ diff --git a/MCSTYLE/MCtorch.scn b/MCSTYLE/MCtorch.scn new file mode 100644 index 0000000..bf58dc7 Binary files /dev/null and b/MCSTYLE/MCtorch.scn differ diff --git a/MCSTYLE/MCwater.scn b/MCSTYLE/MCwater.scn new file mode 100644 index 0000000..6e31193 Binary files /dev/null and b/MCSTYLE/MCwater.scn differ diff --git a/MCSTYLE/MCwheat.scn b/MCSTYLE/MCwheat.scn new file mode 100644 index 0000000..44c0f1a Binary files /dev/null and b/MCSTYLE/MCwheat.scn differ diff --git a/MCSTYLE/MCworkbench.scn b/MCSTYLE/MCworkbench.scn new file mode 100644 index 0000000..3048470 Binary files /dev/null and b/MCSTYLE/MCworkbench.scn differ diff --git a/MCSTYLE/cobble.scn b/MCSTYLE/cobble.scn new file mode 100644 index 0000000..148253d Binary files /dev/null and b/MCSTYLE/cobble.scn differ diff --git a/MCSTYLE/cobblestairs.scn b/MCSTYLE/cobblestairs.scn new file mode 100644 index 0000000..000534c Binary files /dev/null and b/MCSTYLE/cobblestairs.scn differ diff --git a/MCSTYLE/mcrose.scn b/MCSTYLE/mcrose.scn new file mode 100644 index 0000000..8dd4b49 Binary files /dev/null and b/MCSTYLE/mcrose.scn differ diff --git a/MCSTYLE/steven.scn b/MCSTYLE/steven.scn new file mode 100644 index 0000000..495b518 Binary files /dev/null and b/MCSTYLE/steven.scn differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a07fc3 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# MorriCraft + +MorriCraft is a high-performance voxel engine built with C++ and Raylib. It features a custom physics engine, dynamic world generation, and an interactive inventory and crafting system. + +## Features +- **Custom Physics**: Stable "Ground-Lock" system prevents camera jitter and ensures smooth movement. +- **Dynamic World Generation**: Multi-octave Perlin noise (FBM) for realistic terrain, including biomes like grass, sand, and forests. +- **Advanced Crafting**: Includes both a 2x2 player inventory grid and a 3x3 workbench system. +- **Optimized Rendering**: Texture batching and neighbor-chunk caching for stable 60+ FPS. +- **Cross-Platform**: Builds for both Linux and Windows. + +## Version History + +### v1.4.2 +- **UI Redesign**: Overhauled the inventory and crafting windows to match the classic Minecraft layout. +- **Character Preview**: Added a player preview placeholder in the inventory. +- **Centered Layout**: All UI grids are now symmetrically centered. + +### v1.4.1 +- **Texture Fixes**: Corrected leaf and log textures. +- **Inventory Stability**: Fixed the double-toggle bug when pressing 'E'. +- **Bug Fixes**: Resolved grass rendering issues and fixed tree generation. + +### v1.4.0 +- **Crafting Table**: Implemented the Crafting Table block and 3x3 grid interaction. +- **Advanced UI**: Centered stack counts and added red versioning to the title screen. + +### v1.3.x +- **Physics**: Implemented "Ground-Lock" stabilization. +- **Inventory**: Added basic drag-and-drop support. +- **Wireframes**: Added real-time block selection highlights. + +## Build Instructions + +### Linux +1. `mkdir build-linux && cd build-linux` +2. `cmake ..` +3. `make -j$(nproc)` + +### Windows (Cross-compile from Linux) +1. `mkdir build-windows && cd build-windows` +2. `cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-windows.cmake ..` +3. `make -j$(nproc)` + +## Controls +- **WASD**: Movement +- **Space**: Jump +- **Mouse**: Look / Aim +- **Left Click**: Destroy Block +- **Right Click**: Place Block / Use Crafting Table +- **E**: Toggle Inventory +- **1-9**: Hotbar Selection +- **ESC**: Pause Menu / Close Inventory diff --git a/assets/Morricraft-day1.mp3 b/assets/Morricraft-day1.mp3 new file mode 100644 index 0000000..2fbeced Binary files /dev/null and b/assets/Morricraft-day1.mp3 differ diff --git a/assets/Morricraft-title.mp3 b/assets/Morricraft-title.mp3 new file mode 100644 index 0000000..cb9c93b Binary files /dev/null and b/assets/Morricraft-title.mp3 differ diff --git a/assets/PixelFont.ttf b/assets/PixelFont.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/assets/PixelFont.ttf differ diff --git a/assets/TitleImage.png b/assets/TitleImage.png new file mode 100644 index 0000000..ac164aa Binary files /dev/null and b/assets/TitleImage.png differ diff --git a/assets/bedrock.png b/assets/bedrock.png new file mode 100644 index 0000000..70b614f Binary files /dev/null and b/assets/bedrock.png differ diff --git a/assets/cobblestone.png b/assets/cobblestone.png new file mode 100644 index 0000000..435a7af Binary files /dev/null and b/assets/cobblestone.png differ diff --git a/assets/crafting_table_side.png b/assets/crafting_table_side.png new file mode 100644 index 0000000..56cb675 Binary files /dev/null and b/assets/crafting_table_side.png differ diff --git a/assets/crafting_table_top.png b/assets/crafting_table_top.png new file mode 100644 index 0000000..acb5a09 Binary files /dev/null and b/assets/crafting_table_top.png differ diff --git a/assets/diamond_ore.png b/assets/diamond_ore.png new file mode 100644 index 0000000..c2333b7 Binary files /dev/null and b/assets/diamond_ore.png differ diff --git a/assets/dirt.png b/assets/dirt.png new file mode 100644 index 0000000..3f70996 Binary files /dev/null and b/assets/dirt.png differ diff --git a/assets/grass.png b/assets/grass.png new file mode 100644 index 0000000..4e3ae00 Binary files /dev/null and b/assets/grass.png differ diff --git a/assets/grass_top.png b/assets/grass_top.png new file mode 100644 index 0000000..6abb3a1 Binary files /dev/null and b/assets/grass_top.png differ diff --git a/assets/gravel.png b/assets/gravel.png new file mode 100644 index 0000000..9551021 Binary files /dev/null and b/assets/gravel.png differ diff --git a/assets/iron_ore.png b/assets/iron_ore.png new file mode 100644 index 0000000..f3f5cd8 Binary files /dev/null and b/assets/iron_ore.png differ diff --git a/assets/plank.png b/assets/plank.png new file mode 100644 index 0000000..e5a1578 Binary files /dev/null and b/assets/plank.png differ diff --git a/assets/sand.png b/assets/sand.png new file mode 100644 index 0000000..2b35d75 Binary files /dev/null and b/assets/sand.png differ diff --git a/assets/stone.png b/assets/stone.png new file mode 100644 index 0000000..c298969 Binary files /dev/null and b/assets/stone.png differ diff --git a/cmake/mingw-w64-x86_64.cmake b/cmake/mingw-w64-x86_64.cmake new file mode 100644 index 0000000..be3f9a5 --- /dev/null +++ b/cmake/mingw-w64-x86_64.cmake @@ -0,0 +1,18 @@ +# the name of the target operating system +set(CMAKE_SYSTEM_NAME Windows) + +# which compilers to use for C and C++ +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +# where is the target environment located +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) + +# adjust the default behavior of the FIND_XXX() commands: +# search programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# search headers and libraries in the target environment +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..76dd953 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,1858 @@ +#include "raylib.h" +#include "raymath.h" +#include "rcamera.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "rlgl.h" + +#define CHUNK_SIZE 32 +#define CHUNK_HEIGHT 128 +#define RENDER_DISTANCE 1 // 3x3 chunks visible for performance + +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 +}; + +// 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; + 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 Chunk { + int blocks[CHUNK_SIZE][CHUNK_HEIGHT][CHUNK_SIZE]; + bool generated; + bool modified; + int maxY; // Highest non-air Y in this chunk; limits the render loop + Chunk() : generated(false), modified(false), maxY(0) {} +}; + +// Global variables for persistence +std::unordered_map worldChunks; +static unsigned int globalSeedHash = 0; +static std::string currentWorldName = ""; + +// ---- Inventory System ---- +struct InventorySlot { + int blockType = AIR; + int count = 0; + InventorySlot() : blockType(AIR), count(0) {} + InventorySlot(int bt, int c) : blockType(bt), count(c) {} +}; + +static InventorySlot hotbar[9]; // 9 hotbar slots +static InventorySlot inventory[27]; // 3x9 main inventory +static int activeHotbarSlot = 0; +static bool inventoryOpen = false; + +// 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 +} + +// 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 +} + +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()) { + int lx = x - (cx * CHUNK_SIZE); + int lz = z - (cz * CHUNK_SIZE); + worldChunks[key]->blocks[lx][y][lz] = type; + worldChunks[key]->modified = true; + // Keep maxY up to date when placing blocks above the current max + if (type != AIR && y > worldChunks[key]->maxY) + worldChunks[key]->maxY = y; + } +} + +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); + +// 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), eye is 1.6 above feet + return (float)y + 0.5f + 1.6f; + } + } + return 40.0f; // Fallback +} + +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; + 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-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); + if (height < 10) height = 10; + if (height >= CHUNK_HEIGHT - 2) height = CHUNK_HEIGHT - 2; + + 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; + } 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; + } 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; // Newly generated, so we should save it + worldChunks[key] = newChunk; +} + +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 } + }; + + 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) { + 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 DrawGrassBlock(Vector3 position, unsigned int sideTexId, unsigned int topTexId, unsigned int botTexId) { + float x = position.x; + float y = position.y; + float z = position.z; + + // ---- SIDES (4 faces) ---- + rlSetTexture(sideTexId); + rlBegin(RL_QUADS); + rlColor4ub(255, 255, 255, 255); + // Front + 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); + // Back + 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); + // Right + 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); + // Left + 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(); + + // ---- TOP (pure green) ---- + rlSetTexture(topTexId); + rlBegin(RL_QUADS); + rlColor4ub(124, 189, 107, 255); // Minecraft grass green tint + 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); + // ---- BOTTOM (pure dirt) ---- + rlSetTexture(botTexId); + rlBegin(RL_QUADS); + rlColor4ub(255, 255, 255, 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(); +} + +// Draws a crafting table with custom top and sides +void DrawCraftingTable(Vector3 position, unsigned int sideTexId, unsigned int topTexId, unsigned int botTexId) { + float x = position.x; float y = position.y; float z = position.z; + + // SIDES + rlSetTexture(sideTexId); + rlBegin(RL_QUADS); + rlColor4ub(255, 255, 255, 255); + // Front, Back, Right, Left + 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); + 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); + 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); + 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(); + + // TOP + rlSetTexture(topTexId); + rlBegin(RL_QUADS); + 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(); + + // BOTTOM (dirt) + rlSetTexture(botTexId); + rlBegin(RL_QUADS); + 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 IsExposedOptimized(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP) { + if (ly > 0) { if (chunk->blocks[lx][ly-1][lz] == AIR) return true; } else return true; + if (ly < CHUNK_HEIGHT - 1) { if (chunk->blocks[lx][ly+1][lz] == AIR) return true; } else return true; + if (lx > 0) { if (chunk->blocks[lx-1][ly][lz] == AIR) return true; } + else if (nxM) { if (nxM->blocks[CHUNK_SIZE-1][ly][lz] == AIR) return true; } else return true; + if (lx < CHUNK_SIZE - 1) { if (chunk->blocks[lx+1][ly][lz] == AIR) return true; } + else if (nxP) { if (nxP->blocks[0][ly][lz] == AIR) return true; } else return true; + if (lz > 0) { if (chunk->blocks[lx][ly][lz-1] == AIR) return true; } + else if (nzM) { if (nzM->blocks[lx][ly][CHUNK_SIZE-1] == AIR) return true; } else return true; + if (lz < CHUNK_SIZE - 1) { if (chunk->blocks[lx][ly][lz+1] == AIR) return true; } + else if (nzP) { if (nzP->blocks[lx][ly][0] == AIR) return true; } else return true; + return false; +} + +// Optimized helper for batched rendering: Draws only the vertices (no texture bind or rlBegin/End) +void DrawCubeVertices(float x, float y, float z, float w, float h, float l) { + float hw = w/2.0f; float hh = h/2.0f; float hl = l/2.0f; + // Front + 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); + // Back + 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); + // Top + 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); + // Bottom + 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); + // Right + 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); + // Left + 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); +} + +// 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 - Title Screen"); + SetExitKey(KEY_NULL); // Prevent ESC from closing the window + + // Initialize audio device + InitAudioDevice(); + + // Load the title image, music, and font + // NOTE: Textures and Fonts MUST be loaded after Window initialization + Texture2D titleTexture = LoadTexture("assets/TitleImage.png"); + Font customFont = LoadFontEx("assets/PixelFont.ttf", 32, 0, 250); + Music titleMusic = LoadMusicStream("assets/Morricraft-title.mp3"); + titleMusic.looping = true; + PlayMusicStream(titleMusic); + + Music gameplayMusic = LoadMusicStream("assets/Morricraft-day1.mp3"); + gameplayMusic.looping = true; + PlayMusicStream(gameplayMusic); + SetMusicVolume(gameplayMusic, 0.0f); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + float playerVelocityY = 0.0f; + bool isGrounded = false; + + enum MenuState { MAIN_MENU, OPTIONS_MENU, CREATE_WORLD_MENU, LOAD_WORLD_MENU, GAMEPLAY, PAUSE_MENU, CRAFTING_GUI }; + MenuState currentState = MAIN_MENU; + MenuState optionsReturnState = MAIN_MENU; + + InventorySlot mouseHeldItem(AIR, 0); + + float targetZoom = 1.1f; + float currentZoom = 1.1f; + + float masterMusicVolume = 1.0f; + float masterSoundVolume = 1.0f; + + char worldName[64] = "New World"; + int worldNameLen = 9; + char worldSeed[64] = ""; + int worldSeedLen = 0; + int activeTextBox = 0; // 0 = none, 1 = name, 2 = seed + + bool showDeleteConfirm = false; + std::string deletingWorldName = ""; + + Camera2D camera = { 0 }; + camera.offset = (Vector2){ screenWidth/2.0f, screenHeight/2.0f }; + camera.target = (Vector2){ screenWidth/2.0f, screenHeight/2.0f }; + camera.rotation = 0.0f; + camera.zoom = 1.1f; + + // 3D Camera Setup + Camera3D camera3D = { 0 }; + camera3D.position = (Vector3){ 0.0f, 60.0f, 0.0f }; // Spawn high, gravity drops player to surface + camera3D.target = (Vector3){ 0.0f, 60.0f, 1.0f }; + camera3D.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + camera3D.fovy = 70.0f; + camera3D.projection = CAMERA_PERSPECTIVE; + + // 3D Block Textures Setup + Texture2D blockTextures[16] = {0}; + blockTextures[DIRT] = LoadTexture("assets/dirt.png"); + blockTextures[GRASS] = LoadTexture("assets/grass.png"); + blockTextures[COBBLESTONE] = LoadTexture("assets/cobblestone.png"); + blockTextures[LOG] = LoadTexture("assets/plank.png"); // Fallback for missing log.png + blockTextures[LEAVES] = blockTextures[GRASS]; // Leaves share grass texture (tinted in render loop) + blockTextures[PLANK] = LoadTexture("assets/plank.png"); + blockTextures[SAND] = LoadTexture("assets/sand.png"); + blockTextures[STONE] = LoadTexture("assets/stone.png"); + blockTextures[BEDROCK] = LoadTexture("assets/bedrock.png"); + blockTextures[DIAMOND_ORE] = LoadTexture("assets/diamond_ore.png"); + blockTextures[IRON_ORE] = LoadTexture("assets/iron_ore.png"); + blockTextures[GRAVEL] = LoadTexture("assets/gravel.png"); + + // Grass needs separate top/side/bottom textures for correct look + Texture2D grassTopTexture = LoadTexture("assets/grass_top.png"); + Texture2D craftingSideTexture = LoadTexture("assets/crafting_table_side.png"); + Texture2D craftingTopTexture = LoadTexture("assets/crafting_table_top.png"); + blockTextures[CRAFTING_TABLE] = craftingSideTexture; // Default preview + + // Inventory Crafting State + InventorySlot craftingSlots[4]; // Default to AIR/0 + InventorySlot craftingResult(AIR, 0); + + // Crafting Table State (3x3) + InventorySlot tableSlots[9]; + InventorySlot tableResult(AIR, 0); + + auto UpdateCrafting = [&]() { + // 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); + } else { + craftingResult = InventorySlot(AIR, 0); + } + }; + + auto UpdateTableCrafting = [&]() { + // Example: 1 plank -> 4 buttons? No, let's just keep it empty for now or add one. + 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 + //---------------------------------------------------------------------------------- + UpdateMusicStream(titleMusic); + UpdateMusicStream(gameplayMusic); + + // 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 (currentState == GAMEPLAY || currentState == PAUSE_MENU) { + crossfade += 0.02f; + if (crossfade > 1.0f) crossfade = 1.0f; + } else { + crossfade -= 0.02f; + if (crossfade < 0.0f) crossfade = 0.0f; + } + + SetMusicVolume(titleMusic, titleLoopFade * masterMusicVolume * (1.0f - crossfade)); + SetMusicVolume(gameplayMusic, masterMusicVolume * crossfade); + + // 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; + + // Gameplay Update + if (currentState == GAMEPLAY) { + // 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; + + // Toggle inventory + if (IsKeyPressed(KEY_E)) { + inventoryOpen = !inventoryOpen; + if (inventoryOpen) EnableCursor(); + else DisableCursor(); + } + + // 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); + } + } + + // ---- Manual Camera System (Look + WASD) ---- + if (!inventoryOpen) { + 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) { + 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 = 5.0f * GetFrameTime(); + + Vector3 tryX = { oldPos.x + moveVec.x * speed, oldPos.y, oldPos.z }; + if (!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; + } + + // ---- Vertical Physics (Ground-Lock System) ---- + 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; + + // Jump + if (IsKeyPressed(KEY_SPACE) && !inventoryOpen) { + 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.1f) + 0.5f + 1.6f + 0.01f; + 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; + } + } + + // 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) { + 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 (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + int targetBlock = GetBlock(hitX, hitY, hitZ); + if (targetBlock != BEDROCK) { + AddToInventory(targetBlock); + SetBlock(hitX, hitY, hitZ, AIR); + } + } else if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { + int targetBlock = GetBlock(hitX, hitY, hitZ); + if (targetBlock == CRAFTING_TABLE) { + currentState = CRAFTING_GUI; + EnableCursor(); + } else if (hotbar[activeHotbarSlot].count > 0) { + int placeX = hitX + (int)roundf(closestNormal.x); + int placeY = hitY + (int)roundf(closestNormal.y); + int placeZ = hitZ + (int)roundf(closestNormal.z); + SetBlock(placeX, placeY, placeZ, hotbar[activeHotbarSlot].blockType); + hotbar[activeHotbarSlot].count--; + if (hotbar[activeHotbarSlot].count == 0) + hotbar[activeHotbarSlot].blockType = AIR; + } + } + } + } + + 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); + + 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 (v1.4.2) in Red + DrawTextEx(customFont, "v1.4.2", (Vector2){ (float)currentWidth - 140, (float)currentHeight - 40 }, 22, 1.0f, RED); + } + + Vector2 mousePos = GetMousePosition(); + + if (currentState == MAIN_MENU) { + targetZoom = 1.1f; + + // 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 == 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 }); + + int panelWidth = 600; + 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, (Color){ 100, 100, 100, 255 }); + DrawTextEx(customFont, "Load World", (Vector2){ (float)panelX + 20, (float)panelY + 20 }, 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.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 }; + + Rectangle deleteBtn = { worldBtn.x + worldBtn.width - 50, worldBtn.y + 5, 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); + + // 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[i]; + 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(); + + 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 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 = 400; + int confHeight = 200; + int confX = (currentWidth / 2) - (confWidth / 2); + int confY = (currentHeight / 2) - (confHeight / 2); + + DrawRectangle(confX, confY, confWidth, confHeight, (Color){ 50, 20, 20, 255 }); + DrawRectangleLinesEx((Rectangle){ (float)confX, (float)confY, (float)confWidth, (float)confHeight }, 4.0f, RED); + + Vector2 warnSize = MeasureTextEx(customFont, "Delete World?", 24, 1.0f); + DrawTextEx(customFont, "Delete World?", (Vector2){ confX + (confWidth/2) - (warnSize.x/2), confY + 30 }, 24, 1.0f, WHITE); + + std::string warnText = "Delete '" + deletingWorldName + "'?"; + Vector2 nameSize = MeasureTextEx(customFont, warnText.c_str(), 20, 1.0f); + DrawTextEx(customFont, warnText.c_str(), (Vector2){ confX + (confWidth/2) - (nameSize.x/2), confY + 70 }, 20, 1.0f, LIGHTGRAY); + + // Yes Button + Rectangle yesBtn = { (float)confX + 50, (float)confY + 130, 120, 40 }; + bool isYesHovered = CheckCollisionPointRec(mousePos, yesBtn); + DrawRectangleRec(yesBtn, isYesHovered ? RED : MAROON); + DrawRectangleLinesEx(yesBtn, 2.0f, isYesHovered ? WHITE : GRAY); + DrawTextEx(customFont, "YES", (Vector2){ yesBtn.x + 40, yesBtn.y + 10 }, 20, 1.0f, WHITE); + + // No Button + Rectangle noBtn = { (float)confX + confWidth - 170, (float)confY + 130, 120, 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 = 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); + + // 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 == 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 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); + + // 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 }; + + 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); + + // 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); + } + + // 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 }; + + 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; + } + + 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); + + 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); + } + + // Back Button + int btnWidth = 200; + int btnHeight = 50; + int backBtnX = panelX + 40; + 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 (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + currentState = MAIN_MENU; + } + + // Create Button + int createBtnX = panelX + panelWidth - 40 - btnWidth; + 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; + int nameCounter = 1; + while (std::filesystem::exists("saves/" + currentWorldName)) { + currentWorldName = std::string(worldName) + " (" + 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); + + currentState = GAMEPLAY; + DisableCursor(); // Hide and lock cursor for first-person control + + 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(); + + // 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(); + } + + // Give starter kit to new player + for (int i = 0; i < 9; i++) { hotbar[i] = InventorySlot(AIR, 0); } + for (int i = 0; i < 27; i++) { inventory[i] = InventorySlot(AIR, 0); } + hotbar[0] = InventorySlot(DIRT, 64); + hotbar[1] = InventorySlot(COBBLESTONE, 64); + hotbar[2] = InventorySlot(PLANK, 64); + hotbar[3] = InventorySlot(SAND, 64); + hotbar[4] = InventorySlot(GRASS, 64); + activeHotbarSlot = 0; + } + } + + // Draw Gameplay overlay if we entered gameplay + if (currentState == GAMEPLAY || currentState == PAUSE_MENU) { + // Clear the background to a sky color + ClearBackground((Color){ 135, 206, 235, 255 }); // Sky Blue + + BeginMode3D(camera3D); + + int playerCX = (int)floorf(camera3D.position.x / CHUNK_SIZE); + int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE); + + // Optimized Render Loop: Pre-fetch neighbor chunks to eliminate millions of map lookups + for (int renderType = 1; renderType <= 13; renderType++) { + if (renderType == GRASS) { + for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { + for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { + auto it = worldChunks.find({cx, cz}); + if (it == worldChunks.end()) continue; + Chunk* chunk = it->second; + + 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++) { + if (chunk->blocks[lx][ly][lz] != GRASS) continue; + if (!IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) continue; + DrawGrassBlock((Vector3){(float)(worldX+lx), (float)ly, (float)(worldZ+lz)}, + blockTextures[GRASS].id, grassTopTexture.id, blockTextures[DIRT].id); + } + } + } + } + } + } else if (renderType == CRAFTING_TABLE) { + for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { + for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { + auto it = worldChunks.find({cx, cz}); + if (it == worldChunks.end()) continue; + Chunk* chunk = it->second; + + 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++) { + if (chunk->blocks[lx][ly][lz] != CRAFTING_TABLE) continue; + if (!IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) continue; + DrawCraftingTable((Vector3){(float)(worldX+lx), (float)ly, (float)(worldZ+lz)}, + craftingSideTexture.id, craftingTopTexture.id, blockTextures[DIRT].id); + } + } + } + } + } + } else { + unsigned int currentTexId = blockTextures[renderType].id; + rlSetTexture(currentTexId); + rlBegin(RL_QUADS); + rlColor4ub(255, 255, 255, 255); + if (renderType == LEAVES) rlColor4ub(40, 140, 40, 255); + + for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { + for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { + auto it = worldChunks.find({cx, cz}); + if (it == worldChunks.end()) continue; + Chunk* chunk = it->second; + + 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++) { + if (chunk->blocks[lx][ly][lz] != renderType) continue; + if (!IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) continue; + DrawCubeVertices((float)(worldX+lx), (float)ly, (float)(worldZ+lz), 1.0f, 1.0f, 1.0f); + } + } + } + } + } + rlEnd(); + } + } + // Draw Block Selection Wireframe + if (hitBlock && !inventoryOpen) { + DrawCubeWires((Vector3){(float)hitX, (float)hitY, (float)hitZ}, 1.02f, 1.02f, 1.02f, BLACK); + } + + EndMode3D(); + + // 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); + + // ---- 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; + + // 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); + } + + // 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}); + + 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) { + 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); + } + 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 (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 { + InventorySlot tmp = slot; + slot = mouseHeldItem; + mouseHeldItem = tmp; + 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; + 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); + } + } + + if (currentState == PAUSE_MENU) { + // Draw dark overlay + DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 150 }); + + const char* pButtons[] = { "Resume", "Options", "Save & Exit" }; + int pNumButtons = 3; + 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; + DisableCursor(); + } else if (i == 1) { // Options + optionsReturnState = PAUSE_MENU; + currentState = OPTIONS_MENU; + } else if (i == 2) { // Save & Exit + currentState = MAIN_MENU; + inventoryOpen = false; + + // Save player position 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; + 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 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; +}