MorriCraft/src/main.cpp

4222 lines
226 KiB
C++

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