#include #include #define RAYGUI_IMPLEMENTATION #include "includes/raygui.h" #include #include #include #include #include #include #include "entity/player/PlayerController.hpp" #include "entity/player/PlayerRenderer.hpp" #include "net/NetworkManager.hpp" #include "sky/Sky.hpp" #include "render/RenderContext.hpp" #include "utils/Coords.hpp" #include "terrain/Heightmap.hpp" #include "config.hpp" enum GameState { STATE_LOGIN, STATE_CONNECTING, STATE_PLAYING }; class Game { GameConfig config; PlayerController playerController; std::unique_ptr playerRenderer; RenderContext renderContext; Model terrainModel; Heightmap heightmap; NetworkManager network; std::unique_ptr sky; Vector3 playerPos{0, 0, 0}; Texture2D terrainTexture; // Login UI state GameState gameState = STATE_LOGIN; char usernameBuffer[32] = ""; bool editMode = false; std::string loginError = ""; std::string currentUsername = ""; // Heartbeat timing float lastHeartbeatTime = 0.0f; // Debug options bool showDebugAxes = false; bool showWorldBounds = false; public: Game(const GameConfig& cfg) : config(cfg) { InitWindow(config.windowWidth, config.windowHeight, config.windowTitle.c_str()); SetTargetFPS(config.targetFPS); // Initialize components after window is created playerRenderer = std::make_unique(); sky = std::make_unique(); // Configure and load heightmap Heightmap::Config heightConfig; heightConfig.unitsPerSample = config.unitsPerSample; heightConfig.heightScale = config.heightScale; heightmap = Heightmap(heightConfig); if (!heightmap.load(config.heightmapFile)) { std::cerr << "Failed to load heightmap\n"; } else { // Update world bounds based on loaded heightmap Coords::setWorldBounds(heightmap.getWorldBounds()); std::cout << "Loaded heightmap: " << heightmap.getSamplesPerSide() << "x" << heightmap.getSamplesPerSide() << " samples, " << "world size: " << heightmap.getWorldWidth() << "x" << heightmap.getWorldHeight() << " units\n"; } // Load textures terrainTexture = LoadTexture("../assets/textures/black.png"); // Create terrain model auto terrainMesh = heightmap.generateMesh(); terrainModel = LoadModelFromMesh(terrainMesh); terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = terrainTexture; } ~Game() { // Send logout if we're connected if (gameState == STATE_PLAYING && network.isConnected()) { network.sendLogout(); } UnloadTexture(terrainTexture); UnloadModel(terrainModel); CloseWindow(); } void run() { while (!WindowShouldClose()) { update(); render(); } // Clean logout when window is closing if (gameState == STATE_PLAYING && network.isConnected()) { network.sendLogout(); // Give the network time to send the packet std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } private: void update() { switch (gameState) { case STATE_LOGIN: // Login screen - no game updates break; case STATE_CONNECTING: // Check if we've received a response from the server if (network.isConnected()) { gameState = STATE_PLAYING; currentUsername = std::string(usernameBuffer); } else if (network.hasLoginError()) { gameState = STATE_LOGIN; loginError = network.getLoginError(); } break; case STATE_PLAYING: updateGame(); break; } } void updateGame() { if (!network.isConnected()) { // Player disconnected, go back to login gameState = STATE_LOGIN; loginError = "Disconnected from server"; return; } // Update sky with server time if connected, otherwise use local time if (network.isConnected()) { float serverTime = network.getServerTimeOfDay(); sky->updateFromServerTime(serverTime); } else { sky->update(GetFrameTime()); } // Time of day controls (for testing) if (IsKeyPressed(KEY_T)) { float currentTime = sky->getTimeOfDay(); currentTime += 0.1f; if (currentTime > 1.0f) currentTime -= 1.0f; sky->setTimeOfDay(currentTime); } // Debug toggles if (IsKeyPressed(KEY_F1)) showDebugAxes = !showDebugAxes; if (IsKeyPressed(KEY_F2)) showWorldBounds = !showWorldBounds; // Get server position and update player controller playerPos = network.getPosition(); // Clamp position to world bounds playerPos = Coords::clampToWorldBounds(playerPos); playerController.setPlayerPosition(playerPos); // Clean up disconnected player models auto remotePlayers = network.getRemotePlayers(); playerRenderer->cleanupDisconnectedPlayers(remotePlayers); // Update player controller (handles camera) float deltaTime = GetFrameTime(); playerController.update(deltaTime); // Get movement input from player controller Vector3 moveInput = playerController.getMoveInput(); // Send normalized movement direction to server (server handles speed) if (moveInput.x != 0 || moveInput.z != 0) { network.sendMove(moveInput.x, 0, moveInput.z); lastHeartbeatTime = GetTime(); // Reset heartbeat timer when moving } // Send periodic heartbeats when not moving float currentTime = GetTime(); if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) { network.sendHeartbeat(); lastHeartbeatTime = currentTime; } // Handle color change with arrow keys static int currentColorIndex = -1; if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_RIGHT)) { // Get current color index if not set if (currentColorIndex == -1) { auto currentColor = network.getPlayerColor(); for (size_t i = 0; i < NetworkManager::AVAILABLE_COLORS.size(); i++) { if (NetworkManager::AVAILABLE_COLORS[i] == currentColor) { currentColorIndex = i; break; } } if (currentColorIndex == -1) currentColorIndex = 0; } // Change color index if (IsKeyPressed(KEY_LEFT)) { currentColorIndex--; if (currentColorIndex < 0) { currentColorIndex = NetworkManager::AVAILABLE_COLORS.size() - 1; } } else if (IsKeyPressed(KEY_RIGHT)) { currentColorIndex++; if (currentColorIndex >= (int)NetworkManager::AVAILABLE_COLORS.size()) { currentColorIndex = 0; } } // Send color change to server network.sendColorChange(NetworkManager::AVAILABLE_COLORS[currentColorIndex]); } // Handle logout if (IsKeyPressed(KEY_ESCAPE)) { network.sendLogout(); gameState = STATE_LOGIN; loginError = ""; } } void render() { BeginDrawing(); // Clear with a default color first ClearBackground(SKYBLUE); switch (gameState) { case STATE_LOGIN: renderLoginScreen(); break; case STATE_CONNECTING: renderConnectingScreen(); break; case STATE_PLAYING: renderGame(); break; } EndDrawing(); } void renderLoginScreen() { // Center the login box int boxWidth = 300; int boxHeight = 200; int boxX = (GetScreenWidth() - boxWidth) / 2; int boxY = (GetScreenHeight() - boxHeight) / 2; // Draw login panel GuiPanel((Rectangle){(float)boxX, (float)boxY, (float)boxWidth, (float)boxHeight}, "Login"); // Username label and text box GuiLabel((Rectangle){(float)(boxX + 20), (float)(boxY + 50), 80, 30}, "Username:"); if (GuiTextBox((Rectangle){(float)(boxX + 100), (float)(boxY + 50), 180, 30}, usernameBuffer, 32, editMode)) { editMode = !editMode; } // Login button if (GuiButton((Rectangle){(float)(boxX + 100), (float)(boxY + 100), 100, 30}, "Login")) { if (strlen(usernameBuffer) > 0) { // Send login with username network.sendLoginWithUsername(usernameBuffer); gameState = STATE_CONNECTING; loginError = ""; } else { loginError = "Please enter a username"; } } // Error message if (!loginError.empty()) { DrawText(loginError.c_str(), boxX + 20, boxY + 150, 20, RED); } // Instructions DrawText("Enter your username to join the game", 10, 10, 20, WHITE); } void renderConnectingScreen() { DrawText("Connecting to server...", GetScreenWidth()/2 - 100, GetScreenHeight()/2 - 10, 20, WHITE); } void renderGame() { // Begin 3D rendering with render context renderContext.begin3D(playerController.getCamera()); // Submit skybox first (lowest layer) renderContext.submitCustom([this]() { if (sky) { sky->renderSkybox(playerController.getCamera()); } }, RenderContext::RenderLayer::SKYBOX); // Submit terrain renderContext.submitModel(terrainModel, {0, 0, 0}, 1.0f, WHITE, RenderContext::RenderLayer::TERRAIN); // Submit players if connected if (network.isConnected()) { // Local player playerRenderer->renderLocalPlayer(renderContext, playerPos, network.getPlayerColor()); // Remote players auto remotePlayers = network.getRemotePlayers(); playerRenderer->renderRemotePlayers(renderContext, remotePlayers); } // Debug rendering if (showDebugAxes) { renderContext.submitCustom([this]() { Coords::drawDebugAxes(playerPos, 2.0f); // Draw world origin axes Coords::drawDebugAxes({0, 0, 0}, 5.0f); }, RenderContext::RenderLayer::TRANSPARENT); } if (showWorldBounds) { renderContext.submitCustom([]() { Coords::drawWorldBounds(); }, RenderContext::RenderLayer::TRANSPARENT); } // Execute all render commands in order renderContext.end3D(); // UI DrawText(TextFormat("Logged in as: %s", currentUsername.c_str()), 10, 10, 20, WHITE); DrawText("WASD: Move | Q/E: Strafe | Right-Click: Rotate Camera", 10, 35, 20, WHITE); DrawText("Left/Right Arrow: Change Color | Mouse Wheel: Zoom | ESC: Logout", 10, 60, 20, WHITE); DrawText("T: Change Time | F1: Debug Axes | F2: World Bounds", 10, 85, 20, WHITE); if (network.isConnected()) { std::string colorText = "Your color: " + network.getPlayerColor(); DrawText(colorText.c_str(), 10, 110, 20, WHITE); } // Show time of day float timeHour = sky->getTimeOfDay() * 24.0f; int hour = (int)timeHour; int minute = (int)((timeHour - hour) * 60); DrawText(TextFormat("Time: %02d:%02d", hour, minute), 10, 135, 20, WHITE); // Show position if debug is on if (showDebugAxes) { DrawText(TextFormat("Pos: %.1f, %.1f, %.1f", playerPos.x, playerPos.y, playerPos.z), 10, 160, 20, YELLOW); } DrawFPS(10, showDebugAxes ? 185 : 160); } }; int main(int argc, char* argv[]) { GameConfig config = GameConfig::fromArgs(argc, argv); // Show configuration if debug is enabled if (config.showDebugInfo) { std::cout << "Configuration:\n"; std::cout << " Heightmap: " << config.heightmapFile << "\n"; std::cout << " Units per sample: " << config.unitsPerSample << "\n"; std::cout << " Height scale: " << config.heightScale << "\n"; } Game game(config); game.run(); return 0; }