// game/client.c - SMOOTH TERRAIN VERSION WITH DIRECTIONAL LIGHT & SHADOWS #include "raylib.h" #include #include #include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include #pragma comment(lib, "ws2_32.lib") typedef int socklen_t; #define CLOSESOCK closesocket #else #include #include #include #include #include #define CLOSESOCK close #endif #define PROTOCOL_VERSION 67 #define SERVER_PORT 27015 #define MAX_PLAYERS 16 #define USERNAME_MAX 16 #define WEAPON_PISTOL 0 #define WEAPON_RIFLE 1 #define ITEM_NONE 0 #define ITEM_MEDKIT 1 #define ITEM_AMMO_PISTOL 2 #define ITEM_AMMO_RIFLE 3 #define MAX_ITEMS 64 #define BTN_RELOAD (1u << 0) #define BTN_SWITCH_PISTOL (1u << 1) #define BTN_SWITCH_RIFLE (1u << 2) #define BTN_PICK (1u << 3) #define BTN_USE_MEDKIT (1u << 4) #define BTN_JUMP (1u << 5) // ============== DIRECTIONAL LIGHT ============== typedef struct { Vector3 direction; // Normalized light direction Vector3 color; // RGB color (0-1 range) float intensity; // Light intensity multiplier float ambientIntensity; // Ambient light strength float shadowBias; // Shadow bias to prevent z-fighting float shadowIntensity; // How dark shadows are (0-1) } DirectionalLight; // ============== TERRAIN SHADER ============== typedef struct { Shader shader; int locViewPos; int locLightDir; int locLightColor; int locLightIntensity; int locAmbientIntensity; int locTerrainColor; } TerrainShader; static TerrainShader load_terrain_shader(void) { TerrainShader ts = {0}; ts.shader = LoadShader("terrain.vs", "terrain.fs"); // Get uniform locations ts.locViewPos = GetShaderLocation(ts.shader, "viewPos"); ts.locLightDir = GetShaderLocation(ts.shader, "lightDir"); ts.locLightColor = GetShaderLocation(ts.shader, "lightColor"); ts.locLightIntensity = GetShaderLocation(ts.shader, "lightIntensity"); ts.locAmbientIntensity = GetShaderLocation(ts.shader, "ambientIntensity"); ts.locTerrainColor = GetShaderLocation(ts.shader, "terrainColor"); return ts; } static void unload_terrain_shader(TerrainShader *ts) { if (ts && ts->shader.id != 0) { UnloadShader(ts->shader); ts->shader.id = 0; } } // ============== END TERRAIN SHADER ============== static const uint8_t perm[512] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180}; static float grad2(int hash, float x, float y) { int h = hash & 7; float u = h < 4 ? x : y; float v = h < 4 ? y : x; return ((h & 1) ? -u : u) + ((h & 2) ? -2.0f * v : 2.0f * v); } static float simplex_noise_2d(float x, float y) { const float F2 = 0.366025403f; const float G2 = 0.211324865f; float s = (x + y) * F2; int i = (int)floorf(x + s); int j = (int)floorf(y + s); float t = (i + j) * G2; float X0 = i - t, Y0 = j - t; float x0 = x - X0, y0 = y - Y0; int i1 = (x0 > y0) ? 1 : 0; int j1 = (x0 > y0) ? 0 : 1; float x1 = x0 - i1 + G2, y1 = y0 - j1 + G2; float x2 = x0 - 1.0f + 2.0f * G2, y2 = y0 - 1.0f + 2.0f * G2; int ii = i & 255, jj = j & 255; float n0 = 0.0f, n1 = 0.0f, n2 = 0.0f; float t0 = 0.5f - x0 * x0 - y0 * y0; if (t0 >= 0.0f) { t0 *= t0; n0 = t0 * t0 * grad2(perm[ii + perm[jj]], x0, y0); } float t1 = 0.5f - x1 * x1 - y1 * y1; if (t1 >= 0.0f) { t1 *= t1; n1 = t1 * t1 * grad2(perm[ii + i1 + perm[jj + j1]], x1, y1); } float t2 = 0.5f - x2 * x2 - y2 * y2; if (t2 >= 0.0f) { t2 *= t2; n2 = t2 * t2 * grad2(perm[ii + 1 + perm[jj + 1]], x2, y2); } return 45.0f * (n0 + n1 + n2); } static float fbm_noise(float x, float y, int octaves) { float value = 0.0f, amplitude = 1.0f, frequency = 1.0f, max_value = 0.0f; for (int i = 0; i < octaves; i++) { value += simplex_noise_2d(x * frequency, y * frequency) * amplitude; max_value += amplitude; amplitude *= 0.5f; frequency *= 2.0f; } return value / max_value; } float get_terrain_height(float x, float z) { float height = fbm_noise(x * 0.03f, z * 0.03f, 4); height += fbm_noise(x * 0.01f, z * 0.01f, 2) * 1.5f; return height * 6.0f + 2.0f; } #define TERRAIN_SIZE 256 #define TERRAIN_SCALE 1.0f static Mesh generate_terrain_mesh(void) { int size = TERRAIN_SIZE; Mesh mesh = {0}; int vertexCount = size * size; int triangleCount = (size - 1) * (size - 1) * 2; mesh.vertexCount = vertexCount; mesh.triangleCount = triangleCount; mesh.vertices = (float *)MemAlloc(vertexCount * 3 * sizeof(float)); mesh.texcoords = (float *)MemAlloc(vertexCount * 2 * sizeof(float)); mesh.normals = (float *)MemAlloc(vertexCount * 3 * sizeof(float)); mesh.indices = (unsigned short *)MemAlloc(triangleCount * 3 * sizeof(unsigned short)); // Generate vertices for (int z = 0; z < size; z++) { for (int x = 0; x < size; x++) { int idx = z * size + x; float wx = ((float)x - size / 2.0f) * TERRAIN_SCALE; float wz = ((float)z - size / 2.0f) * TERRAIN_SCALE; float wy = get_terrain_height(wx, wz); mesh.vertices[idx * 3 + 0] = wx; mesh.vertices[idx * 3 + 1] = wy; mesh.vertices[idx * 3 + 2] = wz; mesh.texcoords[idx * 2 + 0] = (float)x / (float)(size - 1); mesh.texcoords[idx * 2 + 1] = (float)z / (float)(size - 1); } } // Generate indices int triIdx = 0; for (int z = 0; z < size - 1; z++) { for (int x = 0; x < size - 1; x++) { int i0 = z * size + x; int i1 = z * size + (x + 1); int i2 = (z + 1) * size + x; int i3 = (z + 1) * size + (x + 1); mesh.indices[triIdx * 3 + 0] = i0; mesh.indices[triIdx * 3 + 1] = i2; mesh.indices[triIdx * 3 + 2] = i1; triIdx++; mesh.indices[triIdx * 3 + 0] = i1; mesh.indices[triIdx * 3 + 1] = i2; mesh.indices[triIdx * 3 + 2] = i3; triIdx++; } } // Calculate normals using tangent plane approximation from terrain gradients // This preserves terrain curvature better than simple triangle averaging for (int z = 0; z < size; z++) { for (int x = 0; x < size; x++) { int idx = z * size + x; float wx = ((float)x - size / 2.0f) * TERRAIN_SCALE; float wz = ((float)z - size / 2.0f) * TERRAIN_SCALE; // Sample height gradients to compute terrain normal // Use neighboring vertices for finite difference approximation float h_right = (x + 1 < size) ? mesh.vertices[(z * size + (x + 1)) * 3 + 1] : mesh.vertices[idx * 3 + 1]; float h_left = (x - 1 >= 0) ? mesh.vertices[(z * size + (x - 1)) * 3 + 1] : mesh.vertices[idx * 3 + 1]; float h_down = (z + 1 < size) ? mesh.vertices[((z + 1) * size + x) * 3 + 1] : mesh.vertices[idx * 3 + 1]; float h_up = (z - 1 >= 0) ? mesh.vertices[((z - 1) * size + x) * 3 + 1] : mesh.vertices[idx * 3 + 1]; // Compute finite differences float dh_dx = (h_right - h_left) / (2.0f * TERRAIN_SCALE); float dh_dz = (h_down - h_up) / (2.0f * TERRAIN_SCALE); // Normal from height field: (-dh/dx, 1, -dh/dz) then normalized float nx = -dh_dx; float ny = 1.0f; float nz = -dh_dz; float len = sqrtf(nx * nx + ny * ny + nz * nz); if (len > 0.0001f) { mesh.normals[idx * 3 + 0] = nx / len; mesh.normals[idx * 3 + 1] = ny / len; mesh.normals[idx * 3 + 2] = nz / len; } else { mesh.normals[idx * 3 + 0] = 0; mesh.normals[idx * 3 + 1] = 1; mesh.normals[idx * 3 + 2] = 0; } } } UploadMesh(&mesh, false); return mesh; } // ============== END SIMPLEX NOISE ============== // ============== LIGHTING HELPERS ============== // apply_directional_lighting: Basic Lambertian diffuse lighting // Applies directional light with diffuse component based on surface normal static Color apply_directional_lighting(Color baseColor, Vector3 normal, DirectionalLight light) { // Normalize light direction (it's already normalized, but for safety) Vector3 lightDir = light.direction; // Calculate diffuse component: dot product of negative light direction and // surface normal We use negative because light travels opposite to its // direction vector float diff = fmaxf(0.0f, -(lightDir.x * normal.x + lightDir.y * normal.y + lightDir.z * normal.z)); // Combine diffuse with ambient light // Ambient provides minimum brightness even in shadow float brightness = light.ambientIntensity + (diff * light.intensity * (1.0f - light.ambientIntensity)); // Apply brightness multiplier to base color int r = (int)(baseColor.r * brightness); int g = (int)(baseColor.g * brightness); int b = (int)(baseColor.b * brightness); // Clamp RGB to valid range [0, 255] if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; return (Color){r, g, b, baseColor.a}; } // calculate_shadow_factor: Distance-based shadow softening // Objects at higher elevations or farther from ground get softer, less // pronounced shadows This simulates how shadows fade with distance and // atmospheric scattering static float calculate_shadow_factor(Vector3 worldPos, Vector3 lightDir, float maxShadowDistance) { // Calculate height above ground (approximate) float distFromLight = fmaxf(0.0f, worldPos.y - 2.0f); // Fade out shadow strength with distance (max 15% darkening) float shadowIntensity = fmaxf(0.0f, 1.0f - (distFromLight / maxShadowDistance)); return 1.0f - (shadowIntensity * 0.15f); } // calculate_temporal_shadow: Time-based shadow variance for anti-aliasing // Simulates subtle shadow movement to avoid banding artifacts static float calculate_temporal_shadow(Vector3 worldPos, float timePhase) { // Add subtle time-based variation to shadow boundaries float noiseVal = sinf(worldPos.x * 0.5f + timePhase) * cosf(worldPos.z * 0.5f + timePhase); return 1.0f + (noiseVal * 0.02f); // Very subtle variation } // apply_lighting_with_shadows: Full lighting calculation with shadows // Combines directional light, ambient light, shadows, and temporal variation static Color apply_lighting_with_shadows(Color baseColor, Vector3 normal, Vector3 worldPos, DirectionalLight light) { // Calculate base shadow factor (distance-based) float shadowFactor = calculate_shadow_factor(worldPos, light.direction, 10.0f); // Apply shadow to intensity float adjustedIntensity = light.intensity * shadowFactor; // Calculate diffuse component with adjusted intensity Vector3 lightDir = light.direction; float diff = fmaxf(0.0f, -(lightDir.x * normal.x + lightDir.y * normal.y + lightDir.z * normal.z)); // Combine with ambient using adjusted intensity float brightness = light.ambientIntensity + (diff * adjustedIntensity * (1.0f - light.ambientIntensity)); // Apply brightness to base color int r = (int)(baseColor.r * brightness); int g = (int)(baseColor.g * brightness); int b = (int)(baseColor.b * brightness); // Clamp values to valid RGB range if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; return (Color){r, g, b, baseColor.a}; } static Vector3 v3_normalize(Vector3 v) { float len = sqrtf(v.x * v.x + v.y * v.y + v.z * v.z); if (len < 0.0001f) return (Vector3){0, 1, 0}; return (Vector3){v.x / len, v.y / len, v.z / len}; } // ============== VECTOR3 HELPERS ============== static Vector3 v3(float x, float y, float z) { Vector3 v = {x, y, z}; return v; } static Vector3 v3_add(Vector3 a, Vector3 b) { return v3(a.x + b.x, a.y + b.y, a.z + b.z); } static Vector3 v3_sub(Vector3 a, Vector3 b) { return v3(a.x - b.x, a.y - b.y, a.z - b.z); } static float v3_dot(Vector3 a, Vector3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } static float v3_len(Vector3 a) { return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z); } static Vector3 v3_norm(Vector3 a) { float l = v3_len(a); if (l <= 1e-6f) return v3(0, 0, 1); return v3(a.x / l, a.y / l, a.z / l); } // ============== END VECTOR3 HELPERS ============== // ============== ENHANCED LIGHTING & AMBIENT OCCLUSION ============== // calculate_ambient_occlusion: Estimates how occluded a point is based on // terrain curvature Samples heights in cardinal directions and computes horizon // angle to terrain Returns occlusion factor from 0 (fully occluded) to 1 (fully // lit) static float calculate_ambient_occlusion(Vector3 worldPos, Vector3 normal) { // AO based on terrain curvature: check if terrain rises around the point float sampleRadius = 3.0f; float aoAccum = 0.0f; int numSamples = 8; float centerHeight = worldPos.y; // Sample 8 directions around the point for (int i = 0; i < numSamples; i++) { float angle = (2.0f * 3.14159265f * (float)i) / (float)numSamples; float sx = worldPos.x + cosf(angle) * sampleRadius; float sz = worldPos.z + sinf(angle) * sampleRadius; float sh = get_terrain_height(sx, sz); // Check if terrain is higher relative to surface normal // Higher terrain in shadow-casting areas reduces occlusion float heightDiff = sh - centerHeight; if (heightDiff > 0.1f) { // Terrain is higher, contributes to shadow float aoAmount = fminf(1.0f, heightDiff / 2.0f); aoAccum += aoAmount; } } float aoFactor = 1.0f - (aoAccum / (float)numSamples) * 0.6f; // 60% max occlusion return fmaxf(0.2f, aoFactor); // Min 20% brightness } // apply_phong_lighting_per_pixel: Advanced Phong lighting with per-pixel // normals Includes diffuse, specular highlight, and ambient occlusion for // geometry detail static Color apply_phong_lighting_per_pixel(Color baseColor, Vector3 normal, Vector3 worldPos, Vector3 camPos, DirectionalLight light) { // Normalize inputs Vector3 lightDir = v3_normalize(light.direction); Vector3 normal_norm = v3_normalize(normal); // Compute view direction (from surface to camera) Vector3 viewDir = v3_normalize(v3_sub(camPos, worldPos)); // Diffuse component: Lambertian shading float diffuse = fmaxf(0.0f, -v3_dot(lightDir, normal_norm)); // Specular component: Blinn-Phong specular highlight Vector3 halfVec = v3_normalize(v3_add( v3_norm((Vector3){-lightDir.x, -lightDir.y, -lightDir.z}), viewDir)); float specular = powf(fmaxf(0.0f, v3_dot(halfVec, normal_norm)), 32.0f) * 0.5f; // Ambient occlusion from terrain geometry float ao = calculate_ambient_occlusion(worldPos, normal_norm); // Shadow based on height (distant higher terrain casts softer shadows) float shadowFactor = calculate_shadow_factor(worldPos, light.direction, 10.0f); // Combine lighting components float brightness = light.ambientIntensity * ao; brightness += diffuse * light.intensity * shadowFactor * (1.0f - light.ambientIntensity) * ao; brightness += specular * light.intensity * shadowFactor * 0.6f; // Specular less affected by AO brightness = fminf(1.0f, brightness); // Apply brightness to base color int r = (int)(baseColor.r * brightness); int g = (int)(baseColor.g * brightness); int b = (int)(baseColor.b * brightness); // Clamp to valid RGB range if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; return (Color){r, g, b, baseColor.a}; } // ============== END ENHANCED LIGHTING & AMBIENT OCCLUSION ============== static void set_nonblocking(int sock) { #ifdef _WIN32 u_long mode = 1; ioctlsocket(sock, FIONBIO, &mode); #else int flags = fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, flags | O_NONBLOCK); #endif } #pragma pack(push, 1) typedef enum MsgType : uint8_t { MSG_HELLO = 1, MSG_WELCOME = 2, MSG_INPUT = 3, MSG_SNAPSHOT = 4, MSG_SHOOT = 5, MSG_ITEMS = 6 } MsgType; typedef struct MsgHello { uint8_t type; uint32_t protocol; char username[USERNAME_MAX]; } MsgHello; typedef struct MsgWelcome { uint8_t type; uint8_t playerId; uint32_t serverTick; } MsgWelcome; typedef struct MsgInput { uint8_t type; uint8_t playerId; uint32_t clientTick; float moveX, moveZ, yaw, pitch; uint8_t buttons; } MsgInput; typedef struct MsgShoot { uint8_t type; uint8_t playerId; uint32_t clientTick; } MsgShoot; typedef struct PlayerStateNet { uint8_t id, alive; int16_t hp; float x, y, z, yaw, pitch; uint8_t weapon; int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; int16_t reloadTimeLeft; char username[USERNAME_MAX]; } PlayerStateNet; typedef struct MsgSnapshot { uint8_t type; uint32_t serverTick; uint8_t count; PlayerStateNet p[MAX_PLAYERS]; } MsgSnapshot; typedef struct ItemNet { uint16_t id; uint8_t type; int16_t qty; float x, y, z; } ItemNet; typedef struct MsgItems { uint8_t type; uint32_t serverTick; uint8_t count; ItemNet items[MAX_ITEMS]; } MsgItems; #pragma pack(pop) typedef struct RemotePlayer { int present, alive, hp; Vector3 pos; float yaw, pitch; uint8_t weapon; int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; int16_t reloadTimeLeft; char username[USERNAME_MAX]; } RemotePlayer; typedef struct WorldItem { int present; uint16_t id; uint8_t type; int16_t qty; Vector3 pos; } WorldItem; static const char *ItemName(uint8_t t) { switch (t) { case ITEM_MEDKIT: return "Medkit"; case ITEM_AMMO_PISTOL: return "Pistol ammo"; case ITEM_AMMO_RIFLE: return "Rifle ammo"; default: return "-"; } } static void ui_username_prompt(char outName[USERNAME_MAX]) { memset(outName, 0, USERNAME_MAX); while (!WindowShouldClose()) { int ch = GetCharPressed(); while (ch > 0) { int len = (int)strlen(outName); if (ch >= 32 && ch <= 126) { if (len < USERNAME_MAX - 1) { outName[len] = (char)ch; outName[len + 1] = '\0'; } } ch = GetCharPressed(); } if (IsKeyPressed(KEY_BACKSPACE)) { int len = (int)strlen(outName); if (len > 0) outName[len - 1] = '\0'; } if (IsKeyPressed(KEY_ENTER) && strlen(outName) > 0) return; BeginDrawing(); ClearBackground((Color){20, 24, 32, 255}); DrawText("Enter username (press ENTER):", 60, 80, 28, RAYWHITE); DrawRectangle(60, 130, 420, 48, (Color){40, 48, 64, 255}); DrawRectangleLines(60, 130, 420, 48, (Color){120, 140, 170, 255}); DrawText(outName[0] ? outName : "_", 72, 142, 24, (Color){230, 230, 240, 255}); EndDrawing(); } } int main(void) { #ifdef _WIN32 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); #endif const int sw = 1280, sh = 720; InitWindow(sw, sh, "Voxel Shooter - Client (SMOOTH TERRAIN WITH SHADOWS)"); SetTargetFPS(120); char myName[USERNAME_MAX]; ui_username_prompt(myName); DisableCursor(); // Generate terrain mesh Mesh terrainMesh = generate_terrain_mesh(); Model terrainModel = LoadModelFromMesh(terrainMesh); terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color = (Color){80, 140, 70, 255}; // Setup directional light DirectionalLight dirLight = {0}; dirLight.direction = v3_normalize(v3(-0.8f, -1.0f, -0.6f)); // Coming from upper-left-back dirLight.color = v3(1.0f, 1.0f, 1.0f); dirLight.intensity = 1.2f; dirLight.ambientIntensity = 0.3f; dirLight.shadowBias = 0.005f; dirLight.shadowIntensity = 0.4f; // Load terrain shader TerrainShader terrainShader = load_terrain_shader(); if (terrainShader.shader.id == 0) { fprintf( stderr, "Warning: Failed to load terrain shader, using default rendering\n"); // Fallback: Make terrain very bright red to show shader failed terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color = (Color){255, 100, 100, 255}; } else { terrainModel.materials[0].shader = terrainShader.shader; fprintf(stderr, "Shader loaded successfully!\n"); } int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock < 0) { perror("socket"); return 1; } set_nonblocking(sock); struct sockaddr_in srv = {0}; srv.sin_family = AF_INET; srv.sin_port = htons(SERVER_PORT); inet_pton(AF_INET, "127.0.0.1", &srv.sin_addr); uint8_t myId = 255; uint32_t clientTick = 0; RemotePlayer rp[MAX_PLAYERS] = {0}; WorldItem wi[MAX_ITEMS] = {0}; Vector3 camPos = v3(0, 5.0f, 6); float yaw = 0.0f, pitch = 0.0f; float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f; float fireCooldown = 0.0f; int shotsInBurst = 0; float burstResetTimer = 0.0f; const float pistolFireRate = 4.0f, rifleFireRate = 12.0f; const float recoilReturn = 18.0f, crossReturn = 14.0f; const float pistolKickPitch = 0.010f, pistolKickYaw = 0.004f; const float rifleKickPitch = 0.018f, rifleKickYaw = 0.010f; const float pistolCrossKick = 2.0f, rifleCrossKick = 4.0f; const float pistolSprayGrow = 0.4f, rifleSprayGrow = 1.2f; const float burstResetTime = 0.18f; MsgHello hello = {0}; hello.type = MSG_HELLO; hello.protocol = PROTOCOL_VERSION; strncpy(hello.username, myName, USERNAME_MAX - 1); sendto(sock, (const char *)&hello, (int)sizeof(hello), 0, (const struct sockaddr *)&srv, sizeof(srv)); while (!WindowShouldClose()) { clientTick++; float dt = GetFrameTime(); fireCooldown -= dt; if (fireCooldown < 0.0f) fireCooldown = 0.0f; burstResetTimer -= dt; if (burstResetTimer <= 0.0f) shotsInBurst = 0; { float k = 1.0f - expf(-recoilReturn * dt); recoilYaw += (0.0f - recoilYaw) * k; recoilPitch += (0.0f - recoilPitch) * k; } { float k = 1.0f - expf(-crossReturn * dt); crossSpread += (0.0f - crossSpread) * k; if (crossSpread < 0.01f) crossSpread = 0.0f; } for (;;) { uint8_t buf[1400]; struct sockaddr_in from = {0}; socklen_t fromLen = sizeof(from); int n = (int)recvfrom(sock, (char *)buf, (int)sizeof(buf), 0, (struct sockaddr *)&from, &fromLen); if (n <= 0) { #ifdef _WIN32 if (WSAGetLastError() == WSAEWOULDBLOCK) break; #else if (errno == EWOULDBLOCK || errno == EAGAIN) break; #endif break; } uint8_t type = buf[0]; if (type == MSG_WELCOME && n >= (int)sizeof(MsgWelcome)) { MsgWelcome *w = (MsgWelcome *)buf; myId = w->playerId; } else if (type == MSG_SNAPSHOT && n >= (int)sizeof(MsgSnapshot)) { MsgSnapshot *s = (MsgSnapshot *)buf; for (int i = 0; i < MAX_PLAYERS; i++) rp[i].present = 0; for (int i = 0; i < (int)s->count && i < MAX_PLAYERS; i++) { PlayerStateNet *ps = &s->p[i]; if (ps->id >= MAX_PLAYERS) continue; RemotePlayer *p = &rp[ps->id]; p->present = 1; p->alive = ps->alive; p->hp = ps->hp; p->pos = v3(ps->x, ps->y, ps->z); p->yaw = ps->yaw; p->pitch = ps->pitch; p->weapon = ps->weapon; p->pistolMag = ps->pistolMag; p->rifleMag = ps->rifleMag; p->pistolAmmo = ps->pistolAmmo; p->rifleAmmo = ps->rifleAmmo; p->medkits = ps->medkits; p->reloadTimeLeft = ps->reloadTimeLeft; memset(p->username, 0, USERNAME_MAX); strncpy(p->username, ps->username, USERNAME_MAX - 1); } if (myId != 255 && rp[myId].present) { Vector3 body = rp[myId].pos; camPos.x = body.x; camPos.y = body.y + 1.0f; camPos.z = body.z + 0.0001f; // Prevent camera from clipping into terrain float terrain_h = get_terrain_height(camPos.x, camPos.z); camPos.y = fmaxf(camPos.y, terrain_h + 1.5f); } } else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) { MsgItems *m = (MsgItems *)buf; for (int i = 0; i < MAX_ITEMS; i++) wi[i].present = 0; for (int i = 0; i < (int)m->count && i < MAX_ITEMS; i++) { wi[i].present = 1; wi[i].id = m->items[i].id; wi[i].type = m->items[i].type; wi[i].qty = m->items[i].qty; wi[i].pos = v3(m->items[i].x, m->items[i].y, m->items[i].z); } } } Vector2 md = GetMouseDelta(); const float sens = 0.0025f; yaw -= md.x * sens; pitch -= md.y * sens; if (pitch < -1.5f) pitch = -1.5f; if (pitch > 1.5f) pitch = 1.5f; float viewYaw = yaw + recoilYaw; float viewPitch = pitch + recoilPitch; if (viewPitch < -1.5f) viewPitch = -1.5f; if (viewPitch > 1.5f) viewPitch = 1.5f; float moveX = 0.0f, moveZ = 0.0f; if (IsKeyDown(KEY_A)) moveX += 1.0f; if (IsKeyDown(KEY_D)) moveX -= 1.0f; if (IsKeyDown(KEY_W)) moveZ += 1.0f; if (IsKeyDown(KEY_S)) moveZ -= 1.0f; uint8_t buttons = 0; if (IsKeyPressed(KEY_ONE)) buttons |= BTN_SWITCH_PISTOL; if (IsKeyPressed(KEY_TWO)) buttons |= BTN_SWITCH_RIFLE; if (IsKeyPressed(KEY_R)) buttons |= BTN_RELOAD; if (IsKeyPressed(KEY_F)) buttons |= BTN_PICK; if (IsKeyPressed(KEY_H)) buttons |= BTN_USE_MEDKIT; if (IsKeyPressed(KEY_SPACE)) buttons |= BTN_JUMP; if (myId != 255) { MsgInput in = {0}; in.type = MSG_INPUT; in.playerId = myId; in.clientTick = clientTick; in.moveX = moveX; in.moveZ = moveZ; in.yaw = viewYaw; in.pitch = viewPitch; in.buttons = buttons; sendto(sock, (const char *)&in, (int)sizeof(in), 0, (const struct sockaddr *)&srv, sizeof(srv)); } if (myId != 255 && IsMouseButtonDown(MOUSE_BUTTON_LEFT) && fireCooldown <= 0.0f && rp[myId].present) { int wpn = rp[myId].weapon; float rate = (wpn == WEAPON_PISTOL) ? pistolFireRate : rifleFireRate; fireCooldown = 1.0f / rate; shotsInBurst++; burstResetTimer = burstResetTime; if (wpn == WEAPON_PISTOL) { recoilPitch += pistolKickPitch; recoilYaw += (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * pistolKickYaw; crossSpread += pistolCrossKick + shotsInBurst * pistolSprayGrow; } else { recoilPitch += rifleKickPitch; recoilYaw += (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * rifleKickYaw; crossSpread += rifleCrossKick + shotsInBurst * rifleSprayGrow; } MsgShoot shmsg = {0}; shmsg.type = MSG_SHOOT; shmsg.playerId = myId; shmsg.clientTick = clientTick; sendto(sock, (const char *)&shmsg, (int)sizeof(shmsg), 0, (const struct sockaddr *)&srv, sizeof(srv)); } Vector3 forward = v3(sinf(viewYaw) * cosf(viewPitch), sinf(viewPitch), cosf(viewYaw) * cosf(viewPitch)); Camera3D cam = {0}; cam.position = camPos; cam.target = v3_add(camPos, forward); cam.up = v3(0, 1, 0); cam.fovy = 75.0f; cam.projection = CAMERA_PERSPECTIVE; BeginDrawing(); ClearBackground((Color){135, 206, 235, 255}); BeginMode3D(cam); // Set up shader uniforms for terrain rendering if (terrainShader.shader.id != 0) { // Convert light direction to shader format (should be pointing TO the // light) float lightDirArray[3] = {-dirLight.direction.x, -dirLight.direction.y, -dirLight.direction.z}; float lightColorArray[3] = {dirLight.color.x, dirLight.color.y, dirLight.color.z}; float viewPosArray[3] = {camPos.x, camPos.y, camPos.z}; // Terrain color in 0-1 range (80, 140, 70) / 255 float terrainColorArray[3] = {80.0f / 255.0f, 140.0f / 255.0f, 70.0f / 255.0f}; // Debug mode: 0=normal lighting, 1=show normals as colors, 2=show AO only SetShaderValue(terrainShader.shader, terrainShader.locViewPos, viewPosArray, SHADER_UNIFORM_VEC3); SetShaderValue(terrainShader.shader, terrainShader.locLightDir, lightDirArray, SHADER_UNIFORM_VEC3); SetShaderValue(terrainShader.shader, terrainShader.locLightColor, lightColorArray, SHADER_UNIFORM_VEC3); SetShaderValue(terrainShader.shader, terrainShader.locLightIntensity, &dirLight.intensity, SHADER_UNIFORM_FLOAT); SetShaderValue(terrainShader.shader, terrainShader.locAmbientIntensity, &dirLight.ambientIntensity, SHADER_UNIFORM_FLOAT); SetShaderValue(terrainShader.shader, terrainShader.locTerrainColor, terrainColorArray, SHADER_UNIFORM_VEC3); } // Draw terrain with shader terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color = (Color){80, 140, 70, 255}; DrawModel(terrainModel, (Vector3){0, 0, 0}, 1.0f, WHITE); // Items with basic lighting (no shader for now to keep it simple) for (int i = 0; i < MAX_ITEMS; i++) { if (!wi[i].present) continue; Color ic = (Color){220, 220, 220, 255}; if (wi[i].type == ITEM_MEDKIT) ic = (Color){120, 255, 120, 255}; if (wi[i].type == ITEM_AMMO_PISTOL) ic = (Color){255, 220, 120, 255}; if (wi[i].type == ITEM_AMMO_RIFLE) ic = (Color){255, 180, 120, 255}; // Apply basic lighting to items Vector3 itemNormal = v3(0, 1, 0); Color litColor = apply_lighting_with_shadows(ic, itemNormal, wi[i].pos, dirLight); DrawSphere(wi[i].pos, 0.3f, litColor); } // Players with basic lighting for (int i = 0; i < MAX_PLAYERS; i++) { if (!rp[i].present) continue; Vector3 p = rp[i].pos; Color c = (i == myId) ? (Color){80, 180, 255, 255} : (Color){255, 80, 80, 255}; if (!rp[i].alive) c = (Color){120, 120, 120, 255}; // Apply basic lighting to players Vector3 playerNormal = v3(0, 1, 0); Color litPlayerColor = apply_lighting_with_shadows(c, playerNormal, p, dirLight); DrawCapsule(v3(p.x, p.y - 0.5f, p.z), v3(p.x, p.y + 0.5f, p.z), 0.35f, 8, 8, litPlayerColor); } EndMode3D(); // Nameplates for (int i = 0; i < MAX_PLAYERS; i++) { if (!rp[i].present || !rp[i].username[0]) continue; Vector3 head = v3(rp[i].pos.x, rp[i].pos.y + 1.2f, rp[i].pos.z); Vector3 camForward = v3_norm(v3_sub(cam.target, cam.position)); Vector3 toHead = v3_sub(head, cam.position); if (v3_dot(camForward, toHead) <= 0.0f) continue; Vector2 s = GetWorldToScreen(head, cam); if (s.x < -200 || s.x > sw + 200 || s.y < -200 || s.y > sh + 200) continue; int fontSize = 18; int w = MeasureText(rp[i].username, fontSize); Color tc = (i == myId) ? (Color){180, 230, 255, 255} : RAYWHITE; DrawText(rp[i].username, (int)(s.x - w / 2), (int)(s.y - fontSize), fontSize, tc); } // HUD if (myId == 255 || !rp[myId].present) { DrawText("Connecting...", 10, 10, 20, RAYWHITE); } else { DrawRectangle(10, 10, 280, 110, (Color){0, 0, 0, 120}); DrawText("HP", 20, 20, 20, RAYWHITE); int healthBarWidth = (rp[myId].hp * 220) / 100; Color healthColor = (rp[myId].hp > 60) ? (Color){80, 255, 80, 120} : (rp[myId].hp > 30) ? (Color){255, 200, 80, 120} : (Color){255, 80, 80, 120}; DrawRectangle(20, 45, 220, 20, (Color){40, 40, 40, 255}); DrawRectangle(20, 45, healthBarWidth, 20, healthColor); DrawText(TextFormat("%d", rp[myId].hp), 250, 47, 18, RAYWHITE); DrawText(TextFormat("Medkits: %d (H to use)", rp[myId].medkits), 20, 75, 18, (rp[myId].medkits > 0) ? (Color){120, 255, 120, 120} : (Color){120, 120, 120, 120}); DrawText("1=Pistol 2=Rifle R=Reload F=Pick SPACE=Jump", 20, 95, 12, (Color){150, 150, 150, 150}); const char *weaponName = (rp[myId].weapon == WEAPON_PISTOL) ? "PISTOL" : "RIFLE"; Color weaponColor = (rp[myId].weapon == WEAPON_PISTOL) ? (Color){100, 200, 255, 255} : (Color){255, 150, 100, 255}; int currentMag = (rp[myId].weapon == WEAPON_PISTOL) ? rp[myId].pistolMag : rp[myId].rifleMag; int reserveAmmo = (rp[myId].weapon == WEAPON_PISTOL) ? rp[myId].pistolAmmo : rp[myId].rifleAmmo; DrawRectangle(sw - 260, sh - 120, 250, 110, (Color){0, 0, 0, 180}); DrawText(weaponName, sw - 250, sh - 110, 28, weaponColor); DrawText(TextFormat("%d", currentMag), sw - 250, sh - 75, 40, RAYWHITE); DrawText(TextFormat("/ %d", reserveAmmo), sw - 140, sh - 65, 24, (Color){180, 180, 180, 255}); if (rp[myId].reloadTimeLeft > 0) DrawText("RELOADING...", sw - 250, sh - 30, 20, (Color){255, 200, 80, 255}); else if (currentMag == 0) DrawText("RELOAD!", sw - 250, sh - 30, 20, (Color){255, 80, 80, 255}); } DrawFPS(sw - 90, 10); // Crosshair { int cx = sw / 2, cy = sh / 2; int gap = 6 + (int)crossSpread; int len = 10, thick = 2; Color col = (Color){240, 240, 245, 220}; DrawRectangle(cx - gap - len, cy - thick / 2, len, thick, col); DrawRectangle(cx + gap, cy - thick / 2, len, thick, col); DrawRectangle(cx - thick / 2, cy - gap - len, thick, len, col); DrawRectangle(cx - thick / 2, cy + gap, thick, len, col); DrawCircleLines(cx, cy, 3.0f, (Color){240, 240, 245, 160}); } EndDrawing(); } UnloadModel(terrainModel); unload_terrain_shader(&terrainShader); CloseWindow(); CLOSESOCK(sock); #ifdef _WIN32 WSACleanup(); #endif return 0; }