2639 lines
135 KiB
C++
2639 lines
135 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 "rlgl.h"
|
|
|
|
#define CHUNK_SIZE 32
|
|
#ifndef PI
|
|
#define PI 3.1415926535f
|
|
#endif
|
|
#define CHUNK_HEIGHT 128
|
|
#define RENDER_DISTANCE 1 // 3x3 chunks visible for performance
|
|
|
|
enum BlockType {
|
|
AIR = 0, GRASS = 1, DIRT = 2, COBBLESTONE = 3, LOG = 4, LEAVES = 5, PLANK = 6,
|
|
STONE = 7, BEDROCK = 8, DIAMOND_ORE = 9, IRON_ORE = 10, GRAVEL = 11, CRAFTING_TABLE = 12, SAND = 13,
|
|
STICK = 14, WOOD_AXE = 15
|
|
};
|
|
|
|
// Simple 2D Perlin Noise implementation
|
|
float dotGridGradient(int ix, int iy, float x, float y, unsigned int seed) {
|
|
unsigned int hash = ix * 3284157443 ^ iy * 1911520717;
|
|
hash ^= seed;
|
|
float random = hash * (3.14159265f / ~(~0u >> 1));
|
|
float gradientX = cosf(random);
|
|
float gradientY = sinf(random);
|
|
float dx = x - (float)ix;
|
|
float dy = y - (float)iy;
|
|
return (dx*gradientX + dy*gradientY);
|
|
}
|
|
|
|
float interpolate(float a0, float a1, float w) {
|
|
return (a1 - a0) * (3.0f - w * 2.0f) * w * w;
|
|
}
|
|
|
|
float perlin(float x, float y, unsigned int seed) {
|
|
int x0 = (int)floorf(x);
|
|
int x1 = x0 + 1;
|
|
int y0 = (int)floorf(y);
|
|
int y1 = y0 + 1;
|
|
float sx = x - (float)x0;
|
|
float sy = y - (float)y0;
|
|
float n0 = dotGridGradient(x0, y0, x, y, seed);
|
|
float n1 = dotGridGradient(x1, y0, x, y, seed);
|
|
float ix0 = interpolate(n0, n1, sx);
|
|
n0 = dotGridGradient(x0, y1, x, y, seed);
|
|
n1 = dotGridGradient(x1, y1, x, y, seed);
|
|
float ix1 = interpolate(n0, n1, sx);
|
|
return interpolate(ix0, ix1, sy); // returns approximately -1.0 to 1.0
|
|
}
|
|
|
|
struct ChunkPos {
|
|
int x, z;
|
|
bool operator==(const ChunkPos& other) const { return x == other.x && z == other.z; }
|
|
};
|
|
|
|
struct ChunkPosHash {
|
|
std::size_t operator()(const ChunkPos& k) const {
|
|
return ((std::hash<int>()(k.x) ^ (std::hash<int>()(k.z) << 1)) >> 1);
|
|
}
|
|
};
|
|
|
|
struct Chunk {
|
|
int blocks[CHUNK_SIZE][CHUNK_HEIGHT][CHUNK_SIZE];
|
|
int maxY = 0;
|
|
bool generated = false;
|
|
bool modified = false;
|
|
bool dirty = true;
|
|
std::vector<Vector3> renderLists[16];
|
|
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 bool serverMode = false;
|
|
|
|
enum MenuState { MAIN_MENU, OPTIONS_MENU, CREATE_WORLD_MENU, LOAD_WORLD_MENU, GAMEPLAY, PAUSE_MENU, CRAFTING_GUI, CHECKING_UPDATES, UPDATE_NOTES, UPDATE_FOUND, DOWNLOADING_UPDATE, CONNECT_MENU };
|
|
|
|
// Forward Declarations
|
|
bool IsExposedOptimized(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;
|
|
std::string name;
|
|
Vector3 position;
|
|
float yaw;
|
|
};
|
|
static std::vector<RemotePlayer> remotePlayers;
|
|
|
|
// 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;
|
|
InventorySlot() : blockType(AIR), count(0) {}
|
|
InventorySlot(int bt, int c) : blockType(bt), count(c) {}
|
|
};
|
|
|
|
static InventorySlot hotbar[9]; // 9 hotbar slots
|
|
static InventorySlot inventory[27]; // 3x9 main inventory
|
|
static int activeHotbarSlot = 0;
|
|
static bool inventoryOpen = false;
|
|
|
|
// Adds one block to inventory: first fills existing stacks, then empty slots.
|
|
void AddToInventory(int blockType) {
|
|
// Search hotbar first
|
|
for (int i = 0; i < 9; i++) {
|
|
if (hotbar[i].blockType == blockType && hotbar[i].count < 64) {
|
|
hotbar[i].count++; return;
|
|
}
|
|
}
|
|
// Then main inventory
|
|
for (int i = 0; i < 27; i++) {
|
|
if (inventory[i].blockType == blockType && inventory[i].count < 64) {
|
|
inventory[i].count++; return;
|
|
}
|
|
}
|
|
// Empty hotbar slot
|
|
for (int i = 0; i < 9; i++) {
|
|
if (hotbar[i].count == 0) {
|
|
hotbar[i].blockType = blockType; hotbar[i].count = 1; return;
|
|
}
|
|
}
|
|
// Empty main inventory slot
|
|
for (int i = 0; i < 27; i++) {
|
|
if (inventory[i].count == 0) {
|
|
inventory[i].blockType = blockType; inventory[i].count = 1; return;
|
|
}
|
|
}
|
|
// Inventory full — item is lost for now
|
|
}
|
|
|
|
// Camera look angles — global so world load/create can reset them cleanly
|
|
static float camYaw = 0.0f;
|
|
static float camPitch = 0.0f;
|
|
|
|
bool LoadChunk(Chunk* chunk, int cx, int cz) {
|
|
if (currentWorldName.empty()) return false;
|
|
std::string path = "saves/" + currentWorldName + "/chunk_" + std::to_string(cx) + "_" + std::to_string(cz) + ".dat";
|
|
std::ifstream file(path, std::ios::binary);
|
|
if (file.is_open()) {
|
|
file.read((char*)chunk->blocks, sizeof(chunk->blocks));
|
|
chunk->generated = true;
|
|
chunk->modified = false;
|
|
// Calculate maxY for the loaded chunk
|
|
chunk->maxY = 0;
|
|
for (int x=0; x<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
|
|
}
|
|
|
|
void RebuildChunkRenderList(Chunk* chunk, int cx, int cz) {
|
|
for (int i = 0; i < 16; i++) chunk->renderLists[i].clear();
|
|
|
|
// Neighbors for exposure check
|
|
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;
|
|
|
|
if (IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) {
|
|
chunk->renderLists[bt].push_back((Vector3){(float)(worldX+lx), (float)ly, (float)(worldZ+lz)});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
chunk->dirty = false;
|
|
}
|
|
|
|
void NetSetBlock(int x, int y, int z, int type) {
|
|
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) {
|
|
send(clientSocket, (char*)&head, sizeof(head), 0);
|
|
send(clientSocket, (char*)&bc, sizeof(bc), 0);
|
|
}
|
|
if (serverSocket != INVALID_SOCKET_VAL) {
|
|
for (auto& s : clientSockets) {
|
|
send(s, (char*)&head, sizeof(head), 0);
|
|
send(s, (char*)&bc, sizeof(bc), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetBlock(int x, int y, int z, int type) {
|
|
if (y < 0 || y >= CHUNK_HEIGHT) return;
|
|
int cx = (int)floorf((float)x / CHUNK_SIZE);
|
|
int cz = (int)floorf((float)z / CHUNK_SIZE);
|
|
ChunkPos key = { cx, cz };
|
|
if (worldChunks.find(key) != worldChunks.end()) {
|
|
int lx = x - (cx * CHUNK_SIZE);
|
|
int lz = z - (cz * CHUNK_SIZE);
|
|
worldChunks[key]->blocks[lx][y][lz] = type;
|
|
worldChunks[key]->modified = true;
|
|
worldChunks[key]->dirty = true;
|
|
|
|
// Keep maxY up to date
|
|
if (type != AIR && y > worldChunks[key]->maxY) worldChunks[key]->maxY = y;
|
|
|
|
// Mark neighbors dirty as well since exposure changes
|
|
if (lx == 0) { auto n = worldChunks.find({cx-1, cz}); if (n != worldChunks.end()) n->second->dirty = true; }
|
|
if (lx == CHUNK_SIZE-1) { auto n = worldChunks.find({cx+1, cz}); if (n != worldChunks.end()) n->second->dirty = true; }
|
|
if (lz == 0) { auto n = worldChunks.find({cx, cz-1}); if (n != worldChunks.end()) n->second->dirty = true; }
|
|
if (lz == CHUNK_SIZE-1) { auto n = worldChunks.find({cx, cz+1}); if (n != worldChunks.end()) n->second->dirty = true; }
|
|
}
|
|
}
|
|
|
|
std::string GetRemoteVersion() {
|
|
std::string url = "https://git.linology.tech/michael/MorriCraft/raw/branch/master/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 = result.substr(0, last + 1);
|
|
return result;
|
|
}
|
|
|
|
void SaveConfig() {
|
|
std::ofstream file("config.cfg");
|
|
if (file.is_open()) {
|
|
file << "playerName=" << playerName << "\n";
|
|
file << "serverMode=" << (serverMode ? "true" : "false") << "\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 == "serverMode") serverMode = (val == "true");
|
|
}
|
|
}
|
|
file.close();
|
|
} else {
|
|
playerName = ""; // Trigger 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);
|
|
|
|
// Generate the spawn chunk and scan downward to find the first solid surface.
|
|
// Returns the player eye-level Y (surface block top + 1.6 camera height).
|
|
float FindSpawnY(int spawnX, int spawnZ) {
|
|
int cx = (int)floorf((float)spawnX / CHUNK_SIZE);
|
|
int cz = (int)floorf((float)spawnZ / CHUNK_SIZE);
|
|
GenerateChunk(cx, cz);
|
|
for (int y = CHUNK_HEIGHT - 1; y >= 1; y--) {
|
|
if (GetBlock(spawnX, y, spawnZ) != AIR) {
|
|
// Feet land at top of block (y + 0.5), eye is 1.6 above feet
|
|
return (float)y + 0.5f + 1.6f;
|
|
}
|
|
}
|
|
return 40.0f; // Fallback
|
|
}
|
|
|
|
void GenerateChunk(int cx, int cz) {
|
|
ChunkPos key = { cx, cz };
|
|
if (worldChunks.find(key) != worldChunks.end()) return;
|
|
|
|
Chunk* newChunk = new Chunk();
|
|
|
|
if (LoadChunk(newChunk, cx, cz)) {
|
|
worldChunks[key] = newChunk;
|
|
return;
|
|
}
|
|
|
|
// Initialize with AIR
|
|
for (int x=0; 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-octave fbm noise at a much lower frequency for broad, smooth hills.
|
|
// Base freq 0.006 means features span ~160 blocks, preventing steep cliffs.
|
|
float noiseVal = fbm(worldX * 0.006f, worldZ * 0.006f, globalSeedHash);
|
|
// Terrain sits around Y=32 with only ±6 blocks of variation,
|
|
// so the minimum ground level is 26 -- no sudden deep holes.
|
|
int height = 32 + (int)(noiseVal * 6.0f);
|
|
if (height < 10) height = 10;
|
|
if (height >= CHUNK_HEIGHT - 2) height = CHUNK_HEIGHT - 2;
|
|
|
|
for (int y = 0; y <= height; y++) {
|
|
if (y == 0) {
|
|
newChunk->blocks[x][y][z] = BEDROCK;
|
|
} else if (y == height) {
|
|
newChunk->blocks[x][y][z] = GRASS;
|
|
} else if (y > height - 4) {
|
|
// 4 blocks of dirt under the surface — thick enough that ores
|
|
// can never be exposed by normal terrain features.
|
|
newChunk->blocks[x][y][z] = DIRT;
|
|
} else {
|
|
newChunk->blocks[x][y][z] = STONE;
|
|
|
|
// Ore generation gated by depth so ores are never at surface.
|
|
unsigned int oreHash = (cx * CHUNK_SIZE + x) * 73856093 ^ (cz * CHUNK_SIZE + z) * 19349663 ^ y * 83492791;
|
|
oreHash ^= globalSeedHash;
|
|
|
|
// Diamond only very deep (below Y=16, well below new surface at Y=32)
|
|
if (y <= 16 && oreHash % 120 < 1) {
|
|
newChunk->blocks[x][y][z] = DIAMOND_ORE;
|
|
// Iron ore below Y=24
|
|
} else if (y <= 24 && oreHash % 80 < 3) {
|
|
newChunk->blocks[x][y][z] = IRON_ORE;
|
|
// Gravel pockets below Y=20
|
|
} else if (y <= 20 && oreHash % 60 < 4) {
|
|
newChunk->blocks[x][y][z] = GRAVEL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add trees
|
|
GenerateTrees(newChunk, cx, cz);
|
|
|
|
// Record the highest non-air block so the render loop skips empty space
|
|
newChunk->maxY = 0;
|
|
for (int x = 0; x < CHUNK_SIZE; x++)
|
|
for (int z = 0; z < CHUNK_SIZE; z++)
|
|
for (int y = CHUNK_HEIGHT - 1; y >= 0; y--)
|
|
if (newChunk->blocks[x][y][z] != AIR) {
|
|
if (y > newChunk->maxY) newChunk->maxY = y;
|
|
break;
|
|
}
|
|
|
|
newChunk->generated = true;
|
|
newChunk->modified = true; // Newly generated, so we should save it
|
|
worldChunks[key] = newChunk;
|
|
}
|
|
|
|
bool CheckPlayerCollision(Vector3 pos) {
|
|
BoundingBox playerBox = {
|
|
(Vector3){ pos.x - 0.3f, pos.y - 1.5f, pos.z - 0.3f },
|
|
(Vector3){ pos.x + 0.3f, pos.y + 0.1f, pos.z + 0.3f }
|
|
};
|
|
|
|
int minX = (int)floorf(playerBox.min.x + 0.5f);
|
|
int maxX = (int)floorf(playerBox.max.x + 0.5f);
|
|
int minY = (int)floorf(playerBox.min.y + 0.5f);
|
|
int maxY = (int)floorf(playerBox.max.y + 0.5f);
|
|
int minZ = (int)floorf(playerBox.min.z + 0.5f);
|
|
int maxZ = (int)floorf(playerBox.max.z + 0.5f);
|
|
|
|
for (int x = minX; x <= maxX; x++) {
|
|
for (int y = minY; y <= maxY; y++) {
|
|
for (int z = minZ; z <= maxZ; z++) {
|
|
if (GetBlock(x, y, z) != AIR) {
|
|
BoundingBox blockBox = {
|
|
(Vector3){ x - 0.5f, y - 0.5f, z - 0.5f },
|
|
(Vector3){ x + 0.5f, y + 0.5f, z + 0.5f }
|
|
};
|
|
if (CheckCollisionBoxes(playerBox, blockBox)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void DrawTexturedCube(Vector3 position, float width, float height, float length, Color color) {
|
|
float x = position.x;
|
|
float y = position.y;
|
|
float z = position.z;
|
|
|
|
rlBegin(RL_QUADS);
|
|
rlColor4ub(color.r, color.g, color.b, color.a);
|
|
// Front
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z + length/2);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z + length/2);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z + length/2);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z + length/2);
|
|
// Back
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z - length/2);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z - length/2);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z - length/2);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z - length/2);
|
|
// Top
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - width/2, y + height/2, z - length/2);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z + length/2);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z + length/2);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + width/2, y + height/2, z - length/2);
|
|
// Bottom
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z - length/2);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z - length/2);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + width/2, y - height/2, z + length/2);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - width/2, y - height/2, z + length/2);
|
|
// Right
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z - length/2);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z - length/2);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + width/2, y + height/2, z + length/2);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + width/2, y - height/2, z + length/2);
|
|
// Left
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z - length/2);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - width/2, y - height/2, z + length/2);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z + length/2);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - width/2, y + height/2, z - length/2);
|
|
rlEnd();
|
|
}
|
|
|
|
// Draws a grass block with correct per-face textures:
|
|
// top=green grass, sides=dirt+grass stripe, bottom=pure dirt.
|
|
// Caller must NOT have an active rlSetTexture — this function manages its own binds.
|
|
void DrawGrassBlock(Vector3 position, unsigned int sideTexId, unsigned int topTexId, unsigned int botTexId, Color tint) {
|
|
float x = position.x;
|
|
float y = position.y;
|
|
float z = position.z;
|
|
|
|
// ---- SIDES (4 faces) ----
|
|
rlSetTexture(sideTexId);
|
|
rlBegin(RL_QUADS);
|
|
rlColor4ub(tint.r, tint.g, tint.b, 255);
|
|
// Front
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f);
|
|
// Back
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f);
|
|
// Right
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f);
|
|
// Left
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f);
|
|
rlEnd();
|
|
|
|
// ---- TOP (pure green) ----
|
|
rlSetTexture(topTexId);
|
|
rlBegin(RL_QUADS);
|
|
// Combine grass green with ambient light
|
|
rlColor4ub((unsigned char)(124 * (tint.r/255.0f)),
|
|
(unsigned char)(189 * (tint.g/255.0f)),
|
|
(unsigned char)(107 * (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);
|
|
// ---- BOTTOM (pure dirt) ----
|
|
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();
|
|
}
|
|
|
|
// Draws a crafting table with custom top and sides
|
|
void DrawCraftingTable(Vector3 position, unsigned int sideTexId, unsigned int topTexId, unsigned int botTexId, Color tint) {
|
|
float x = position.x; float y = position.y; float z = position.z;
|
|
|
|
// SIDES
|
|
rlSetTexture(sideTexId);
|
|
rlBegin(RL_QUADS);
|
|
rlColor4ub(tint.r, tint.g, tint.b, 255);
|
|
// Front, Back, Right, Left
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z - 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + 0.5f, y - 0.5f, z + 0.5f);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z - 0.5f); rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - 0.5f, y - 0.5f, z + 0.5f); rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z + 0.5f); rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - 0.5f, y + 0.5f, z - 0.5f);
|
|
rlEnd();
|
|
|
|
// TOP
|
|
rlSetTexture(topTexId);
|
|
rlBegin(RL_QUADS);
|
|
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();
|
|
|
|
// BOTTOM (dirt)
|
|
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 IsExposedOptimized(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP) {
|
|
if (ly > 0) { if (chunk->blocks[lx][ly-1][lz] == AIR) return true; } else return true;
|
|
if (ly < CHUNK_HEIGHT - 1) { if (chunk->blocks[lx][ly+1][lz] == AIR) return true; } else return true;
|
|
if (lx > 0) { if (chunk->blocks[lx-1][ly][lz] == AIR) return true; }
|
|
else if (nxM) { if (nxM->blocks[CHUNK_SIZE-1][ly][lz] == AIR) return true; } else return true;
|
|
if (lx < CHUNK_SIZE - 1) { if (chunk->blocks[lx+1][ly][lz] == AIR) return true; }
|
|
else if (nxP) { if (nxP->blocks[0][ly][lz] == AIR) return true; } else return true;
|
|
if (lz > 0) { if (chunk->blocks[lx][ly][lz-1] == AIR) return true; }
|
|
else if (nzM) { if (nzM->blocks[lx][ly][CHUNK_SIZE-1] == AIR) return true; } else return true;
|
|
if (lz < CHUNK_SIZE - 1) { if (chunk->blocks[lx][ly][lz+1] == AIR) return true; }
|
|
else if (nzP) { if (nzP->blocks[lx][ly][0] == AIR) return true; } else return true;
|
|
return false;
|
|
}
|
|
|
|
// Optimized helper for batched rendering: Draws only the vertices (no texture bind or rlBegin/End)
|
|
void DrawCubeVertices(float x, float y, float z, float w, float h, float l) {
|
|
float hw = w/2.0f; float hh = h/2.0f; float hl = l/2.0f;
|
|
// Front
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - hw, y - hh, z + hl);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + hw, y - hh, z + hl);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + hw, y + hh, z + hl);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - hw, y + hh, z + hl);
|
|
// Back
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - hw, y - hh, z - hl);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - hw, y + hh, z - hl);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + hw, y + hh, z - hl);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + hw, y - hh, z - hl);
|
|
// Top
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - hw, y + hh, z - hl);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - hw, y + hh, z + hl);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + hw, y + hh, z + hl);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + hw, y + hh, z - hl);
|
|
// Bottom
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - hw, y - hh, z - hl);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + hw, y - hh, z - hl);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + hw, y - hh, z + hl);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - hw, y - hh, z + hl);
|
|
// Right
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x + hw, y - hh, z - hl);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x + hw, y + hh, z - hl);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x + hw, y + hh, z + hl);
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x + hw, y - hh, z + hl);
|
|
// Left
|
|
rlTexCoord2f(0.0f, 1.0f); rlVertex3f(x - hw, y - hh, z - hl);
|
|
rlTexCoord2f(1.0f, 1.0f); rlVertex3f(x - hw, y - hh, z + hl);
|
|
rlTexCoord2f(1.0f, 0.0f); rlVertex3f(x - hw, y + hh, z + hl);
|
|
rlTexCoord2f(0.0f, 0.0f); rlVertex3f(x - hw, y + hh, z - hl);
|
|
}
|
|
|
|
// Optimized check to see if a block has any exposed faces
|
|
bool IsExposed(int x, int ly, int z, Chunk* chunk, int lx, int lz) {
|
|
// Local check (inside same chunk)
|
|
if (lx > 0 && lx < CHUNK_SIZE - 1 && ly > 0 && ly < CHUNK_HEIGHT - 1 && lz > 0 && lz < CHUNK_SIZE - 1) {
|
|
if (chunk->blocks[lx-1][ly][lz] == AIR) return true;
|
|
if (chunk->blocks[lx+1][ly][lz] == AIR) return true;
|
|
if (chunk->blocks[lx][ly-1][lz] == AIR) return true;
|
|
if (chunk->blocks[lx][ly+1][lz] == AIR) return true;
|
|
if (chunk->blocks[lx][ly][lz-1] == AIR) return true;
|
|
if (chunk->blocks[lx][ly][lz+1] == AIR) return true;
|
|
return false;
|
|
}
|
|
// Global check (at chunk boundary)
|
|
if (GetBlock(x-1, ly, z) == AIR) return true;
|
|
if (GetBlock(x+1, ly, z) == AIR) return true;
|
|
if (GetBlock(x, ly-1, z) == AIR) return true;
|
|
if (GetBlock(x, ly+1, z) == AIR) return true;
|
|
if (GetBlock(x, ly, z-1) == AIR) return true;
|
|
if (GetBlock(x, ly, z+1) == AIR) return true;
|
|
return false;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
SetRandomSeed((unsigned int)time(NULL));
|
|
// Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
const int screenWidth = 1280;
|
|
const int screenHeight = 720;
|
|
|
|
// Set config flags to allow window resizing.
|
|
// By default, windows have minimize, maximize, and close buttons on the top bar.
|
|
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT);
|
|
|
|
InitWindow(screenWidth, screenHeight, "MorriCraft v2.0.1");
|
|
LoadConfig();
|
|
SetExitKey(KEY_NULL); // Prevent ESC from closing the window
|
|
|
|
// Initialize audio device
|
|
InitAudioDevice();
|
|
|
|
// Load the title image, music, and font
|
|
// NOTE: Textures and Fonts MUST be loaded after Window initialization
|
|
Texture2D titleTexture = LoadTexture("assets/TitleImage.png");
|
|
Font customFont = LoadFontEx("assets/PixelFont.ttf", 32, 0, 250);
|
|
Music titleMusic = LoadMusicStream("assets/Morricraft-title.mp3");
|
|
titleMusic.looping = true;
|
|
PlayMusicStream(titleMusic);
|
|
|
|
Music gameplayMusic = LoadMusicStream("assets/Morricraft-day1.mp3");
|
|
gameplayMusic.looping = true;
|
|
PlayMusicStream(gameplayMusic);
|
|
SetMusicVolume(gameplayMusic, 0.0f);
|
|
|
|
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");
|
|
Sound digWood = LoadSound("assets/wood1.ogg");
|
|
Sound digStone = LoadSound("assets/stone1.ogg");
|
|
Sound digSand = LoadSound("assets/sand1.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 updateReady = false;
|
|
float updateTimer = 0.0f;
|
|
float downloadProgress = 0.0f;
|
|
std::string latestVersion = "";
|
|
std::string localVersion = "v1.9.0"; // 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);
|
|
float gameTime = 75.0f; // Start at 6: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;
|
|
|
|
float masterMusicVolume = 1.0f;
|
|
float masterSoundVolume = 1.0f;
|
|
|
|
char worldName[64] = "New World";
|
|
int worldNameLen = 9;
|
|
char worldSeed[64] = "";
|
|
int worldSeedLen = 0;
|
|
int activeTextBox = 0; // 0 = none, 1 = name, 2 = seed
|
|
|
|
bool showDeleteConfirm = false;
|
|
std::string deletingWorldName = "";
|
|
|
|
Camera2D camera = { 0 };
|
|
camera.offset = (Vector2){ screenWidth/2.0f, screenHeight/2.0f };
|
|
camera.target = (Vector2){ screenWidth/2.0f, screenHeight/2.0f };
|
|
camera.rotation = 0.0f;
|
|
camera.zoom = 1.1f;
|
|
|
|
// 3D Camera Setup
|
|
Camera3D camera3D = { 0 };
|
|
camera3D.position = (Vector3){ 0.0f, 60.0f, 0.0f }; // Spawn high, gravity drops player to surface
|
|
camera3D.target = (Vector3){ 0.0f, 60.0f, 1.0f };
|
|
camera3D.up = (Vector3){ 0.0f, 1.0f, 0.0f };
|
|
camera3D.fovy = 70.0f;
|
|
camera3D.projection = CAMERA_PERSPECTIVE;
|
|
|
|
// 3D Block Textures Setup
|
|
Texture2D blockTextures[32] = {0};
|
|
blockTextures[DIRT] = LoadTexture("assets/dirt.png");
|
|
blockTextures[GRASS] = LoadTexture("assets/grass.png");
|
|
blockTextures[COBBLESTONE] = LoadTexture("assets/cobblestone.png");
|
|
blockTextures[LOG] = LoadTexture("assets/plank.png"); // Fallback for missing log.png
|
|
blockTextures[LEAVES] = blockTextures[GRASS]; // Leaves share grass texture (tinted in render loop)
|
|
blockTextures[PLANK] = LoadTexture("assets/plank.png");
|
|
blockTextures[SAND] = LoadTexture("assets/sand.png");
|
|
blockTextures[STONE] = LoadTexture("assets/stone.png");
|
|
blockTextures[BEDROCK] = LoadTexture("assets/bedrock.png");
|
|
blockTextures[DIAMOND_ORE] = LoadTexture("assets/diamond_ore.png");
|
|
blockTextures[IRON_ORE] = LoadTexture("assets/iron_ore.png");
|
|
blockTextures[GRAVEL] = LoadTexture("assets/gravel.png");
|
|
|
|
// Grass needs separate top/side/bottom textures for correct look
|
|
Texture2D grassTopTexture = LoadTexture("assets/grass_top.png");
|
|
Texture2D craftingSideTexture = LoadTexture("assets/crafting_table_side.png");
|
|
Texture2D craftingTopTexture = LoadTexture("assets/crafting_table_top.png");
|
|
|
|
// 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];
|
|
blockTextures[CRAFTING_TABLE] = craftingSideTexture; // Preview for inventory
|
|
|
|
blockTextures[STICK] = LoadTexture("assets/stick.png");
|
|
blockTextures[WOOD_AXE] = LoadTexture("assets/wooden_axe.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 = [&]() {
|
|
// 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);
|
|
}
|
|
// Recipe: 2 Planks (vertical) -> 4 Sticks
|
|
else if ((craftingSlots[0].blockType == PLANK && craftingSlots[2].blockType == PLANK) ||
|
|
(craftingSlots[1].blockType == PLANK && craftingSlots[3].blockType == PLANK)) {
|
|
craftingResult = InventorySlot(STICK, 4);
|
|
}
|
|
else {
|
|
craftingResult = InventorySlot(AIR, 0);
|
|
}
|
|
};
|
|
|
|
auto UpdateTableCrafting = [&]() {
|
|
// Wooden Axe Recipe
|
|
// [P P .]
|
|
// [P S .]
|
|
// [. S .]
|
|
if (tableSlots[0].blockType == PLANK && tableSlots[1].blockType == PLANK &&
|
|
tableSlots[3].blockType == PLANK && tableSlots[4].blockType == STICK &&
|
|
tableSlots[7].blockType == STICK) {
|
|
tableResult = InventorySlot(WOOD_AXE, 1);
|
|
}
|
|
// Sticks (vertical anywhere)
|
|
else if ((tableSlots[0].blockType == PLANK && tableSlots[3].blockType == PLANK) ||
|
|
(tableSlots[1].blockType == PLANK && tableSlots[4].blockType == PLANK) ||
|
|
(tableSlots[2].blockType == PLANK && tableSlots[5].blockType == PLANK) ||
|
|
(tableSlots[3].blockType == PLANK && tableSlots[6].blockType == PLANK) ||
|
|
(tableSlots[4].blockType == PLANK && tableSlots[7].blockType == PLANK) ||
|
|
(tableSlots[5].blockType == PLANK && tableSlots[8].blockType == PLANK)) {
|
|
tableResult = InventorySlot(STICK, 4);
|
|
}
|
|
// 4 Planks -> 1 Table (3x3 grid)
|
|
else if (tableSlots[0].blockType == PLANK && tableSlots[1].blockType == PLANK &&
|
|
tableSlots[3].blockType == PLANK && tableSlots[4].blockType == PLANK) {
|
|
tableResult = InventorySlot(CRAFTING_TABLE, 1);
|
|
}
|
|
else {
|
|
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 == 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);
|
|
send(clientSocket, (char*)&head, sizeof(head), 0);
|
|
send(clientSocket, (char*)&hand, sizeof(hand), 0);
|
|
}
|
|
}
|
|
|
|
// Handle incoming data
|
|
auto handleIncoming = [&](Socket sock, bool isServer, int clientIdx = -1) {
|
|
PacketHeader head;
|
|
int bytes = recv(sock, (char*)&head, sizeof(head), 0);
|
|
if (bytes > 0) {
|
|
if (head.type == PACKET_HANDSHAKE) {
|
|
PacketHandshake hand;
|
|
recv(sock, (char*)&hand, sizeof(hand), 0);
|
|
if (isServer) {
|
|
RemotePlayer rp;
|
|
rp.sock = sock;
|
|
rp.name = hand.name;
|
|
rp.position = (Vector3){0,0,0};
|
|
remotePlayers.push_back(rp);
|
|
// Send seed to new client
|
|
PacketHeader sHead = { (uint8_t)PACKET_SEED_SYNC, (uint32_t)sizeof(PacketSeedSync) };
|
|
PacketSeedSync sData = { globalSeedHash };
|
|
send(sock, (char*)&sHead, sizeof(sHead), 0);
|
|
send(sock, (char*)&sData, sizeof(sData), 0);
|
|
}
|
|
} else if (head.type == PACKET_PLAYER_UPDATE) {
|
|
PacketPlayerUpdate pu;
|
|
recv(sock, (char*)&pu, sizeof(pu), 0);
|
|
for (auto& rp : remotePlayers) {
|
|
if (rp.sock == sock) {
|
|
rp.position = (Vector3){ pu.x, pu.y, pu.z };
|
|
rp.yaw = pu.yaw;
|
|
break;
|
|
}
|
|
}
|
|
if (isServer) {
|
|
// Broadcast to others
|
|
for (auto& other : clientSockets) {
|
|
if (other != sock) {
|
|
send(other, (char*)&head, sizeof(head), 0);
|
|
send(other, (char*)&pu, sizeof(pu), 0);
|
|
}
|
|
}
|
|
}
|
|
} else if (head.type == PACKET_BLOCK_CHANGE) {
|
|
PacketBlockChange bc;
|
|
recv(sock, (char*)&bc, sizeof(bc), 0);
|
|
SetBlock(bc.x, bc.y, bc.z, bc.blockType);
|
|
if (isServer) {
|
|
for (auto& other : clientSockets) {
|
|
if (other != sock) {
|
|
send(other, (char*)&head, sizeof(head), 0);
|
|
send(other, (char*)&bc, sizeof(bc), 0);
|
|
}
|
|
}
|
|
}
|
|
} else if (head.type == PACKET_TIME_SYNC) {
|
|
PacketTimeSync ts;
|
|
recv(sock, (char*)&ts, sizeof(ts), 0);
|
|
if (!isServer) gameTime = ts.gameTime;
|
|
} else if (head.type == PACKET_SEED_SYNC) {
|
|
PacketSeedSync ss;
|
|
recv(sock, (char*)&ss, sizeof(ss), 0);
|
|
if (!isServer) {
|
|
globalSeedHash = ss.seed;
|
|
// Regerate world if seed changed
|
|
for (auto& pair : worldChunks) delete pair.second;
|
|
worldChunks.clear();
|
|
}
|
|
}
|
|
} else if (bytes == 0 || (bytes < 0 && SOCKET_ERROR_VAL != -1)) {
|
|
// Disconnect handling
|
|
if (isServer && clientIdx != -1) {
|
|
for (auto it = remotePlayers.begin(); it != remotePlayers.end(); ++it) {
|
|
if (it->sock == sock) {
|
|
remotePlayers.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
closesocket(sock);
|
|
clientSockets.erase(clientSockets.begin() + clientIdx);
|
|
}
|
|
}
|
|
};
|
|
|
|
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 };
|
|
send(clientSocket, (char*)&head, sizeof(head), 0);
|
|
send(clientSocket, (char*)&pu, sizeof(pu), 0);
|
|
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) {
|
|
send(s, (char*)&head, sizeof(head), 0);
|
|
send(s, (char*)&ts, sizeof(ts), 0);
|
|
}
|
|
timeSyncTimer = 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 = 300.0f;
|
|
if (currentState == GAMEPLAY) gameTime += GetFrameTime();
|
|
float timeOfDay = fmodf(gameTime, cycleLength) / cycleLength;
|
|
float sunAngle = timeOfDay * 2.0f * 3.14159f - 3.14159f/2.0f;
|
|
float 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;
|
|
|
|
// 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;
|
|
|
|
// Toggle inventory
|
|
if (IsKeyPressed(KEY_E)) {
|
|
inventoryOpen = !inventoryOpen;
|
|
if (inventoryOpen) EnableCursor();
|
|
else DisableCursor();
|
|
}
|
|
|
|
// Dynamic Chunk Loading
|
|
int playerCX = (int)floorf(camera3D.position.x / CHUNK_SIZE);
|
|
int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE);
|
|
for (int x = playerCX - RENDER_DISTANCE; x <= playerCX + RENDER_DISTANCE; x++) {
|
|
for (int z = playerCZ - RENDER_DISTANCE; z <= playerCZ + RENDER_DISTANCE; z++) {
|
|
GenerateChunk(x, z);
|
|
}
|
|
}
|
|
|
|
// ---- Manual Camera System (Look + WASD) ----
|
|
if (!inventoryOpen) {
|
|
const float MOUSE_SENS = 0.002f;
|
|
Vector2 md = GetMouseDelta();
|
|
camYaw -= md.x * MOUSE_SENS;
|
|
camPitch -= md.y * MOUSE_SENS;
|
|
if (camPitch > 1.5f) camPitch = 1.5f;
|
|
if (camPitch < -1.5f) camPitch = -1.5f;
|
|
}
|
|
|
|
// Direction vectors (horizontal)
|
|
Vector3 forward = { sinf(camYaw), 0, cosf(camYaw) };
|
|
Vector3 right = { cosf(camYaw), 0, -sinf(camYaw) };
|
|
|
|
Vector3 oldPos = camera3D.position;
|
|
Vector3 moveVec = { 0, 0, 0 };
|
|
|
|
if (!inventoryOpen) {
|
|
if (IsKeyDown(KEY_W)) moveVec = Vector3Add(moveVec, forward);
|
|
if (IsKeyDown(KEY_S)) moveVec = Vector3Subtract(moveVec, forward);
|
|
if (IsKeyDown(KEY_A)) moveVec = Vector3Add(moveVec, right);
|
|
if (IsKeyDown(KEY_D)) moveVec = Vector3Subtract(moveVec, right);
|
|
}
|
|
|
|
if (Vector3Length(moveVec) > 0) {
|
|
moveVec = Vector3Normalize(moveVec);
|
|
float speed = 5.0f * GetFrameTime();
|
|
|
|
Vector3 tryX = { oldPos.x + moveVec.x * speed, oldPos.y, oldPos.z };
|
|
if (!CheckPlayerCollision(tryX)) camera3D.position.x = tryX.x;
|
|
|
|
Vector3 tryZ = { camera3D.position.x, oldPos.y, oldPos.z + moveVec.z * speed };
|
|
if (!CheckPlayerCollision(tryZ)) camera3D.position.z = tryZ.z;
|
|
}
|
|
|
|
// ---- Vertical Physics (Ground-Lock System) ----
|
|
if (isGrounded) {
|
|
playerVelocityY = 0.0f;
|
|
|
|
// Keep player exactly on top of the block with a small epsilon
|
|
float feetY = camera3D.position.y - 1.6f;
|
|
float expectedFeetY = floorf(feetY + 0.1f) + 0.5f;
|
|
camera3D.position.y = expectedFeetY + 1.6f + 0.01f;
|
|
|
|
// Jump
|
|
if (IsKeyPressed(KEY_SPACE) && !inventoryOpen) {
|
|
playerVelocityY = 8.5f;
|
|
isGrounded = false;
|
|
} else {
|
|
// Check if we walked off an edge (use a generous 0.2f margin)
|
|
Vector3 checkBelow = camera3D.position;
|
|
checkBelow.y -= 0.2f;
|
|
if (!CheckPlayerCollision(checkBelow)) isGrounded = false;
|
|
}
|
|
} else {
|
|
// Falling / Jumping
|
|
playerVelocityY -= 25.0f * GetFrameTime();
|
|
float dy = playerVelocityY * GetFrameTime();
|
|
|
|
Vector3 nextYPos = camera3D.position;
|
|
nextYPos.y += dy;
|
|
|
|
if (CheckPlayerCollision(nextYPos)) {
|
|
if (playerVelocityY < 0.0f) {
|
|
// Landed: Lock to surface
|
|
float feetY = nextYPos.y - 1.6f;
|
|
camera3D.position.y = floorf(feetY + 0.1f) + 0.5f + 1.6f + 0.01f;
|
|
playerVelocityY = 0.0f;
|
|
isGrounded = true;
|
|
} else {
|
|
// Hit ceiling
|
|
camera3D.position.y = ceilf(nextYPos.y + 0.1f) - 0.5f - 0.41f;
|
|
playerVelocityY = 0.0f;
|
|
}
|
|
} else {
|
|
camera3D.position.y = nextYPos.y;
|
|
}
|
|
}
|
|
|
|
// Final Camera state
|
|
camera3D.target.x = camera3D.position.x + sinf(camYaw) * cosf(camPitch);
|
|
camera3D.target.y = camera3D.position.y + sinf(camPitch);
|
|
camera3D.target.z = camera3D.position.z + cosf(camYaw) * cosf(camPitch);
|
|
camera3D.up = (Vector3){ 0, 1, 0 };
|
|
|
|
|
|
// Block Raycasting (moved outside to update every frame for wireframe)
|
|
hitBlock = false;
|
|
if (!inventoryOpen) {
|
|
Ray ray = GetMouseRay((Vector2){ (float)currentWidth / 2, (float)currentHeight / 2 }, camera3D);
|
|
float closestDist = 8.0f;
|
|
|
|
int startX = (int)floorf(camera3D.position.x - 8);
|
|
int endX = (int)floorf(camera3D.position.x + 8);
|
|
int startY = (int)floorf(camera3D.position.y - 8);
|
|
int endY = (int)floorf(camera3D.position.y + 8);
|
|
int startZ = (int)floorf(camera3D.position.z - 8);
|
|
int endZ = (int)floorf(camera3D.position.z + 8);
|
|
|
|
for (int x = startX; x <= endX; x++) {
|
|
for (int y = startY; y <= endY; y++) {
|
|
for (int z = startZ; z <= endZ; z++) {
|
|
if (GetBlock(x, y, z) != AIR) {
|
|
BoundingBox box = {
|
|
(Vector3){ x - 0.5f, y - 0.5f, z - 0.5f },
|
|
(Vector3){ x + 0.5f, y + 0.5f, z + 0.5f }
|
|
};
|
|
RayCollision collision = GetRayCollisionBox(ray, box);
|
|
if (collision.hit && collision.distance < closestDist) {
|
|
closestDist = collision.distance;
|
|
closestNormal = collision.normal;
|
|
hitBlock = true;
|
|
hitX = x; hitY = y; hitZ = z;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hitBlock) {
|
|
if (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
|
|
|
|
// Axe logic: Wood/Plank/Logs are faster
|
|
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) {
|
|
breakSpeed = 0.2f; // Very fast
|
|
}
|
|
|
|
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) {
|
|
AddToInventory(targetBlock);
|
|
NetSetBlock(hitX, hitY, hitZ, AIR);
|
|
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)) {
|
|
int targetBlock = GetBlock(hitX, hitY, hitZ);
|
|
if (targetBlock == CRAFTING_TABLE) {
|
|
currentState = CRAFTING_GUI;
|
|
EnableCursor();
|
|
} else if (hotbar[activeHotbarSlot].count > 0) {
|
|
int placeX = hitX + (int)roundf(closestNormal.x);
|
|
int placeY = hitY + (int)roundf(closestNormal.y);
|
|
int placeZ = hitZ + (int)roundf(closestNormal.z);
|
|
NetSetBlock(placeX, placeY, placeZ, hotbar[activeHotbarSlot].blockType);
|
|
hotbar[activeHotbarSlot].count--;
|
|
if (hotbar[activeHotbarSlot].count == 0)
|
|
hotbar[activeHotbarSlot].blockType = AIR;
|
|
isSwinging = true; // Swing when placing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (latestVersion != "error" && latestVersion != localVersion) {
|
|
updateReady = true;
|
|
}
|
|
}
|
|
|
|
if (updateTimer > 2.0f) {
|
|
if (updateReady) currentState = UPDATE_FOUND;
|
|
else currentState = MAIN_MENU;
|
|
}
|
|
} else if (currentState == UPDATE_FOUND) {
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 220 });
|
|
int pw = 450, ph = 250;
|
|
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, GREEN);
|
|
|
|
DrawTextEx(customFont, "NEW VERSION AVAILABLE!", (Vector2){ pBox.x + 40, pBox.y + 30 }, 24, 1.0f, GREEN);
|
|
std::string updMsg = latestVersion + " is now ready for download.";
|
|
DrawTextEx(customFont, updMsg.c_str(), (Vector2){ pBox.x + 40, pBox.y + 70 }, 18, 1.0f, LIGHTGRAY);
|
|
|
|
// Update Button
|
|
Rectangle updBtn = { pBox.x + 40, pBox.y + 120, 370, 40 };
|
|
bool isUpdHovered = CheckCollisionPointRec(mousePos, updBtn);
|
|
DrawRectangleRec(updBtn, isUpdHovered ? GREEN : (Color){ 0, 100, 0, 255 });
|
|
DrawTextEx(customFont, "UPDATE NOW", (Vector2){ updBtn.x + 100, updBtn.y + 10 }, 20, 1.0f, WHITE);
|
|
if (isUpdHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) currentState = DOWNLOADING_UPDATE;
|
|
|
|
// Update Notes Button
|
|
Rectangle noteBtn = { pBox.x + 40, pBox.y + 175, 370, 40 };
|
|
bool isNoteHovered = CheckCollisionPointRec(mousePos, noteBtn);
|
|
DrawRectangleRec(noteBtn, isNoteHovered ? DARKGRAY : BLACK);
|
|
DrawRectangleLinesEx(noteBtn, 2.0f, GRAY);
|
|
DrawTextEx(customFont, "VIEW UPDATE NOTES", (Vector2){ noteBtn.x + 65, noteBtn.y + 10 }, 20, 1.0f, WHITE);
|
|
if (isNoteHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) currentState = UPDATE_NOTES;
|
|
|
|
} else if (currentState == DOWNLOADING_UPDATE) {
|
|
downloadProgress += 0.005f;
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 20, 20, 20, 255 });
|
|
DrawTextEx(customFont, "DOWNLOADING UPDATE...", (Vector2){ (float)currentWidth/2 - 150, (float)currentHeight/2 - 40 }, 24, 1.0f, WHITE);
|
|
|
|
// Progress Bar
|
|
int bw = 400, bh = 30;
|
|
DrawRectangle(currentWidth/2 - bw/2, currentHeight/2, bw, bh, BLACK);
|
|
DrawRectangle(currentWidth/2 - bw/2, currentHeight/2, (int)(bw * downloadProgress), bh, GREEN);
|
|
DrawRectangleLines(currentWidth/2 - bw/2, currentHeight/2, bw, bh, GRAY);
|
|
|
|
if (downloadProgress >= 1.0f) {
|
|
// Simulate restart
|
|
currentState = MAIN_MENU;
|
|
updateReady = false;
|
|
}
|
|
} 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';
|
|
}
|
|
}
|
|
|
|
// Join Button Logic
|
|
Rectangle joinBtn = { cBox.x + 50, cBox.y + 280, 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 + 280, 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 != 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.0.1) in Red
|
|
DrawTextEx(customFont, "v2.0.1", (Vector2){ (float)currentWidth - 140, (float)currentHeight - 40 }, 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
|
|
// Future: Open Direct Connect UI
|
|
} else if (i == 3) { // Options button
|
|
optionsReturnState = MAIN_MENU;
|
|
currentState = OPTIONS_MENU;
|
|
}
|
|
}
|
|
}
|
|
|
|
Vector2 textSize = MeasureTextEx(customFont, buttons[i], fontSize, 1.0f);
|
|
int textPosX = posX + (buttonWidth / 2) - (textSize.x / 2);
|
|
int textPosY = posY + (buttonHeight / 2) - (textSize.y / 2);
|
|
|
|
DrawTextEx(customFont, buttons[i], (Vector2){ (float)textPosX + 2, (float)textPosY + 2 }, fontSize, 1.0f, (Color){ 30, 30, 30, 255 });
|
|
Color textColor = isHovered ? (Color){ 255, 255, 160, 255 } : (Color){ 220, 220, 220, 255 };
|
|
DrawTextEx(customFont, buttons[i], (Vector2){ (float)textPosX, (float)textPosY }, fontSize, 1.0f, textColor);
|
|
}
|
|
}
|
|
} else if (currentState == LOAD_WORLD_MENU) {
|
|
targetZoom = 1.0f;
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 });
|
|
|
|
int panelWidth = 600;
|
|
int panelHeight = 500;
|
|
int panelX = (currentWidth / 2) - (panelWidth / 2);
|
|
int panelY = (currentHeight / 2) - (panelHeight / 2);
|
|
|
|
DrawRectangle(panelX, panelY, panelWidth, panelHeight, (Color){ 40, 40, 40, 240 });
|
|
DrawRectangleLinesEx((Rectangle){ (float)panelX, (float)panelY, (float)panelWidth, (float)panelHeight }, 4.0f, (Color){ 100, 100, 100, 255 });
|
|
DrawTextEx(customFont, "Load World", (Vector2){ (float)panelX + 20, (float)panelY + 20 }, 32, 1.0f, WHITE);
|
|
|
|
// List saves
|
|
std::vector<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.empty()) {
|
|
DrawTextEx(customFont, "No saved worlds found.", (Vector2){ (float)panelX + 40, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY);
|
|
} else {
|
|
for (size_t i = 0; i < savedWorlds.size() && i < 5; i++) {
|
|
Rectangle worldBtn = { (float)panelX + 40, (float)panelY + 100 + (float)(i * 60), 520, 50 };
|
|
|
|
Rectangle deleteBtn = { worldBtn.x + worldBtn.width - 50, worldBtn.y + 5, 40, 40 };
|
|
bool isDeleteHovered = CheckCollisionPointRec(mousePos, deleteBtn);
|
|
bool isHovered = CheckCollisionPointRec(mousePos, worldBtn) && !isDeleteHovered;
|
|
|
|
DrawRectangleRec(worldBtn, isHovered ? (Color){ 80, 80, 80, 255 } : DARKGRAY);
|
|
DrawRectangleLinesEx(worldBtn, 2.0f, isHovered ? WHITE : GRAY);
|
|
DrawTextEx(customFont, savedWorlds[i].c_str(), (Vector2){ worldBtn.x + 10, worldBtn.y + 15 }, 20, 1.0f, WHITE);
|
|
|
|
// Draw delete button
|
|
DrawRectangleRec(deleteBtn, isDeleteHovered ? RED : MAROON);
|
|
DrawRectangleLinesEx(deleteBtn, 2.0f, isDeleteHovered ? WHITE : GRAY);
|
|
DrawTextEx(customFont, "X", (Vector2){ deleteBtn.x + 13, deleteBtn.y + 10 }, 20, 1.0f, WHITE);
|
|
|
|
if (!showDeleteConfirm) {
|
|
if (isDeleteHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
deletingWorldName = savedWorlds[i];
|
|
showDeleteConfirm = true;
|
|
} else if (isHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
// Load this world
|
|
currentWorldName = savedWorlds[i];
|
|
|
|
// Load seed and player position from world.dat
|
|
std::ifstream worldFile("saves/" + currentWorldName + "/world.dat");
|
|
float savedX = 0.0f, savedY = -1.0f, savedZ = 0.0f;
|
|
if (worldFile.is_open()) {
|
|
worldFile >> globalSeedHash >> savedX >> savedY >> savedZ;
|
|
worldFile.close();
|
|
} else {
|
|
globalSeedHash = 12345;
|
|
}
|
|
|
|
// Clear old chunks just in case
|
|
for (auto& pair : worldChunks) {
|
|
delete pair.second;
|
|
}
|
|
worldChunks.clear();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
currentState = GAMEPLAY;
|
|
DisableCursor();
|
|
inventoryOpen = false;
|
|
|
|
// Restore saved position, or find surface if no save
|
|
float spawnY;
|
|
if (savedY > 0.0f) {
|
|
spawnY = savedY;
|
|
} else {
|
|
spawnY = FindSpawnY((int)savedX, (int)savedZ);
|
|
}
|
|
camera3D.position = (Vector3){ savedX, spawnY, savedZ };
|
|
camera3D.target = (Vector3){ savedX, spawnY, savedZ + 1.0f };
|
|
playerVelocityY = 0.0f;
|
|
camYaw = 0.0f; camPitch = 0.0f;
|
|
|
|
// Load inventory from disk
|
|
std::ifstream invf("saves/" + currentWorldName + "/inventory.dat", std::ios::binary);
|
|
if (invf.is_open()) {
|
|
invf.read((char*)hotbar, sizeof(hotbar));
|
|
invf.read((char*)inventory, sizeof(inventory));
|
|
invf.read((char*)&activeHotbarSlot, sizeof(activeHotbarSlot));
|
|
invf.close();
|
|
}
|
|
|
|
// Pre-generate all chunks around spawn so world appears immediately
|
|
int spawnCX = (int)floorf(savedX / CHUNK_SIZE);
|
|
int spawnCZ = (int)floorf(savedZ / CHUNK_SIZE);
|
|
for (int cx = spawnCX - RENDER_DISTANCE; cx <= spawnCX + RENDER_DISTANCE; cx++)
|
|
for (int cz = spawnCZ - RENDER_DISTANCE; cz <= spawnCZ + RENDER_DISTANCE; cz++)
|
|
GenerateChunk(cx, cz);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Back Button
|
|
int btnWidth = 200;
|
|
int btnHeight = 50;
|
|
int backBtnX = panelX + (panelWidth / 2) - (btnWidth / 2);
|
|
int btnY = panelY + panelHeight - 80;
|
|
|
|
Rectangle backBtnBounds = { (float)backBtnX, (float)btnY, (float)btnWidth, (float)btnHeight };
|
|
bool isBackHovered = CheckCollisionPointRec(mousePos, backBtnBounds);
|
|
DrawRectangleRec(backBtnBounds, isBackHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 });
|
|
DrawRectangleLinesEx(backBtnBounds, 3.0f, isBackHovered ? WHITE : GRAY);
|
|
Vector2 backTextSize = MeasureTextEx(customFont, "Back", 20, 1.0f);
|
|
DrawTextEx(customFont, "Back", (Vector2){ backBtnX + (btnWidth/2) - (backTextSize.x/2), btnY + (btnHeight/2) - (backTextSize.y/2) }, 20, 1.0f, WHITE);
|
|
|
|
if (!showDeleteConfirm) {
|
|
if (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
currentState = MAIN_MENU;
|
|
}
|
|
}
|
|
|
|
// Draw Confirmation Overlay
|
|
if (showDeleteConfirm) {
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 200 });
|
|
|
|
int confWidth = 400;
|
|
int confHeight = 200;
|
|
int confX = (currentWidth / 2) - (confWidth / 2);
|
|
int confY = (currentHeight / 2) - (confHeight / 2);
|
|
|
|
DrawRectangle(confX, confY, confWidth, confHeight, (Color){ 50, 20, 20, 255 });
|
|
DrawRectangleLinesEx((Rectangle){ (float)confX, (float)confY, (float)confWidth, (float)confHeight }, 4.0f, RED);
|
|
|
|
Vector2 warnSize = MeasureTextEx(customFont, "Delete World?", 24, 1.0f);
|
|
DrawTextEx(customFont, "Delete World?", (Vector2){ confX + (confWidth/2) - (warnSize.x/2), confY + 30 }, 24, 1.0f, WHITE);
|
|
|
|
std::string warnText = "Delete '" + deletingWorldName + "'?";
|
|
Vector2 nameSize = MeasureTextEx(customFont, warnText.c_str(), 20, 1.0f);
|
|
DrawTextEx(customFont, warnText.c_str(), (Vector2){ confX + (confWidth/2) - (nameSize.x/2), confY + 70 }, 20, 1.0f, LIGHTGRAY);
|
|
|
|
// Yes Button
|
|
Rectangle yesBtn = { (float)confX + 50, (float)confY + 130, 120, 40 };
|
|
bool isYesHovered = CheckCollisionPointRec(mousePos, yesBtn);
|
|
DrawRectangleRec(yesBtn, isYesHovered ? RED : MAROON);
|
|
DrawRectangleLinesEx(yesBtn, 2.0f, isYesHovered ? WHITE : GRAY);
|
|
DrawTextEx(customFont, "YES", (Vector2){ yesBtn.x + 40, yesBtn.y + 10 }, 20, 1.0f, WHITE);
|
|
|
|
// No Button
|
|
Rectangle noBtn = { (float)confX + confWidth - 170, (float)confY + 130, 120, 40 };
|
|
bool isNoHovered = CheckCollisionPointRec(mousePos, noBtn);
|
|
DrawRectangleRec(noBtn, isNoHovered ? (Color){ 100, 100, 100, 255 } : DARKGRAY);
|
|
DrawRectangleLinesEx(noBtn, 2.0f, isNoHovered ? WHITE : GRAY);
|
|
DrawTextEx(customFont, "NO", (Vector2){ noBtn.x + 45, noBtn.y + 10 }, 20, 1.0f, WHITE);
|
|
|
|
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
if (isYesHovered) {
|
|
std::filesystem::remove_all("saves/" + deletingWorldName);
|
|
showDeleteConfirm = false;
|
|
} else if (isNoHovered) {
|
|
showDeleteConfirm = false;
|
|
}
|
|
}
|
|
}
|
|
} else if (currentState == OPTIONS_MENU) {
|
|
targetZoom = 1.0f;
|
|
|
|
// Draw dark overlay
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 });
|
|
|
|
// Draw options panel
|
|
int panelWidth = 600;
|
|
int panelHeight = 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);
|
|
|
|
// Multiplayer Options
|
|
DrawTextEx(customFont, "Multiplayer", (Vector2){ (float)panelX + 40, (float)panelY + 290 }, 20, 1.0f, LIGHTGRAY);
|
|
Rectangle serverCheck = { (float)panelX + 40, (float)panelY + 320, 20, 20 };
|
|
bool isServerHovered = CheckCollisionPointRec(mousePos, serverCheck);
|
|
DrawRectangleRec(serverCheck, serverMode ? GREEN : DARKGRAY);
|
|
DrawRectangleLinesEx(serverCheck, 2.0f, isServerHovered ? WHITE : GRAY);
|
|
DrawTextEx(customFont, "Server Mode (Experimental)", (Vector2){ serverCheck.x + 35, serverCheck.y }, 18, 1.0f, WHITE);
|
|
|
|
// Connect Button
|
|
Rectangle connBtn = { (float)currentWidth/2 - 100, (float)currentHeight/2 + 70, 200, 40 };
|
|
bool isConnHovered = CheckCollisionPointRec(mousePos, connBtn);
|
|
DrawRectangleRec(connBtn, isConnHovered ? DARKGRAY : BLACK);
|
|
DrawRectangleLinesEx(connBtn, 2, GRAY);
|
|
DrawTextEx(customFont, "CONNECT", (Vector2){ connBtn.x + 50, connBtn.y + 10 }, 20, 1.0f, WHITE);
|
|
if (isConnHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
currentState = CONNECT_MENU;
|
|
}
|
|
|
|
if (isServerHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
serverMode = !serverMode;
|
|
SaveConfig();
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Draw dark overlay
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 180 });
|
|
|
|
// Draw Create World panel
|
|
int panelWidth = 600;
|
|
int panelHeight = 500;
|
|
int panelX = (currentWidth / 2) - (panelWidth / 2);
|
|
int panelY = (currentHeight / 2) - (panelHeight / 2);
|
|
|
|
DrawRectangle(panelX, panelY, panelWidth, panelHeight, (Color){ 40, 40, 40, 240 });
|
|
DrawRectangleLinesEx((Rectangle){ (float)panelX, (float)panelY, (float)panelWidth, (float)panelHeight }, 4.0f, (Color){ 100, 100, 100, 255 });
|
|
|
|
// Title
|
|
DrawTextEx(customFont, "Create New World", (Vector2){ (float)panelX + 20, (float)panelY + 20 }, 32, 1.0f, WHITE);
|
|
|
|
// World Name
|
|
DrawTextEx(customFont, "World Name", (Vector2){ (float)panelX + 40, (float)panelY + 100 }, 20, 1.0f, LIGHTGRAY);
|
|
Rectangle nameBox = { (float)panelX + 40, (float)panelY + 130, 520, 40 };
|
|
|
|
if (CheckCollisionPointRec(mousePos, nameBox) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) activeTextBox = 1;
|
|
|
|
DrawRectangleRec(nameBox, DARKGRAY);
|
|
DrawRectangleLinesEx(nameBox, 2.0f, activeTextBox == 1 ? WHITE : GRAY);
|
|
DrawTextEx(customFont, worldName, (Vector2){ nameBox.x + 10, nameBox.y + 10 }, 20, 1.0f, WHITE);
|
|
|
|
// Blinking cursor
|
|
if (activeTextBox == 1 && ((int)(GetTime() * 2) % 2 == 0)) {
|
|
int textW = MeasureTextEx(customFont, worldName, 20, 1.0f).x;
|
|
DrawRectangle(nameBox.x + 12 + textW, nameBox.y + 10, 12, 20, LIGHTGRAY);
|
|
}
|
|
|
|
// World Seed
|
|
DrawTextEx(customFont, "Seed (Optional)", (Vector2){ (float)panelX + 40, (float)panelY + 200 }, 20, 1.0f, LIGHTGRAY);
|
|
Rectangle seedBox = { (float)panelX + 40, (float)panelY + 230, 520, 40 };
|
|
|
|
if (CheckCollisionPointRec(mousePos, seedBox) && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) activeTextBox = 2;
|
|
// Click outside to unfocus
|
|
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(mousePos, nameBox) && !CheckCollisionPointRec(mousePos, seedBox)) {
|
|
activeTextBox = 0;
|
|
}
|
|
|
|
DrawRectangleRec(seedBox, DARKGRAY);
|
|
DrawRectangleLinesEx(seedBox, 2.0f, activeTextBox == 2 ? WHITE : GRAY);
|
|
DrawTextEx(customFont, worldSeed, (Vector2){ seedBox.x + 10, seedBox.y + 10 }, 20, 1.0f, WHITE);
|
|
|
|
if (activeTextBox == 2 && ((int)(GetTime() * 2) % 2 == 0)) {
|
|
int textW = MeasureTextEx(customFont, worldSeed, 20, 1.0f).x;
|
|
DrawRectangle(seedBox.x + 12 + textW, seedBox.y + 10, 12, 20, LIGHTGRAY);
|
|
}
|
|
|
|
// Back Button
|
|
int btnWidth = 200;
|
|
int btnHeight = 50;
|
|
int backBtnX = panelX + 40;
|
|
int btnY = panelY + panelHeight - 80;
|
|
|
|
Rectangle backBtnBounds = { (float)backBtnX, (float)btnY, (float)btnWidth, (float)btnHeight };
|
|
bool isBackHovered = CheckCollisionPointRec(mousePos, backBtnBounds);
|
|
DrawRectangleRec(backBtnBounds, isBackHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 });
|
|
DrawRectangleLinesEx(backBtnBounds, 3.0f, isBackHovered ? WHITE : GRAY);
|
|
Vector2 backTextSize = MeasureTextEx(customFont, "Back", 20, 1.0f);
|
|
DrawTextEx(customFont, "Back", (Vector2){ backBtnX + (btnWidth/2) - (backTextSize.x/2), btnY + (btnHeight/2) - (backTextSize.y/2) }, 20, 1.0f, WHITE);
|
|
|
|
if (isBackHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
currentState = MAIN_MENU;
|
|
}
|
|
|
|
// Create Button
|
|
int createBtnX = panelX + panelWidth - 40 - btnWidth;
|
|
Rectangle createBtnBounds = { (float)createBtnX, (float)btnY, (float)btnWidth, (float)btnHeight };
|
|
bool isCreateHovered = CheckCollisionPointRec(mousePos, createBtnBounds);
|
|
DrawRectangleRec(createBtnBounds, isCreateHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 });
|
|
DrawRectangleLinesEx(createBtnBounds, 3.0f, isCreateHovered ? WHITE : GRAY);
|
|
Vector2 createTextSize = MeasureTextEx(customFont, "Create", 20, 1.0f);
|
|
DrawTextEx(customFont, "Create", (Vector2){ createBtnX + (btnWidth/2) - (createTextSize.x/2), btnY + (btnHeight/2) - (createTextSize.y/2) }, 20, 1.0f, WHITE);
|
|
|
|
if (isCreateHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
currentWorldName = worldName;
|
|
int nameCounter = 1;
|
|
while (std::filesystem::exists("saves/" + currentWorldName)) {
|
|
currentWorldName = std::string(worldName) + " (" + std::to_string(nameCounter) + ")";
|
|
nameCounter++;
|
|
}
|
|
// Update the worldName string so the HUD shows it correctly
|
|
snprintf(worldName, sizeof(worldName), "%s", currentWorldName.c_str());
|
|
worldNameLen = strlen(worldName);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
currentState = GAMEPLAY;
|
|
DisableCursor(); // Hide and lock cursor for first-person control
|
|
|
|
std::filesystem::create_directories("saves/" + currentWorldName);
|
|
|
|
// Simple hash for seed string
|
|
globalSeedHash = 0;
|
|
for (int i = 0; worldSeed[i] != '\0'; i++) {
|
|
globalSeedHash = globalSeedHash * 31 + worldSeed[i];
|
|
}
|
|
if (worldSeed[0] == '\0') globalSeedHash = GetRandomValue(0, 100000);
|
|
|
|
// Clear old chunks
|
|
for (auto& pair : worldChunks) {
|
|
delete pair.second;
|
|
}
|
|
worldChunks.clear();
|
|
|
|
// Generate spawn chunk and place player on surface
|
|
float spawnY = FindSpawnY(0, 0);
|
|
camera3D.position = (Vector3){ 0.0f, spawnY, 0.0f };
|
|
camera3D.target = (Vector3){ 0.0f, spawnY, 1.0f };
|
|
playerVelocityY = 0.0f;
|
|
camYaw = 0.0f; camPitch = 0.0f; // Reset look so WASD matches view
|
|
|
|
// Re-write world.dat with correct spawnY now that we know it
|
|
std::ofstream worldFile2("saves/" + currentWorldName + "/world.dat");
|
|
if (worldFile2.is_open()) {
|
|
worldFile2 << globalSeedHash << " "
|
|
<< camera3D.position.x << " "
|
|
<< camera3D.position.y << " "
|
|
<< camera3D.position.z;
|
|
worldFile2.close();
|
|
}
|
|
|
|
// Give starter kit to new player
|
|
for (int i = 0; i < 9; i++) { hotbar[i] = InventorySlot(AIR, 0); }
|
|
for (int i = 0; i < 27; i++) { inventory[i] = InventorySlot(AIR, 0); }
|
|
hotbar[0] = InventorySlot(DIRT, 64);
|
|
hotbar[1] = InventorySlot(COBBLESTONE, 64);
|
|
hotbar[2] = InventorySlot(PLANK, 64);
|
|
hotbar[3] = InventorySlot(SAND, 64);
|
|
hotbar[4] = InventorySlot(GRASS, 64);
|
|
activeHotbarSlot = 0;
|
|
}
|
|
}
|
|
|
|
// Draw Gameplay overlay if we entered gameplay
|
|
if (currentState == GAMEPLAY || currentState == PAUSE_MENU || currentState == CRAFTING_GUI) {
|
|
// ---- 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) {
|
|
DrawCapsule(rp.position, (Vector3){ rp.position.x, rp.position.y + 1.8f, rp.position.z }, 0.4f, 8, 8, BLUE);
|
|
// Draw Nameplate
|
|
Vector2 namePos = GetWorldToScreen((Vector3){ rp.position.x, rp.position.y + 2.2f, 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++) {
|
|
// FRUSTUM CULLING
|
|
Vector3 chunkCenter = { (float)(cx * CHUNK_SIZE + 8), 32.0f, (float)(cz * CHUNK_SIZE + 8) };
|
|
Vector3 toChunk = Vector3Normalize(Vector3Subtract(chunkCenter, camera3D.position));
|
|
if (Vector3Length(Vector3Subtract(chunkCenter, camera3D.position)) > 24.0f &&
|
|
Vector3DotProduct(toChunk, camForward) < -0.2f) 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);
|
|
}
|
|
}
|
|
|
|
// 2. Batch render per type
|
|
for (int renderType = 1; renderType <= 13; renderType++) {
|
|
if (renderType == GRASS) {
|
|
for (Chunk* chunk : visibleChunks) {
|
|
for (auto& pos : chunk->renderLists[GRASS]) {
|
|
DrawGrassBlock(pos, blockTextures[GRASS].id, grassTopTexture.id, blockTextures[DIRT].id, blockTint);
|
|
}
|
|
}
|
|
} else if (renderType == CRAFTING_TABLE) {
|
|
for (Chunk* chunk : visibleChunks) {
|
|
for (auto& pos : chunk->renderLists[CRAFTING_TABLE]) {
|
|
DrawCraftingTable(pos, craftingSideTexture.id, craftingTopTexture.id, blockTextures[DIRT].id, blockTint);
|
|
}
|
|
}
|
|
} else {
|
|
rlSetTexture(blockTextures[renderType].id);
|
|
rlBegin(RL_QUADS);
|
|
rlColor4ub(blockTint.r, blockTint.g, blockTint.b, 255);
|
|
if (renderType == LEAVES) {
|
|
rlColor4ub((unsigned char)(blockTint.r * 0.2f), (unsigned char)(blockTint.g * 0.6f), (unsigned char)(blockTint.b * 0.2f), 255);
|
|
}
|
|
|
|
for (Chunk* chunk : visibleChunks) {
|
|
for (auto& pos : chunk->renderLists[renderType]) {
|
|
DrawCubeVertices(pos.x, pos.y, pos.z, 1.0f, 1.0f, 1.0f);
|
|
}
|
|
}
|
|
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
|
|
// 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));
|
|
|
|
// Draw Arm (rotated cube)
|
|
// We use rlgl to rotate it to point roughly forward
|
|
rlPushMatrix();
|
|
rlTranslatef(handPos.x, handPos.y, handPos.z);
|
|
rlRotatef(camYaw * 180.0f/PI, 0, 1, 0); // Yaw with camera
|
|
rlRotatef(-20.0f - swingVal * 30.0f, 1, 0, 0); // Slight slant
|
|
DrawCube((Vector3){0,0,0}, 0.08f, 0.1f, 0.35f, (Color){220, 180, 150, 255});
|
|
rlPopMatrix();
|
|
|
|
// Draw Held Item
|
|
int heldBT = hotbar[activeHotbarSlot].blockType;
|
|
if (heldBT != AIR) {
|
|
Vector3 itemPos = Vector3Add(handPos, Vector3Scale(forwardVM, 0.12f));
|
|
itemPos = Vector3Add(itemPos, Vector3Scale(upVM, 0.12f));
|
|
itemPos = Vector3Add(itemPos, Vector3Scale(rightVM, -0.05f));
|
|
|
|
Texture2D itemTex = (heldBT == GRASS) ? grassTopTexture : (heldBT == CRAFTING_TABLE ? craftingSideTexture : blockTextures[heldBT]);
|
|
if (itemTex.id > 0) {
|
|
if (heldBT < STICK) {
|
|
// 3D Block in hand
|
|
rlSetTexture(itemTex.id);
|
|
rlPushMatrix();
|
|
rlTranslatef(itemPos.x, itemPos.y, itemPos.z);
|
|
rlRotatef(camYaw * 180.0f/PI + 45.0f, 0, 1, 0);
|
|
rlRotatef(20.0f, 1, 0, 1);
|
|
DrawCubeVertices(0,0,0, 0.12f, 0.12f, 0.12f);
|
|
rlPopMatrix();
|
|
rlSetTexture(0);
|
|
} else {
|
|
// Item (Stick/Axe) as Billboard - Scaled larger for better visibility
|
|
DrawBillboard(camera3D, itemTex, itemPos, 0.35f, WHITE);
|
|
}
|
|
} else {
|
|
// Fallback
|
|
DrawCube(itemPos, 0.1f, 0.1f, 0.1f, RED);
|
|
}
|
|
}
|
|
rlPopMatrix();
|
|
rlEnableDepthTest();
|
|
}
|
|
|
|
EndMode3D();
|
|
|
|
// Draw 2D Crosshair overlay (hide when inventory open)
|
|
if (!inventoryOpen) {
|
|
DrawRectangle(currentWidth/2 - 2, currentHeight/2 - 10, 4, 20, (Color){255, 255, 255, 150});
|
|
DrawRectangle(currentWidth/2 - 10, currentHeight/2 - 2, 20, 4, (Color){255, 255, 255, 150});
|
|
}
|
|
|
|
// Draw debug info (top-left)
|
|
DrawTextEx(customFont, "MorriCraft Pre-Alpha", (Vector2){ 10, 10 }, 20, 1.0f, BLACK);
|
|
DrawTextEx(customFont, TextFormat("World: %s", worldName), (Vector2){ 10, 40 }, 16, 1.0f, BLACK);
|
|
DrawTextEx(customFont, TextFormat("FPS: %i", GetFPS()), (Vector2){ 10, 60 }, 16, 1.0f, RED);
|
|
DrawTextEx(customFont, TextFormat("XYZ: %.1f / %.1f / %.1f",
|
|
camera3D.position.x, camera3D.position.y - 1.6f, camera3D.position.z),
|
|
(Vector2){ 10, 80 }, 16, 1.0f, DARKGRAY);
|
|
|
|
// --- In-Game Clock ---
|
|
int totalMinutes = (int)(timeOfDay * 24 * 60);
|
|
int hours = (totalMinutes / 60) % 24;
|
|
int minutes = totalMinutes % 60;
|
|
const char* ampm = (hours >= 12) ? "PM" : "AM";
|
|
int displayHours = hours % 12;
|
|
if (displayHours == 0) displayHours = 12;
|
|
|
|
DrawTextEx(customFont, TextFormat("Time: %i:%02i %s", displayHours, minutes, ampm), (Vector2){ 10, 110 }, 18, 1.0f, (dayFactor > 0.5f) ? BLACK : WHITE);
|
|
DrawTextEx(customFont, (dayFactor > 0.5f) ? "Status: Day" : "Status: Night", (Vector2){ 10, 130 }, 16, 1.0f, (dayFactor > 0.5f) ? (Color){180, 150, 0, 255} : (Color){100, 100, 255, 255});
|
|
|
|
// ---- HOTBAR ----
|
|
const int slotSize = 50;
|
|
const int slotGap = 4;
|
|
int hotbarW = 9 * slotSize + 8 * slotGap;
|
|
int hotbarX = currentWidth / 2 - hotbarW / 2;
|
|
int hotbarY = currentHeight - slotSize - 12;
|
|
|
|
// Hotbar background panel
|
|
DrawRectangle(hotbarX - 6, hotbarY - 6, hotbarW + 12, slotSize + 12,
|
|
(Color){ 30, 30, 30, 180 });
|
|
DrawRectangleLinesEx((Rectangle){ (float)(hotbarX-6), (float)(hotbarY-6),
|
|
(float)(hotbarW+12), (float)(slotSize+12) },
|
|
2.0f, (Color){ 80, 80, 80, 220 });
|
|
|
|
for (int s = 0; s < 9; s++) {
|
|
int sx = hotbarX + s * (slotSize + slotGap);
|
|
bool selected = (s == activeHotbarSlot);
|
|
|
|
// Slot background
|
|
DrawRectangle(sx, hotbarY, slotSize, slotSize,
|
|
(Color){ 60, 60, 60, 220 });
|
|
// Slot border (gold for selected, gray otherwise)
|
|
Color borderCol = selected ? (Color){255, 215, 0, 255} : (Color){100, 100, 100, 255};
|
|
float borderW = selected ? 3.0f : 1.5f;
|
|
DrawRectangleLinesEx((Rectangle){ (float)sx, (float)hotbarY,
|
|
(float)slotSize, (float)slotSize },
|
|
borderW, borderCol);
|
|
|
|
// Block texture preview
|
|
if (hotbar[s].count > 0 && hotbar[s].blockType != AIR) {
|
|
int bt = hotbar[s].blockType;
|
|
Texture2D tex = (bt == GRASS) ? grassTopTexture : blockTextures[bt];
|
|
if (tex.id > 0) {
|
|
Rectangle src = { 0, 0, (float)tex.width, (float)tex.height };
|
|
Rectangle dst = { (float)(sx+4), (float)(hotbarY+4),
|
|
(float)(slotSize-8), (float)(slotSize-8) };
|
|
Color tint = (bt == GRASS) ? (Color){124,189,107,255} : WHITE;
|
|
DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint);
|
|
}
|
|
// Item count centered bottom
|
|
Vector2 countSize = MeasureTextEx(customFont, TextFormat("%i", hotbar[s].count), 12, 1.0f);
|
|
DrawTextEx(customFont, TextFormat("%i", hotbar[s].count),
|
|
(Vector2){ (float)(sx + (slotSize/2) - (countSize.x/2)), (float)(hotbarY + slotSize - 14) },
|
|
12, 1.0f, WHITE);
|
|
}
|
|
|
|
// Slot number label
|
|
DrawTextEx(customFont, TextFormat("%i", s+1),
|
|
(Vector2){ (float)(sx+3), (float)(hotbarY+2) }, 12, 1.0f,
|
|
(Color){180,180,180,200});
|
|
}
|
|
|
|
// ---- UI HELPERS & STATE ----
|
|
const int invSlotSize = 52;
|
|
const int invGap = 4;
|
|
const int invCols = 9;
|
|
const int invRows = 3;
|
|
const int invPanelW = 550;
|
|
const int invPanelH = 500;
|
|
|
|
auto drawSlotEx = [&](InventorySlot& slot, int px, int py, bool isResultSlot = false, bool isTableResult = false) {
|
|
Rectangle slotRect = { (float)px, (float)py, (float)invSlotSize, (float)invSlotSize };
|
|
bool hov = CheckCollisionPointRec(mousePos, slotRect);
|
|
|
|
DrawRectangleRec(slotRect, hov ? (Color){90,90,90,255} : (Color){60,60,60,255});
|
|
DrawRectangleLinesEx(slotRect, 2.0f, hov ? WHITE : (Color){100,100,100,200});
|
|
|
|
if (slot.count > 0 && slot.blockType != AIR) {
|
|
int bt = slot.blockType;
|
|
Texture2D tex = (bt == GRASS) ? grassTopTexture : (bt == CRAFTING_TABLE ? craftingSideTexture : blockTextures[bt]);
|
|
if (tex.id > 0 && tex.width > 0 && tex.height > 0) {
|
|
Rectangle src = { 0,0,(float)tex.width,(float)tex.height };
|
|
Rectangle dst = { (float)(px+5),(float)(py+5), (float)(invSlotSize-10),(float)(invSlotSize-10) };
|
|
Color tint = (bt == GRASS) ? (Color){124,189,107,255} : WHITE;
|
|
DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint);
|
|
} else if (bt >= STICK) {
|
|
// Fallback rendering for items if texture failed
|
|
Color itemCol = (bt == STICK) ? BROWN : DARKGRAY;
|
|
DrawRectangle(px + 10, py + 10, invSlotSize - 20, invSlotSize - 20, itemCol);
|
|
}
|
|
Vector2 cSize = MeasureTextEx(customFont, TextFormat("%i", slot.count), 12, 1.0f);
|
|
DrawTextEx(customFont, TextFormat("%i", slot.count),
|
|
(Vector2){ (float)(px + (invSlotSize/2) - (cSize.x/2)), (float)(py + invSlotSize - 14) },
|
|
12, 1.0f, WHITE);
|
|
}
|
|
|
|
if (hov && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
|
if (isResultSlot) {
|
|
if (mouseHeldItem.blockType == AIR && slot.blockType != AIR) {
|
|
mouseHeldItem = slot;
|
|
slot = InventorySlot(AIR, 0);
|
|
if (isTableResult) {
|
|
for(int i=0; i<9; i++) {
|
|
if (tableSlots[i].count > 0) tableSlots[i].count--;
|
|
if (tableSlots[i].count == 0) tableSlots[i].blockType = AIR;
|
|
}
|
|
UpdateTableCrafting();
|
|
} else {
|
|
for(int i=0; i<4; i++) {
|
|
if (craftingSlots[i].count > 0) craftingSlots[i].count--;
|
|
if (craftingSlots[i].count == 0) craftingSlots[i].blockType = AIR;
|
|
}
|
|
UpdateCrafting();
|
|
}
|
|
}
|
|
} else {
|
|
InventorySlot tmp = slot;
|
|
slot = mouseHeldItem;
|
|
mouseHeldItem = tmp;
|
|
UpdateCrafting();
|
|
UpdateTableCrafting();
|
|
}
|
|
}
|
|
|
|
if (hov && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) {
|
|
if (!isResultSlot) {
|
|
if (mouseHeldItem.blockType == AIR) {
|
|
// Pick up half? Not requested, but common.
|
|
// For now just following user request: "holding a stack... right click into a box, place one"
|
|
} else {
|
|
// Holding something, place one
|
|
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;
|
|
DisableCursor();
|
|
}
|
|
}
|
|
|
|
// --- Draw Held Item Last ---
|
|
if (mouseHeldItem.blockType != AIR && mouseHeldItem.count > 0) {
|
|
int bt = mouseHeldItem.blockType;
|
|
Texture2D tex = (bt == GRASS) ? grassTopTexture : (bt == CRAFTING_TABLE ? craftingSideTexture : blockTextures[bt]);
|
|
if (tex.id > 0) {
|
|
Rectangle src = { 0,0,(float)tex.width,(float)tex.height };
|
|
Rectangle dst = { mousePos.x - (invSlotSize/2), mousePos.y - (invSlotSize/2), (float)invSlotSize, (float)invSlotSize };
|
|
Color tint = (bt == GRASS) ? (Color){124,189,107,255} : WHITE;
|
|
DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint);
|
|
Vector2 hSize = MeasureTextEx(customFont, TextFormat("%i", mouseHeldItem.count), 12, 1.0f);
|
|
DrawTextEx(customFont, TextFormat("%i", mouseHeldItem.count), (Vector2){ dst.x + invSlotSize/2 - hSize.x/2, dst.y + invSlotSize - 14 }, 12, 1.0f, WHITE);
|
|
}
|
|
}
|
|
|
|
if (currentState == PAUSE_MENU) {
|
|
// Draw dark overlay
|
|
DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 150 });
|
|
|
|
const char* pButtons[] = { "Resume", "Options", "Save & Exit" };
|
|
int pNumButtons = 3;
|
|
int pBtnWidth = 400;
|
|
int pBtnHeight = 60;
|
|
int pBtnSpacing = 15;
|
|
int pStartY = (currentHeight / 2) - ((pNumButtons * pBtnHeight + (pNumButtons - 1) * pBtnSpacing) / 2);
|
|
|
|
for (int i = 0; i < pNumButtons; i++) {
|
|
int pPosX = (currentWidth / 2) - (pBtnWidth / 2);
|
|
int pPosY = pStartY + i * (pBtnHeight + pBtnSpacing);
|
|
|
|
Rectangle btnBounds = { (float)pPosX, (float)pPosY, (float)pBtnWidth, (float)pBtnHeight };
|
|
bool isHovered = CheckCollisionPointRec(mousePos, btnBounds);
|
|
|
|
Color baseColor = isHovered ? (Color){ 100, 100, 100, 255 } : (Color){ 60, 60, 60, 255 };
|
|
Color borderColor = isHovered ? (Color){ 200, 200, 200, 255 } : (Color){ 20, 20, 20, 255 };
|
|
|
|
DrawRectangleRec(btnBounds, baseColor);
|
|
DrawRectangleLinesEx(btnBounds, 3.0f, borderColor);
|
|
|
|
if (isHovered) {
|
|
DrawRectangleLinesEx((Rectangle){btnBounds.x - 2, btnBounds.y - 2, btnBounds.width + 4, btnBounds.height + 4}, 2.0f, (Color){255, 255, 255, 50});
|
|
|
|
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
|
|
if (i == 0) { // Resume
|
|
currentState = GAMEPLAY;
|
|
DisableCursor();
|
|
} else if (i == 1) { // Options
|
|
optionsReturnState = PAUSE_MENU;
|
|
currentState = OPTIONS_MENU;
|
|
} else if (i == 2) { // Save & Exit
|
|
currentState = MAIN_MENU;
|
|
inventoryOpen = false;
|
|
|
|
// Save player position to world.dat
|
|
std::ofstream wf("saves/" + currentWorldName + "/world.dat");
|
|
if (wf.is_open()) {
|
|
wf << globalSeedHash << " "
|
|
<< camera3D.position.x << " "
|
|
<< camera3D.position.y << " "
|
|
<< camera3D.position.z;
|
|
wf.close();
|
|
}
|
|
// Save inventory
|
|
std::ofstream invf("saves/" + currentWorldName + "/inventory.dat", std::ios::binary);
|
|
if (invf.is_open()) {
|
|
invf.write((char*)hotbar, sizeof(hotbar));
|
|
invf.write((char*)inventory, sizeof(inventory));
|
|
invf.write((char*)&activeHotbarSlot, sizeof(activeHotbarSlot));
|
|
invf.close();
|
|
}
|
|
|
|
// Save chunks and clear memory
|
|
for (auto& pair : worldChunks) {
|
|
SaveChunk(pair.second, pair.first.x, pair.first.z);
|
|
delete pair.second;
|
|
}
|
|
worldChunks.clear();
|
|
currentWorldName = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
Vector2 textSize = MeasureTextEx(customFont, pButtons[i], 24, 1.0f);
|
|
DrawTextEx(customFont, pButtons[i], (Vector2){ (float)pPosX + (pBtnWidth / 2) - (textSize.x / 2) + 2, (float)pPosY + (pBtnHeight / 2) - (textSize.y / 2) + 2 }, 24, 1.0f, (Color){ 30, 30, 30, 255 });
|
|
Color textColor = isHovered ? (Color){ 255, 255, 160, 255 } : (Color){ 220, 220, 220, 255 };
|
|
DrawTextEx(customFont, pButtons[i], (Vector2){ (float)pPosX + (pBtnWidth / 2) - (textSize.x / 2), (float)pPosY + (pBtnHeight / 2) - (textSize.y / 2) }, 24, 1.0f, textColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
EndDrawing();
|
|
//----------------------------------------------------------------------------------
|
|
}
|
|
|
|
// De-Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
for (int i = 1; i <= 5; i++) UnloadTexture(blockTextures[i]);
|
|
for (int i = 7; i <= 11; i++) UnloadTexture(blockTextures[i]);
|
|
UnloadTexture(grassTopTexture);
|
|
UnloadTexture(titleTexture); // Texture unloading
|
|
|
|
for (auto& pair : worldChunks) {
|
|
delete pair.second;
|
|
}
|
|
UnloadFont(customFont); // Font unloading
|
|
UnloadMusicStream(titleMusic); // Music unloading
|
|
UnloadMusicStream(gameplayMusic);
|
|
|
|
CloseAudioDevice(); // Close audio device
|
|
CloseWindow(); // Close window and OpenGL context
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
return 0;
|
|
}
|