1092 lines
36 KiB
C
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;
|
|
}
|