suicmez/game/client.c
2025-12-18 21:55:51 +05:30

1092 lines
36 KiB
C

// game/client.c - SMOOTH TERRAIN VERSION WITH DIRECTIONAL LIGHT & SHADOWS
#include "raylib.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#define CLOSESOCK closesocket
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#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;
}