#include "raylib.h" #include "raymath.h" #include "rlgl.h" #include #include #include #include "shared.h" #include "client.h" #include "net/net.h" #define FPS_SAMPLES 120 double fpsSamples[FPS_SAMPLES] = {0}; int fpsSampleIndex = 0; double avgFps = 0; #define HUD_FPS_X 10 #define HUD_PING_X 120 #define HUD_COORDS_X 300 typedef struct { Vector3 position; Vector3 spawnPosition; // remember where they started Vector3 target; // where they're walking to float rotation; float scale; float moveTimer; // how long to walk float waitTimer; // how long to wait bool isMoving; } NPC; #define MAX_PARTICLES 15 typedef struct { Vector3 position; Vector3 velocity; float life; float size; } FireParticle; FireParticle particles[MAX_PARTICLES]; Vector3 firePos = {0.0f, -0.10f, 1.5f}; int selectedChar = -1; void InitParticles() { for (int i = 0; i < MAX_PARTICLES; i++) { particles[i].life = 0; } } void UpdateParticles(float dt) { for (int i = 0; i < MAX_PARTICLES; i++) { if (particles[i].life <= 0) { // Respawn at fire base particles[i].position = (Vector3){ firePos.x + ((float)GetRandomValue(-10,10)/100.0f), firePos.y + 0.1f, firePos.z + ((float)GetRandomValue(-10,10)/100.0f) }; particles[i].velocity = (Vector3){ ((float)GetRandomValue(-5,5)/100.0f), // slight sideways drift 0.6f + (float)GetRandomValue(0,40)/100.0f, ((float)GetRandomValue(-5,5)/100.0f) }; particles[i].life = 0.6f + (float)GetRandomValue(0,40)/100.0f; particles[i].size = 0.15f + (float)GetRandomValue(0,1)/100.0f; } // Movement particles[i].position.x += particles[i].velocity.x * dt; particles[i].position.y += particles[i].velocity.y * dt; particles[i].position.z += particles[i].velocity.z * dt; // subtle swirl (makes flame feel alive) particles[i].position.x += sinf(GetTime()*5 + i) * 0.002f; particles[i].position.z += cosf(GetTime()*5 + i) * 0.002f; // Fade particles[i].life -= dt; // Clamp height (keeps fire tight) if (particles[i].position.y > firePos.y + 0.5f) { particles[i].life = 0; } } } void DrawParticles() { for (int i = 0; i < MAX_PARTICLES; i++) { if (particles[i].life > 0) { float t = particles[i].life; // Color gradient (yellow → orange → red) Color color; if (t > 0.5f) { color = (Color){255, 200, 50, (unsigned char)(255 * t)}; } else if (t > 0.25f) { color = (Color){255, 120, 20, (unsigned char)(255 * t)}; } else { color = (Color){200, 40, 10, (unsigned char)(255 * t)}; } float size = particles[i].size * t; DrawCubeV(particles[i].position, (Vector3){size, size, size}, color); } } } int BuildInputMask() { int inputs = 0; if (IsKeyDown(KEY_W)) inputs |= INPUT_FORWARD; if (IsKeyDown(KEY_S)) inputs |= INPUT_BACK; if (IsKeyDown(KEY_A)) inputs |= INPUT_LEFT; if (IsKeyDown(KEY_D)) inputs |= INPUT_RIGHT; if (IsKeyDown(KEY_SPACE)) inputs |= INPUT_JUMP; if (IsKeyDown(KEY_LEFT_SHIFT)) inputs |= INPUT_SPRINT; return inputs; } void UpdateNPC(NPC *npc, Mesh terrainMesh, Matrix terrainTransform, float dt) { if (npc->isMoving) { // walk toward target Vector3 dir = Vector3Subtract(npc->target, npc->position); dir.y = 0; float dist = Vector3Length(dir); if (dist > 1.0f) { dir = Vector3Normalize(dir); npc->position.x += dir.x * 20.0f * dt; // npc walk speed npc->position.z += dir.z * 20.0f * dt; // snap to terrain height npc->position.y = GetTerrainHeight( npc->position.x, npc->position.z); // face direction of travel npc->rotation = atan2f(dir.x, dir.z) * RAD2DEG; } npc->moveTimer -= dt; if (npc->moveTimer <= 0.0f || dist <= 1.0f) { // stop walking, start waiting npc->isMoving = false; npc->waitTimer = (float)GetRandomValue(200, 500) / 100.0f; // wait 2-5 sec } } else { // waiting npc->waitTimer -= dt; if (npc->waitTimer <= 0.0f) { // pick a new random target near spawn float range = 100.0f; // how far they wander from spawn float tx = npc->spawnPosition.x + (float)GetRandomValue(-range, range); float tz = npc->spawnPosition.z + (float)GetRandomValue(-range, range); npc->target = (Vector3){tx, 0, tz}; npc->isMoving = true; npc->moveTimer = (float)GetRandomValue(300, 800) / 100.0f; // walk 3-8 sec } } } void UpdatePlayer(Player *p, Camera3D *cam, Mesh terrainMesh, Matrix terrainTransform, float dt) { Vector2 mouseDelta = GetMouseDelta(); p->yaw -= mouseDelta.x * 0.003f; p->pitch -= mouseDelta.y * 0.003f; p->pitch = Clamp(p->pitch, -1.4f, 1.4f); // prevent flipping // --- Movement direction (relative to yaw) --- Vector3 forward = { sinf(p->yaw), 0, cosf(p->yaw) }; Vector3 right = { cosf(p->yaw), 0, -sinf(p->yaw) }; // --- Jump --- if (IsKeyPressed(KEY_SPACE) && p->onGround) { p->velocity.y = 500.0f; p->onGround = false; } Vector3 moveDir = {0}; if (IsKeyDown(KEY_W)) moveDir = Vector3Add(moveDir, forward); if (IsKeyDown(KEY_S)) moveDir = Vector3Subtract(moveDir, forward); if (IsKeyDown(KEY_D)) moveDir = Vector3Subtract(moveDir, right); if (IsKeyDown(KEY_A)) moveDir = Vector3Add(moveDir, right); if (IsKeyDown(KEY_LEFT_SHIFT)) { if (p->onGround) { p->sprint = 5; } else { p->sprint = 10; } } else { p->sprint = 1; } // normalize so diagonals aren't faster if (Vector3Length(moveDir) > 0) moveDir = Vector3Normalize(moveDir); p->position.x += moveDir.x * p->speed * p-> sprint * dt; p->position.z += moveDir.z * p->speed * p-> sprint * dt; // --- Gravity --- if (!p->onGround) p->velocity.y -= 300.0f * dt; p->position.y += p->velocity.y * dt; // --- Terrain Collision (downward ray) --- Vector3 rayOrigin = { p->position.x, p->position.y + 2000.0f, p->position.z }; Ray downRay = { rayOrigin, (Vector3){0, -1, 0} }; RayCollision hit = GetRayCollisionMesh(downRay, terrainMesh, terrainTransform); if (hit.hit) { float groundY = hit.point.y; if (p->position.y <= groundY + 0.5f) { // snap to ground p->position.y = groundY; p->velocity.y = 0; p->onGround = true; } else { p->onGround = false; } } else { p->onGround = false; } // --- Attach Camera to Player Head --- Vector3 headPos = { p->position.x, p->position.y + p->height, p->position.z }; cam->position = headPos; // camera looks in yaw+pitch direction cam->target = (Vector3){ headPos.x + sinf(p->yaw) * cosf(p->pitch), headPos.y + sinf(p->pitch), headPos.z + cosf(p->yaw) * cosf(p->pitch) }; } Player player = {.position = (Vector3){600, 3000.0f, -1200}, .velocity = {0}, .yaw = 0.0f, .pitch = 0.0f, .height = 5.0f, .speed = 30.0f, .sprint = 1.0f, .onGround = false}; Mesh terrainMesh; Matrix terrainTransform; int scene = 0; int main() { SetConfigFlags(FLAG_VSYNC_HINT); InitWindow(0, 0, "Pants - MMO"); SetTargetFPS(0); SetRandomSeed(1234567890); Model terrain = LoadModel("assets/terrain.glb"); if (terrain.meshCount == 0) { TraceLog(LOG_ERROR, "Terrain failed to load!"); } // Bake scale into vertices so rendering and collision match float scaleXZ = 10.0f; float scaleY = 10.0f; for (int i = 0; i < terrain.meshes[0].vertexCount; i++) { terrain.meshes[0].vertices[i * 3 + 0] *= scaleXZ; terrain.meshes[0].vertices[i * 3 + 1] *= scaleY; terrain.meshes[0].vertices[i * 3 + 2] *= scaleXZ; } UpdateMeshBuffer(terrain.meshes[0], 0, terrain.meshes[0].vertices, terrain.meshes[0].vertexCount * 3 * sizeof(float), 0); terrain.transform = MatrixIdentity(); terrainMesh = terrain.meshes[0]; // first mesh in the model terrainTransform = terrain.transform; // world transform of the model InitRemotePlayers(); NetworkInit(); QueueInit(&incomingQueue); pthread_t recvThread, sendThread; int r1 = pthread_create(&recvThread, NULL, RecvThread, NULL); int r2 = pthread_create(&sendThread, NULL, SendThread, NULL); int fontSize = 25; char* message = "Welcome to Pants a game by Matthew & Devyn Challman"; int messageWidth = MeasureText(message, fontSize); int messagePosX = (GetScreenWidth() - messageWidth) / 2; int messagePosY = (GetScreenHeight() - fontSize) / 2 - 200; char* subMessage = "Select a character to connect"; int subMessageWidth = MeasureText(subMessage, fontSize); int subMessagePosX = (GetScreenWidth() - subMessageWidth) / 2; int subMessagePosY = (GetScreenHeight() - fontSize) / 2 - 150; int buttonW = 150; int buttonH = 50; int buttonPosX = (GetScreenWidth() - buttonW) / 2; int buttonPosY = (GetScreenHeight() - buttonH) / 2 + 100; char *connect = "Connect"; int connectWidth = MeasureText(connect, fontSize); int connectPosX = (GetScreenWidth() - connectWidth) / 2; int connectPosY = (GetScreenHeight() - fontSize) / 2 + 100; Camera3D camera = {0}; camera.up = (Vector3){0, 1, 0}; camera.fovy = 70.0f; camera.projection = CAMERA_PERSPECTIVE; Camera3D startCamera = {0}; startCamera.position = (Vector3){ 0.0f, 1.5f, -1.0f }; startCamera.target = (Vector3){ 0.0f, -0.5f, 10.0f }; // look toward your model startCamera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; startCamera.fovy = 70.0f; startCamera.projection = CAMERA_PERSPECTIVE; Model npcModels[47]; char *paths[47]; paths[0] = "assets/characters/Models/GLB format/character-a.glb"; paths[1] = "assets/characters/Models/GLB format/character-b.glb"; paths[2] = "assets/characters/Models/GLB format/character-c.glb"; paths[3] = "assets/characters/Models/GLB format/character-d.glb"; paths[4] = "assets/characters/Models/GLB format/character-e.glb"; paths[5] = "assets/characters/Models/GLB format/character-f.glb"; paths[6] = "assets/characters/Models/GLB format/character-g.glb"; paths[7] = "assets/characters/Models/GLB format/character-h.glb"; paths[8] = "assets/characters/Models/GLB format/character-i.glb"; paths[9] = "assets/characters/Models/GLB format/character-j.glb"; paths[10] = "assets/characters/Models/GLB format/character-k.glb"; paths[11] = "assets/characters/Models/GLB format/character-l.glb"; paths[12] = "assets/characters/Models/GLB format/character-m.glb"; paths[13] = "assets/characters/Models/GLB format/character-n.glb"; paths[14] = "assets/characters/Models/GLB format/character-o.glb"; paths[15] = "assets/characters/Models/GLB format/character-p.glb"; paths[16] = "assets/characters/Models/GLB format/character-q.glb"; paths[17] = "assets/characters/Models/GLB format/character-r.glb"; paths[18] = "assets/graveyard/Models/GLB format/character-ghost.glb"; paths[19] = "assets/graveyard/Models/GLB format/character-skeleton.glb"; paths[20] = "assets/graveyard/Models/GLB format/character-zombie.glb"; paths[21] = "assets/graveyard/Models/GLB format/character-keeper.glb"; paths[22] = "assets/graveyard/Models/GLB format/character-vampire.glb"; paths[23] = "assets/pets/Models/GLB format/animal-beaver.glb"; paths[24] = "assets/pets/Models/GLB format/animal-caterpillar.glb"; paths[25] = "assets/pets/Models/GLB format/animal-deer.glb"; paths[26] = "assets/pets/Models/GLB format/animal-fox.glb"; paths[27] = "assets/pets/Models/GLB format/animal-lion.glb"; paths[28] = "assets/pets/Models/GLB format/animal-penguin.glb"; paths[29] = "assets/pets/Models/GLB format/animal-bee.glb"; paths[30] = "assets/pets/Models/GLB format/animal-chick.glb"; paths[31] = "assets/pets/Models/GLB format/animal-dog.glb"; paths[32] = "assets/pets/Models/GLB format/animal-giraffe.glb"; paths[33] = "assets/pets/Models/GLB format/animal-monkey.glb"; paths[34] = "assets/pets/Models/GLB format/animal-pig.glb"; paths[35] = "assets/pets/Models/GLB format/animal-bunny.glb"; paths[36] = "assets/pets/Models/GLB format/animal-cow.glb"; paths[37] = "assets/pets/Models/GLB format/animal-elephant.glb"; paths[38] = "assets/pets/Models/GLB format/animal-hog.glb"; paths[39] = "assets/pets/Models/GLB format/animal-panda.glb"; paths[40] = "assets/pets/Models/GLB format/animal-polar.glb"; paths[41] = "assets/pets/Models/GLB format/animal-cat.glb"; paths[42] = "assets/pets/Models/GLB format/animal-crab.glb"; paths[43] = "assets/pets/Models/GLB format/animal-fish.glb"; paths[44] = "assets/pets/Models/GLB format/animal-koala.glb"; paths[45] = "assets/pets/Models/GLB format/animal-parrot.glb"; paths[46] = "assets/pets/Models/GLB format/animal-tiger.glb"; for (int i = 0; i < 47; ++i) { npcModels[i] = LoadModel(paths[i]); } Vector3 npcPositions[1000]; float npcRotations[1000]; for (int i = 0; i < 1000; ++i) { float npcX = (float)GetRandomValue(-500, 500); float npcZ = (float)GetRandomValue(-500, 500); float npcY = GetTerrainHeight(npcX, npcZ); npcPositions[i] = (Vector3){ npcX, npcY, npcZ }; npcRotations[i] = (float)GetRandomValue(0, 360); // random degrees } Vector3 petPositions[1000]; float petRotations[1000]; for (int i = 0; i < 1000; ++i) { float npcX = (float)GetRandomValue(-500, 500); float npcZ = (float)GetRandomValue(-500, 500); float npcY = GetTerrainHeight(npcX, npcZ); petPositions[i] = (Vector3){ npcX, npcY, npcZ }; petRotations[i] = (float)GetRandomValue(0, 360); // random degrees } Model treeModels[5]; char *treePaths[5]; treePaths[0] = "assets/world/Models/GLB format/tree-crooked.glb"; treePaths[1] = "assets/world/Models/GLB format/tree-high-crooked.glb"; treePaths[2] = "assets/world/Models/GLB format/tree-high-round.glb"; treePaths[3] = "assets/world/Models/GLB format/tree-high.glb"; treePaths[4] = "assets/world/Models/GLB format/tree.glb"; Vector3 treePositions[1000]; float treeRotations[1000]; float treeScales[1000]; for (int i = 0; i < 1000; ++i) { float npcX = (float)GetRandomValue(-500, 500); float npcZ = (float)GetRandomValue(-500, 500); float npcY = GetTerrainHeight(npcX, npcZ); treePositions[i] = (Vector3){ npcX, npcY - 1, npcZ }; treeRotations[i] = (float)GetRandomValue(0, 360); // random degrees treeScales[i] = (float)GetRandomValue(10, 400) / 10.0f; // 0.8 to 1.5 } for (int i = 0; i < 5; ++i) { treeModels[i] = LoadModel(treePaths[i]); } Model rockModels[3]; char *rockPaths[3]; rockPaths[0] = "assets/world/Models/GLB format/rock-large.glb"; rockPaths[1] = "assets/world/Models/GLB format/rock-small.glb"; rockPaths[2] = "assets/world/Models/GLB format/rock-wide.glb"; Vector3 rockPositions[100]; float rockRotations[100]; float rockScales[100]; for (int i = 0; i < 100; ++i) { float npcX = (float)GetRandomValue(-500, 500); float npcZ = (float)GetRandomValue(-500, 500); float npcY = GetTerrainHeight(npcX, npcZ); rockPositions[i] = (Vector3){ npcX, npcY - 5, npcZ }; rockRotations[i] = (float)GetRandomValue(0, 360); // random degrees rockScales[i] = (float)GetRandomValue(10, 200) / 10.0f; // 0.8 to 1.5 } for (int i = 0; i < 3; ++i) { rockModels[i] = LoadModel(rockPaths[i]); } Model houseModels[2]; char *housePaths[2]; housePaths[0] = "assets/house.glb"; housePaths[1] = "assets/house2.glb"; Vector3 housePositions[20]; float houseRotations[20]; float houseScales[20]; for (int i = 0; i < 20; ++i) { float npcX = (float)GetRandomValue(600, 1200); float npcZ = (float)GetRandomValue(-1300, -2000); float npcY = GetTerrainHeight(npcX, npcZ); housePositions[i] = (Vector3){ npcX, npcY - 1, npcZ }; houseRotations[i] = (float)GetRandomValue(0, 360); // random degrees houseScales[i] = (float)GetRandomValue(100, 200) / 10.0f; // 0.8 to 1.5 } for (int i = 0; i < 2; ++i) { houseModels[i] = LoadModel(housePaths[i]); } Model playerModel = LoadModel("assets/dungeon/Models/GLB format/character-human.glb"); int animCount; ModelAnimation *anims = LoadModelAnimations( "assets/arena/Models/GLB format/character-soldier.glb", &animCount); Model orcModel = LoadModel("assets/dungeon/Models/GLB format/character-orc-f.glb"); Model campFireModel = LoadModel("assets/survival/Models/GLB format/campfire-pit.glb"); Model soldierModel = LoadModel("assets/arena/Models/GLB format/character-soldier.glb"); if (animCount > 0) { printf("This GLB has %d animation(s)\n", animCount); } else { printf("No animations found\n"); } for (int i = 0; i < animCount; i++) { printf("Animation %d: %s, frames: %d\n", i, anims[i].name, anims[i].frameCount); } // Let's assume idle is animation index 1 int orcIdleAnim = 7; int playerIdleAnim = 7; int soldierIdleAnim = 7; float orcFrame = 0.0f; float playerFrame = 0.0f; float soldierFrame = 0.0f; while (!WindowShouldClose()) { if (scene == 0) { float dt = GetFrameTime(); float animSpeed = 24.0f; // frames per second of the idle animation orcFrame += animSpeed * dt; playerFrame += animSpeed * dt; soldierFrame += animSpeed * dt; Ray ray = GetMouseRay(GetMousePosition(), startCamera); BoundingBox playerBox = GetModelBoundingBox(playerModel); playerBox.min = Vector3Add(playerBox.min, (Vector3){0.0, 0, 3}); playerBox.max = Vector3Add(playerBox.max, (Vector3){0.0, 0, 3}); RayCollision hitPlayer = GetRayCollisionBox(ray, playerBox); BoundingBox soldierBox = GetModelBoundingBox(soldierModel); soldierBox.min = Vector3Add(soldierBox.min, (Vector3){-1.25, 0, 2.5}); soldierBox.max = Vector3Add(soldierBox.max, (Vector3){-1.25, 0, 2.5}); RayCollision hitSoldier = GetRayCollisionBox(ray, soldierBox); BoundingBox orcBox = GetModelBoundingBox(orcModel); orcBox.min = Vector3Add(orcBox.min, (Vector3){1.25, 0, 2.5}); orcBox.max = Vector3Add(orcBox.max, (Vector3){1.25, 0, 2.5}); RayCollision hitOrc = GetRayCollisionBox(ray, orcBox); int hitIndex = -1; if (hitOrc.hit) hitIndex = 0; if (hitPlayer.hit) hitIndex = 1; if (hitSoldier.hit) hitIndex = 2; if (hitOrc.hit || hitPlayer.hit || hitSoldier.hit) { SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); } else { SetMouseCursor(MOUSE_CURSOR_DEFAULT); } if (hitIndex > -1 && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { pthread_mutex_lock(&packetLock); latestPacket.inputs = BuildInputMask(); latestPacket.yaw = player.yaw; latestPacket.type = PACKET_HANDSHAKE; latestPacket.playerId = player.id; latestPacket.characterClass = hitIndex; pthread_mutex_unlock(&packetLock); if (hitIndex == 0) player.height = 7.0f; } if (hitOrc.hit) { orcIdleAnim = 19; } else { orcIdleAnim = 7; } if (hitPlayer.hit) { playerIdleAnim = 19; } else { playerIdleAnim = 7; } if (hitSoldier.hit) { soldierIdleAnim = 19; } else { soldierIdleAnim = 7; } // Loop the animation if (orcFrame >= anims[orcIdleAnim].frameCount) orcFrame = 0.0f; if (playerFrame >= anims[playerIdleAnim].frameCount) playerFrame = 0.0f; if (soldierFrame >= anims[soldierIdleAnim].frameCount) soldierFrame = 0.0f; UpdateModelAnimation(playerModel, anims[playerIdleAnim], playerFrame); UpdateModelAnimation(orcModel, anims[orcIdleAnim], orcFrame); UpdateModelAnimation(soldierModel, anims[soldierIdleAnim], soldierFrame); UpdateParticles(dt); BeginDrawing(); ClearBackground(RAYWHITE); BeginMode3D(startCamera); DrawGrid(20, 1.0f); DrawModelEx(orcModel, (Vector3){1.25, 0, 2.5}, (Vector3){0, 1, 0}, 200, (Vector3){1.5, 1.5, 1.5}, WHITE); if (hitOrc.hit) { BeginBlendMode(BLEND_ADDITIVE); DrawCylinder((Vector3){1.25f, 0.0f, 2.5f}, 0.6f, 0.6f, 4.0f, 32, (Color){255, 120, 30, 30}); DrawCylinder((Vector3){1.25f, 0.0f, 2.5f}, 0.4f, 0.4f, 4.0f, 32, (Color){255, 160, 60, 80}); EndBlendMode(); } DrawModelEx(playerModel, (Vector3){0.0, 0, 3}, (Vector3){0, 1, 0}, 180, (Vector3){1, 1, 1}, WHITE); if (hitPlayer.hit) { BeginBlendMode(BLEND_ADDITIVE); DrawCylinder((Vector3){0.0f, 0.0f, 3.0f}, 0.5f, 0.5f, 4.0f, 32, (Color){255, 120, 30, 30}); DrawCylinder((Vector3){0.0f, 0.0f, 3.0f}, 0.3f, 0.3f, 4.0f, 32, (Color){255, 160, 60, 80}); EndBlendMode(); } DrawModelEx(soldierModel, (Vector3){-1.15, 0, 2.5}, (Vector3){0, 1, 0}, 160, (Vector3){1, 1, 1}, WHITE); if (hitSoldier.hit) { BeginBlendMode(BLEND_ADDITIVE); DrawCylinder((Vector3){-1.15f, 0.0f, 2.5f}, 0.5f, 0.5f, 4.0f, 32, (Color){255, 120, 30, 30}); DrawCylinder((Vector3){-1.15f, 0.0f, 2.5f}, 0.3f, 0.3f, 4.0f, 32, (Color){255, 160, 60, 80}); EndBlendMode(); } DrawModelEx(campFireModel, (Vector3){0.0, -0.10, 1.5}, (Vector3){0, 1, 0}, 180, (Vector3){1.5, 1.5, 1.5}, WHITE); DrawParticles(); EndMode3D(); DrawText(message, messagePosX, messagePosY, fontSize, BLACK); DrawText(subMessage, subMessagePosX, subMessagePosY, fontSize, BLACK); EndDrawing(); ProcessIncoming(); } else { float dt = GetFrameTime(); static double lastPingTime = 0; fpsSamples[fpsSampleIndex % FPS_SAMPLES] = 1.0 / dt; fpsSampleIndex++; double total = 0; int count = fpsSampleIndex < FPS_SAMPLES ? fpsSampleIndex : FPS_SAMPLES; for (int i = 0; i < count; i++) total += fpsSamples[i]; avgFps = total / count; if (GetWallTime() - lastPingTime > 1.0) { SendPing(); lastPingTime = GetWallTime(); } pthread_mutex_lock(&packetLock); latestPacket.inputs = BuildInputMask(); latestPacket.yaw = player.yaw; latestPacket.type = PACKET_CLIENT_INPUT; latestPacket.playerId = player.id; pthread_mutex_unlock(&packetLock); UpdatePlayer(&player, &camera, terrainMesh, terrainTransform, dt); for (int i = 0; i < MAX_PLAYERS; i++) { if (!remotePlayers[i].active) continue; if (i == player.id) continue; // skip yourself UpdateRemotePlayer(&remotePlayers[i], dt); } bool connecting = false; if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { struct Vector2 mousePos = GetMousePosition(); if (mousePos.x >= buttonPosX && mousePos.x <= buttonPosX + buttonW && mousePos.y >= buttonPosY && mousePos.y <= buttonPosY + buttonH) { connecting = true; } } BeginDrawing(); ClearBackground(RAYWHITE); BeginMode3D(camera); DrawModel(terrain, (Vector3){0, 0, 0}, 1.0f, WHITE); DrawGrid(20, 1.0f); for (int i = 0; i < MAX_PLAYERS; i++) { if (!remotePlayers[i].active) continue; if (i == player.id) continue; float dist = Vector3Distance(player.position, remotePlayers[i].currentPosition); Vector3 scale = {10.0f, 10.0f, 10.0f}; if (remotePlayers[i].characterClass == 0) { scale.x = 15.0f; scale.y = 15.0f; scale.z = 15.0; } // beam disappears when close if (dist > 50.0f) { Vector3 pos = remotePlayers[i].currentPosition; Vector3 beamBottom = { pos.x, pos.y, pos.z }; Vector3 beamTop = { pos.x, pos.y + 2000, pos.z }; DrawCylinderEx((Vector3){ pos.x, pos.y+13, pos.z }, beamTop, 4.0f, 4.0f, 8, (Color){255, 255, 255, 220}); DrawCylinderEx(beamBottom, beamTop, 8.0f, 8.0f, 8, (Color){255, 255, 0, 80}); } DrawModelEx(remotePlayers[i].model, remotePlayers[i].currentPosition, (Vector3){0, 1, 0}, remotePlayers[i].currentYaw * RAD2DEG, scale, WHITE); } for (int i = 0; i < MAX_VIEW_NPCS; ++i) { DrawModelEx( npcModels[clientNPCs[i].model], clientNPCs[i].serverPosition, (Vector3){0, 1, 0}, // rotate around Y axis (vertical) clientNPCs[i].serverRotation, // random angle in degrees clientNPCs[i].scale, WHITE ); } for (int i = 0; i < 1000; ++i) { DrawModelEx( treeModels[i % 5], treePositions[i], (Vector3){0, 1, 0}, // rotate around Y axis (vertical) treeRotations[i], // random angle in degrees (Vector3){treeScales[i], treeScales[i], treeScales[i]}, // scale WHITE ); } for (int i = 0; i < 100; ++i) { DrawModelEx( rockModels[i % 3], rockPositions[i], (Vector3){0, 1, 0}, // rotate around Y axis (vertical) rockRotations[i], // random angle in degrees (Vector3){rockScales[i], rockScales[i], rockScales[i]}, // scale WHITE ); } for (int i = 0; i < 20; ++i) { DrawModelEx( houseModels[i % 2], housePositions[i], (Vector3){0, 1, 0}, // rotate around Y axis (vertical) houseRotations[i], // random angle in degrees (Vector3){houseScales[i], houseScales[i], houseScales[i]}, // scale WHITE ); } EndMode3D(); // fixed x positions - never move regardless of content int y = 10; int padY = 5; int totalW = 700; int totalH = fontSize + padY * 2; // background DrawRectangle(0, y - padY, totalW, totalH, (Color){0, 0, 0, 140}); // fps - always at same x char fpsBuf[32]; snprintf(fpsBuf, sizeof(fpsBuf), "FPS: %3.0f", avgFps); Color fpsColor = avgFps > 50 ? GREEN : avgFps > 30 ? YELLOW : RED; DrawText(fpsBuf, HUD_FPS_X, y, fontSize, fpsColor); // ping - always at same x char pingBuf[32]; snprintf(pingBuf, sizeof(pingBuf), "PING: %5.1f ms", pingMs); Color pingColor = pingMs < 50 ? GREEN : pingMs < 100 ? YELLOW : RED; DrawText(pingBuf, HUD_PING_X, y, fontSize, pingColor); // coords - always at same x char coordsBuf[64]; snprintf(coordsBuf, sizeof(coordsBuf), "X:%8.1f Y:%8.1f Z:%8.1f", player.position.x, player.position.y, player.position.z); DrawText(coordsBuf, HUD_COORDS_X, y, fontSize, WHITE); EndDrawing(); ProcessIncoming(); } } NetworkShutdown(); CloseWindow(); return 0; }