diff options
Diffstat (limited to 'client/main.c')
| -rw-r--r-- | client/main.c | 843 |
1 files changed, 812 insertions, 31 deletions
diff --git a/client/main.c b/client/main.c index c0ed27e..dd079b0 100644 --- a/client/main.c +++ b/client/main.c @@ -1,63 +1,844 @@ #include "raylib.h" +#include "raymath.h" +#include "rlgl.h" +#include <math.h> #include <stdio.h> +#include <time.h> +#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() { - const int screenWidth = 800; - const int screenHeight = 450; + SetConfigFlags(FLAG_VSYNC_HINT); + InitWindow(0, 0, "Pants - MMO"); + SetTargetFPS(0); + SetRandomSeed(1234567890); + Model terrain = LoadModel("assets/terrain.glb"); - InitWindow(screenWidth, screenHeight, "Pants - MMO"); + if (terrain.meshCount == 0) { + TraceLog(LOG_ERROR, "Terrain failed to load!"); + } - SetTargetFPS(60); +// 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); - char* message = "Welcome to Pants a game by Matthew & Devyn Challman"; - int fontSize = 25; + 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 = (screenWidth - messageWidth) / 2; - int messagePosY = (screenHeight - fontSize) / 2; + 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 = (screenWidth - buttonW) / 2; - int buttonPosY = (screenHeight - buttonH) / 2 + 100; + int buttonPosX = (GetScreenWidth() - buttonW) / 2; + int buttonPosY = (GetScreenHeight() - buttonH) / 2 + 100; char *connect = "Connect"; int connectWidth = MeasureText(connect, fontSize); - int connectPosX = (screenWidth - connectWidth) / 2; - int connectPosY = (screenHeight - fontSize) / 2 + 100; + 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}); - bool connecting = false; - if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - struct Vector2 mousePos = GetMousePosition(); + RayCollision hitSoldier = GetRayCollisionBox(ray, soldierBox); - if (mousePos.x >= buttonPosX && mousePos.x <= buttonPosX + buttonW && - mousePos.y >= buttonPosY && mousePos.y <= buttonPosY + buttonH) { + 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}); - connecting = true; + 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); } - } - BeginDrawing(); - ClearBackground(RAYWHITE); - DrawText(message, messagePosX, messagePosY, fontSize, LIGHTGRAY); + 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++; - if(connecting) { - DrawRectangle(buttonPosX, buttonPosY, buttonW, buttonH, RED); - } else { - DrawRectangle(buttonPosX, buttonPosY, buttonW, buttonH, GREEN); + 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); - DrawText(connect, connectPosX, connectPosY, fontSize, BLUE); + DrawGrid(20, 1.0f); - DrawFPS(0,0); - EndDrawing(); + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!remotePlayers[i].active) continue; + if (i == player.id) continue; - if (connecting) - ServerConnect(); + 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; } |