From 6f198b17a98b58375f21c7abfe69945f4ccabdb5 Mon Sep 17 00:00:00 2001 From: Masashi Date: Sat, 20 Dec 2025 04:00:59 +0530 Subject: [PATCH] unsafe --- game/client | Bin 34456 -> 43752 bytes game/client.c | 1482 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 1156 insertions(+), 326 deletions(-) diff --git a/game/client b/game/client index 673d436fc8da0fddcf31bc3c5d26e67f38e74558..989692a8d348427eb311a9e24d0731cf620da34b 100755 GIT binary patch literal 43752 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 Y(|)xmeqnG>{dtc|iO;7gq@l+D0{Wm;4gdfE literal 34456 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 oIsPzQZ|W{f%KPGbBz(G`! +#include #include -#include -#include #include -void* gc_alloc(const TypeInfo* type, size_t size); -void gc_init(void); -void gc_shutdown(void); +#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 -// Helper for allocating arrays -static void* suic_alloc_array(const TypeInfo* type, size_t elem_size, size_t len, void* init_data) { - void* ptr = gc_alloc(type, elem_size * len); - if (init_data) memcpy(ptr, init_data, elem_size * len); - return ptr; +#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) + +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; + +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; } -// Helper for allocating structs -static void* suic_alloc_struct(const TypeInfo* type, size_t size, void* init_data) { - void* ptr = gc_alloc(type, size); - if (init_data) memcpy(ptr, init_data, size); - return ptr; +static void unload_terrain_shader(TerrainShader *ts) { + if (ts && ts->shader.id != 0) { + UnloadShader(ts->shader); + ts->shader.id = 0; + } } +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}; -struct DirectionalLight { - struct Vector3* direction; - struct Vector3* color; - float intensity; - float ambientIntensity; - float shadowBias; - float shadowIntensity; -}; -struct TerrainShader { - int shader; - int locViewPos; - int locLightDir; - int locLightColor; - int locLightIntensity; - int locAmbientIntensity; - int locTerrainColor; -}; -struct MsgType_MSG_HELLO { -}; -struct MsgType_MSG_WELCOME { -}; -struct MsgType_MSG_INPUT { -}; -struct MsgType_MSG_SNAPSHOT { -}; -struct MsgType_MSG_SHOOT { -}; -struct MsgType_MSG_ITEMS { -}; -struct MsgType_MSG_ROOM_STATE { -}; -struct MsgType_union { - struct MsgType_MSG_HELLO msg_hello; - struct MsgType_MSG_WELCOME msg_welcome; - struct MsgType_MSG_INPUT msg_input; - struct MsgType_MSG_SNAPSHOT msg_snapshot; - struct MsgType_MSG_SHOOT msg_shoot; - struct MsgType_MSG_ITEMS msg_items; - struct MsgType_MSG_ROOM_STATE msg_room_state; -}; -struct MsgType { - int discriminant; - struct MsgType_union data; -}; -struct MsgHello { - uint8_t type; - struct u32* protocol; - char* username; -}; -struct MsgWelcome { - uint8_t type; - uint8_t playerId; - struct u32* serverTick; -}; -struct MsgInput { - uint8_t type; - uint8_t playerId; - struct u32* clientTick; - float moveX; - float moveZ; - float yaw; - float pitch; - uint8_t buttons; -}; -struct MsgShoot { - uint8_t type; - uint8_t playerId; - struct u32* clientTick; -}; -struct PlayerStateNet { - uint8_t id; - uint8_t alive; - struct i16* hp; - float x; - float y; - float z; - float yaw; - float pitch; - uint8_t weapon; - struct i16* pistolMag; - struct i16* rifleMag; - struct i16* pistolAmmo; - struct i16* rifleAmmo; - struct i16* medkits; - struct i16* reloadTimeLeft; - char* username; -}; -struct MsgSnapshot { - uint8_t type; - struct u32* serverTick; - uint8_t count; - struct PlayerStateNet** p; -}; -struct ItemNet { - struct u16* id; - uint8_t type; - struct i16* qty; - float x; - float y; - float z; -}; -struct MsgItems { - uint8_t type; - struct u32* serverTick; - uint8_t count; - struct ItemNet** items; -}; -struct MsgRoomState { - uint8_t type; - uint8_t state; - float countdownRemaining; - uint8_t winnerId; - char* winnerName; -}; -struct RemotePlayer { - int present; - int alive; - int hp; - struct Vector3* pos; - struct Vector3* prevPos; - float yaw; - float pitch; - uint8_t weapon; - struct i16* pistolMag; - struct i16* rifleMag; - struct i16* pistolAmmo; - struct i16* rifleAmmo; - struct i16* medkits; - struct i16* reloadTimeLeft; - char* username; -}; -struct WorldItem { - int present; - struct u16* id; - uint8_t type; - struct i16* qty; - struct Vector3* pos; -}; - -static const uint8_t sui_bitmap_DirectionalLight[] = { 1, 1, 0, 0, 0, 0 }; -static const TypeInfo sui_typeinfo_DirectionalLight = { - .field_count = 6, - .pointer_count = 2, - .pointer_bitmap = sui_bitmap_DirectionalLight -}; -static const uint8_t sui_bitmap_TerrainShader[] = { 0, 0, 0, 0, 0, 0, 0 }; -static const TypeInfo sui_typeinfo_TerrainShader = { - .field_count = 7, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_TerrainShader -}; -static const uint8_t sui_bitmap_MsgType_MSG_HELLO[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_HELLO = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_HELLO -}; -static const uint8_t sui_bitmap_MsgType_MSG_WELCOME[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_WELCOME = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_WELCOME -}; -static const uint8_t sui_bitmap_MsgType_MSG_INPUT[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_INPUT = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_INPUT -}; -static const uint8_t sui_bitmap_MsgType_MSG_SNAPSHOT[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_SNAPSHOT = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_SNAPSHOT -}; -static const uint8_t sui_bitmap_MsgType_MSG_SHOOT[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_SHOOT = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_SHOOT -}; -static const uint8_t sui_bitmap_MsgType_MSG_ITEMS[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_ITEMS = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_ITEMS -}; -static const uint8_t sui_bitmap_MsgType_MSG_ROOM_STATE[] = { }; -static const TypeInfo sui_typeinfo_MsgType_MSG_ROOM_STATE = { - .field_count = 0, - .pointer_count = 0, - .pointer_bitmap = sui_bitmap_MsgType_MSG_ROOM_STATE -}; -static const uint8_t sui_bitmap_MsgType_union[] = { 1, 1, 1, 1, 1, 1, 1 }; -static const TypeInfo sui_typeinfo_MsgType_union = { - .field_count = 7, - .pointer_count = 7, - .pointer_bitmap = sui_bitmap_MsgType_union -}; -static const uint8_t sui_bitmap_MsgType[] = { 0, 1 }; -static const TypeInfo sui_typeinfo_MsgType = { - .field_count = 2, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgType -}; -static const uint8_t sui_bitmap_MsgHello[] = { 0, 1, 1 }; -static const TypeInfo sui_typeinfo_MsgHello = { - .field_count = 3, - .pointer_count = 2, - .pointer_bitmap = sui_bitmap_MsgHello -}; -static const uint8_t sui_bitmap_MsgWelcome[] = { 0, 0, 1 }; -static const TypeInfo sui_typeinfo_MsgWelcome = { - .field_count = 3, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgWelcome -}; -static const uint8_t sui_bitmap_MsgInput[] = { 0, 0, 1, 0, 0, 0, 0, 0 }; -static const TypeInfo sui_typeinfo_MsgInput = { - .field_count = 8, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgInput -}; -static const uint8_t sui_bitmap_MsgShoot[] = { 0, 0, 1 }; -static const TypeInfo sui_typeinfo_MsgShoot = { - .field_count = 3, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgShoot -}; -static const uint8_t sui_bitmap_PlayerStateNet[] = { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 }; -static const TypeInfo sui_typeinfo_PlayerStateNet = { - .field_count = 16, - .pointer_count = 8, - .pointer_bitmap = sui_bitmap_PlayerStateNet -}; -static const uint8_t sui_bitmap_MsgSnapshot[] = { 0, 1, 0, 0 }; -static const TypeInfo sui_typeinfo_MsgSnapshot = { - .field_count = 4, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgSnapshot -}; -static const uint8_t sui_bitmap_ItemNet[] = { 1, 0, 1, 0, 0, 0 }; -static const TypeInfo sui_typeinfo_ItemNet = { - .field_count = 6, - .pointer_count = 2, - .pointer_bitmap = sui_bitmap_ItemNet -}; -static const uint8_t sui_bitmap_MsgItems[] = { 0, 1, 0, 0 }; -static const TypeInfo sui_typeinfo_MsgItems = { - .field_count = 4, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgItems -}; -static const uint8_t sui_bitmap_MsgRoomState[] = { 0, 0, 0, 0, 1 }; -static const TypeInfo sui_typeinfo_MsgRoomState = { - .field_count = 5, - .pointer_count = 1, - .pointer_bitmap = sui_bitmap_MsgRoomState -}; -static const uint8_t sui_bitmap_RemotePlayer[] = { 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 }; -static const TypeInfo sui_typeinfo_RemotePlayer = { - .field_count = 15, - .pointer_count = 9, - .pointer_bitmap = sui_bitmap_RemotePlayer -}; -static const uint8_t sui_bitmap_WorldItem[] = { 0, 1, 0, 1, 1 }; -static const TypeInfo sui_typeinfo_WorldItem = { - .field_count = 5, - .pointer_count = 3, - .pointer_bitmap = sui_bitmap_WorldItem -}; - -char* item_name(uint8_t t); -struct Color* apply_directional_lighting(struct Color* baseColor, struct Vector3* normal, struct DirectionalLight* light); -int suic_main(void); - - -char* item_name(uint8_t t) { - return ((t == 1) ? suic_alloc_array(NULL, sizeof(char), 7, "Medkit") : ((t == 2) ? suic_alloc_array(NULL, sizeof(char), 12, "Pistol ammo") : ((t == 3) ? suic_alloc_array(NULL, sizeof(char), 11, "Rifle ammo") : suic_alloc_array(NULL, sizeof(char), 2, "-")))); +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); } -struct Color* apply_directional_lighting(struct Color* baseColor, struct Vector3* normal, struct DirectionalLight* light) { - struct Vector3* lightDir = (*light).direction; - float diff = fmaxf(0.000000, -((((*lightDir).x * (*normal).x) + ((*lightDir).y * (*normal).y)) + ((*lightDir).z * (*normal).z))); - float brightness = ((*light).ambientIntensity + ((diff * (*light).intensity) * (1.000000 - (*light).ambientIntensity))); - uint8_t r = (uint8_t) ((float) (*baseColor).r * brightness); - uint8_t g = (uint8_t) ((float) (*baseColor).g * brightness); - uint8_t b = (uint8_t) ((float) (*baseColor).b * brightness); - return suic_alloc_struct(&sui_typeinfo_Color, sizeof(struct Color), &(struct Color){ .r = r, .g = g, .b = b, .a = (*baseColor).a }); +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); } -int suic_main(void) { - int sw = 1280; - int sh = 720; - suic_init_window(sw, sh, suic_alloc_array(NULL, sizeof(char), 23, "Voxel Shooter - Client")); - suic_set_target_fps(120); - suic_disable_cursor(); - while ((suic_window_should_close() == false)) { - suic_begin_drawing(); - suic_clear_background(135, 206, 235, 255); - suic_draw_text(suic_alloc_array(NULL, sizeof(char), 14, "Hello Suicmez"), 10, 10, 20, 255, 255, 255, 255); - draw_fps(10, 40); - suic_end_drawing(); +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.05f, z * 0.05f, 2); + height += fbm_noise(x * 0.01f, z * 0.01f, 2) * 1.5f; + return height * 4.0f + 2.0f; +} + +#define TERRAIN_SIZE 256 +#define TERRAIN_SCALE 1.0f +#define TERRAIN_MIN (-TERRAIN_SIZE * TERRAIN_SCALE / 2.0f) +#define TERRAIN_MAX (TERRAIN_SIZE * TERRAIN_SCALE / 2.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); } - suic_close_window(); - return 0; + } + + // 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; } -int main(int argc, char* argv[]) { - // Initialize GC - gc_init(); - // init globals - // init event loop - int result = suic_main(); - // Shutdown GC - gc_shutdown(); - return result; +// 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}; +} + +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 Vector3 v3_mul(Vector3 a, float s) { + return v3(a.x * s, a.y * s, a.z * s); +} +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); +} + +// 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}; +} + +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, + MSG_ROOM_STATE = 7 +} 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; + +typedef struct MsgRoomState { + uint8_t type; + uint8_t state; + float countdownRemaining; + uint8_t winnerId; + char winnerName[USERNAME_MAX]; +} MsgRoomState; +#pragma pack(pop) + +typedef struct RemotePlayer { + int present, alive, hp; + Vector3 pos; + Vector3 prevPos; + 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}; + + // Room state + int roomState = 0; + float countdownRemaining = 0.0f; + char winnerName[USERNAME_MAX] = {0}; + + Vector3 camPos = v3(0, 5.0f, 6); + float yaw = 0.0f, pitch = 0.0f; + + Vector3 prevBodyPos = v3(0, 5.0f, 6); + Vector3 currentTargetBody = v3(0, 5.0f, 6); + float interpTimer = 0.0f; + + float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f; + int scoped = 0; + 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->prevPos = p->pos; + 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) { + prevBodyPos = currentTargetBody; + currentTargetBody = rp[myId].pos; + interpTimer = 0.0f; + } + } 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); + } + } else if (type == MSG_ROOM_STATE && n >= (int)sizeof(MsgRoomState)) { + const MsgRoomState *rs = (const MsgRoomState *)buf; + roomState = rs->state; + countdownRemaining = rs->countdownRemaining; + memset(winnerName, 0, USERNAME_MAX); + if (rs->winnerId < 255) { + strncpy(winnerName, rs->winnerName, USERNAME_MAX - 1); + } + } + } + + // Interpolate camera position + if (myId != 255 && rp[myId].present) { + float interpFactor = interpTimer / (1.0f / 20.0f); + if (interpFactor > 1.0f) + interpFactor = 1.0f; + Vector3 interpBody = v3_add(v3_mul(prevBodyPos, 1.0f - interpFactor), + v3_mul(currentTargetBody, interpFactor)); + camPos.x = interpBody.x; + camPos.y = interpBody.y + 1.0f; + camPos.z = interpBody.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); + } + interpTimer += dt; + + 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 (IsKeyPressed(KEY_Z)) + scoped = !scoped; + + 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)); + + // Offset camera position: 0.3m in front, 0.2m below + camPos = v3_add(camPos, v3_mul(forward, 0.3f)); + camPos.y -= 0.2f; + + // Ensure camera doesn't clip into terrain after offset + float terrain_h = get_terrain_height(camPos.x, camPos.z); + camPos.y = fmaxf(camPos.y, terrain_h + 1.5f); + + Camera3D cam = {0}; + cam.position = camPos; + cam.target = v3_add(camPos, forward); + cam.up = v3(0, 1, 0); + cam.fovy = scoped ? 30.0f : 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); + + // Draw border + float borderSize = TERRAIN_MAX - TERRAIN_MIN; + DrawCube((Vector3){0, 0, 0}, borderSize, 1000, borderSize, (Color){255, 0, 0, 100}); + + // 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; + float interpFactor = interpTimer / (1.0f / 20.0f); + if (interpFactor > 1.0f) + interpFactor = 1.0f; + Vector3 p = v3_add(v3_mul(rp[i].prevPos, 1.0f - interpFactor), + v3_mul(rp[i].pos, interpFactor)); + 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; + float interpFactor = interpTimer / (1.0f / 20.0f); + if (interpFactor > 1.0f) + interpFactor = 1.0f; + Vector3 p = v3_add(v3_mul(rp[i].prevPos, 1.0f - interpFactor), + v3_mul(rp[i].pos, interpFactor)); + Vector3 head = v3(p.x, p.y + 1.2f, p.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); + + // Room state overlay + if (roomState == 0) { // Waiting + DrawRectangle(sw / 2 - 150, sh / 2 - 50, 350, 100, (Color){0, 0, 0, 200}); + DrawText("WAITING FOR PLAYERS", sw / 2 - 120, sh / 2 - 30, 24, RAYWHITE); + DrawText("Need at least 2 players", sw / 2 - 100, sh / 2 - 5, 18, + (Color){200, 200, 200, 255}); + } else if (roomState == 1) { // Counting down + DrawRectangle(sw / 2 - 150, sh / 2 - 50, 350, 100, (Color){0, 0, 0, 200}); + DrawText("GAME STARTING SOON", sw / 2 - 120, sh / 2 - 30, 24, + (Color){255, 255, 80, 255}); + DrawText(TextFormat("%.1f seconds", countdownRemaining), sw / 2 - 60, + sh / 2 - 5, 20, RAYWHITE); + } else if (roomState == 3) { // Finished + DrawRectangle(sw / 2 - 200, sh / 2 - 50, 450, 100, (Color){0, 0, 0, 200}); + DrawText("GAME FINISHED", sw / 2 - 80, sh / 2 - 30, 28, + (Color){255, 80, 80, 255}); + if (winnerName[0]) { + DrawText(TextFormat("Winner: %s", winnerName), sw / 2 - 100, sh / 2 - 5, + 24, (Color){255, 255, 80, 255}); + } else { + DrawText("No winner", sw / 2 - 50, sh / 2 - 5, 24, RAYWHITE); + } + } + + // Crosshair + { + int cx = sw / 2, cy = sh / 2; + int gap = scoped ? 2 : 6 + (int)crossSpread; + int len = scoped ? 5 : 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, scoped ? 1.5f : 3.0f, + (Color){240, 240, 245, 160}); + } + + EndDrawing(); + } + + UnloadModel(terrainModel); + unload_terrain_shader(&terrainShader); + CloseWindow(); + CLOSESOCK(sock); +#ifdef _WIN32 + WSACleanup(); +#endif + return 0; +}