diff --git a/game/client.c b/game/client.c index 42833be..c7eb9e5 100644 --- a/game/client.c +++ b/game/client.c @@ -1,9 +1,4 @@ -// client.c - raylib client (C99) -// Features: username prompt, inverted A/D, inverted pitch, recoil, crosshair -// widening, spray, inventory UI toggle on E, pick/drop/use items, renders -// dropped items from server. Build (Linux example): -// gcc client.c -O2 -o client -lraylib -lm -lpthread -ldl -lrt -lX11 - +// game/client.c - SMOOTH TERRAIN VERSION WITH DIRECTIONAL LIGHT & SHADOWS #include "raylib.h" #include #include @@ -26,32 +21,355 @@ typedef int socklen_t; #define CLOSESOCK close #endif -#define PROTOCOL_VERSION 4 +#define PROTOCOL_VERSION 5 #define SERVER_PORT 27015 #define MAX_PLAYERS 16 #define USERNAME_MAX 16 #define WEAPON_PISTOL 0 #define WEAPON_RIFLE 1 -#define WEAPON_COUNT 2 -#define INV_SLOTS 6 -#define MAX_ITEMS 64 - -// Inventory item types (must match server) #define ITEM_NONE 0 #define ITEM_MEDKIT 1 -#define ITEM_ARMORPLATE 2 -#define ITEM_AMMO_PISTOL 3 -#define ITEM_AMMO_RIFLE 4 +#define ITEM_AMMO_PISTOL 2 +#define ITEM_AMMO_RIFLE 3 + +#define MAX_ITEMS 64 -// MsgInput.buttons bits #define BTN_RELOAD (1u << 0) -#define BTN_WPN1 (1u << 1) -#define BTN_WPN2 (1u << 2) +#define BTN_SWITCH_PISTOL (1u << 1) +#define BTN_SWITCH_RIFLE (1u << 2) #define BTN_PICK (1u << 3) -#define BTN_DROP (1u << 4) -#define BTN_USE (1u << 5) +#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; + +// ============== SIMPLEX NOISE ============== +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 64 +#define 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); + } + } + + // 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 + for (int i = 0; i < vertexCount; i++) { + mesh.normals[i * 3 + 0] = 0; + mesh.normals[i * 3 + 1] = 1; + mesh.normals[i * 3 + 2] = 0; + } + + for (int t = 0; t < triangleCount; t++) { + int i0 = mesh.indices[t * 3 + 0]; + int i1 = mesh.indices[t * 3 + 1]; + int i2 = mesh.indices[t * 3 + 2]; + + float v0x = mesh.vertices[i0 * 3 + 0]; + float v0y = mesh.vertices[i0 * 3 + 1]; + float v0z = mesh.vertices[i0 * 3 + 2]; + + float v1x = mesh.vertices[i1 * 3 + 0]; + float v1y = mesh.vertices[i1 * 3 + 1]; + float v1z = mesh.vertices[i1 * 3 + 2]; + + float v2x = mesh.vertices[i2 * 3 + 0]; + float v2y = mesh.vertices[i2 * 3 + 1]; + float v2z = mesh.vertices[i2 * 3 + 2]; + + float e1x = v1x - v0x, e1y = v1y - v0y, e1z = v1z - v0z; + float e2x = v2x - v0x, e2y = v2y - v0y, e2z = v2z - v0z; + + float nx = e1y * e2z - e1z * e2y; + float ny = e1z * e2x - e1x * e2z; + float nz = e1x * e2y - e1y * e2x; + + mesh.normals[i0 * 3 + 0] += nx; + mesh.normals[i0 * 3 + 1] += ny; + mesh.normals[i0 * 3 + 2] += nz; + + mesh.normals[i1 * 3 + 0] += nx; + mesh.normals[i1 * 3 + 1] += ny; + mesh.normals[i1 * 3 + 2] += nz; + + mesh.normals[i2 * 3 + 0] += nx; + mesh.normals[i2 * 3 + 1] += ny; + mesh.normals[i2 * 3 + 2] += nz; + } + + // Normalize normals + for (int i = 0; i < vertexCount; i++) { + float nx = mesh.normals[i * 3 + 0]; + float ny = mesh.normals[i * 3 + 1]; + float nz = mesh.normals[i * 3 + 2]; + float len = sqrtf(nx * nx + ny * ny + nz * nz); + if (len > 0.0001f) { + mesh.normals[i * 3 + 0] = nx / len; + mesh.normals[i * 3 + 1] = ny / len; + mesh.normals[i * 3 + 2] = nz / len; + } + } + + 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}; +} +// ============== END LIGHTING HELPERS ============== static void set_nonblocking(int sock) { #ifdef _WIN32 @@ -70,8 +388,7 @@ typedef enum MsgType : uint8_t { MSG_INPUT = 3, MSG_SNAPSHOT = 4, MSG_SHOOT = 5, - MSG_HIT = 6, - MSG_ITEMS = 7 + MSG_ITEMS = 6 } MsgType; typedef struct MsgHello { @@ -90,12 +407,8 @@ typedef struct MsgInput { uint8_t type; uint8_t playerId; uint32_t clientTick; - float moveX; - float moveZ; - float yaw; - float pitch; - uint8_t buttons; // BTN_* - uint8_t slotIndex; // 0..INV_SLOTS-1 (for DROP/USE) + float moveX, moveZ, yaw, pitch; + uint8_t buttons; } MsgInput; typedef struct MsgShoot { @@ -105,20 +418,11 @@ typedef struct MsgShoot { } MsgShoot; typedef struct PlayerStateNet { - uint8_t id; - uint8_t alive; + uint8_t id, alive; int16_t hp; - int16_t armor; // NEW - float x, y, z; - float yaw, pitch; - + float x, y, z, yaw, pitch; uint8_t weapon; - int16_t ammoMag[WEAPON_COUNT]; - int16_t ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int16_t invQty[INV_SLOTS]; - + int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; char username[USERNAME_MAX]; } PlayerStateNet; @@ -129,13 +433,6 @@ typedef struct MsgSnapshot { PlayerStateNet p[MAX_PLAYERS]; } MsgSnapshot; -typedef struct MsgHit { - uint8_t type; - uint8_t shooterId; - uint8_t victimId; - int16_t victimHp; -} MsgHit; - typedef struct ItemNet { uint16_t id; uint8_t type; @@ -152,20 +449,11 @@ typedef struct MsgItems { #pragma pack(pop) typedef struct RemotePlayer { - int present; - int alive; - int hp; - int armor; + int present, alive, hp; Vector3 pos; float yaw, pitch; - uint8_t weapon; - int16_t ammoMag[WEAPON_COUNT]; - int16_t ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int16_t invQty[INV_SLOTS]; - + int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; char username[USERNAME_MAX]; } RemotePlayer; @@ -193,22 +481,17 @@ static float v3_dot(Vector3 a, Vector3 b) { static float v3_len(Vector3 a) { return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z); } -static Vector3 v3_scale(Vector3 a, float s) { - return v3(a.x * s, a.y * s, a.z * s); -} static Vector3 v3_norm(Vector3 a) { float l = v3_len(a); if (l <= 1e-6f) return v3(0, 0, 1); - return v3_scale(a, 1.0f / l); + return v3(a.x / l, a.y / l, a.z / l); } static const char *ItemName(uint8_t t) { switch (t) { case ITEM_MEDKIT: return "Medkit"; - case ITEM_ARMORPLATE: - return "Armor plate"; case ITEM_AMMO_PISTOL: return "Pistol ammo"; case ITEM_AMMO_RIFLE: @@ -218,7 +501,6 @@ static const char *ItemName(uint8_t t) { } } -// Simple username input (ASCII-ish) static void ui_username_prompt(char outName[USERNAME_MAX]) { memset(outName, 0, USERNAME_MAX); while (!WindowShouldClose()) { @@ -259,7 +541,7 @@ int main(void) { #endif const int sw = 1280, sh = 720; - InitWindow(sw, sh, "Voxel Shooter - Client"); + InitWindow(sw, sh, "Voxel Shooter - Client (SMOOTH TERRAIN WITH SHADOWS)"); SetTargetFPS(120); char myName[USERNAME_MAX]; @@ -267,6 +549,22 @@ int main(void) { 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; + int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock < 0) { perror("socket"); @@ -285,47 +583,22 @@ int main(void) { RemotePlayer rp[MAX_PLAYERS] = {0}; WorldItem wi[MAX_ITEMS] = {0}; - // view/camera - Vector3 camPos = v3(0, 1.7f, 6); - float yaw = 0.0f; - float pitch = 0.0f; + Vector3 camPos = v3(0, 5.0f, 6); + float yaw = 0.0f, pitch = 0.0f; - // recoil/crosshair - float recoilYaw = 0.0f, recoilPitch = 0.0f; - float crossSpread = 0.0f; - - // firing + float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f; float fireCooldown = 0.0f; int shotsInBurst = 0; float burstResetTimer = 0.0f; - // inventory UI - int invOpen = 0; - int selectedSlot = 0; - - // desired weapon - int desiredWeapon = WEAPON_RIFLE; - - // tuning - const float pistolFireRate = 4.0f; - const float rifleFireRate = 12.0f; - const float recoilReturn = 18.0f; - const float crossReturn = 14.0f; - - const float pistolKickPitch = 0.010f; - const float pistolKickYaw = 0.004f; - const float rifleKickPitch = 0.018f; - const float rifleKickYaw = 0.010f; - - const float pistolCrossKick = 2.0f; - const float rifleCrossKick = 4.0f; - - const float pistolSprayGrow = 0.4f; - const float rifleSprayGrow = 1.2f; - + 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; - // HELLO MsgHello hello = {0}; hello.type = MSG_HELLO; hello.protocol = PROTOCOL_VERSION; @@ -337,7 +610,6 @@ int main(void) { clientTick++; float dt = GetFrameTime(); - // timers fireCooldown -= dt; if (fireCooldown < 0.0f) fireCooldown = 0.0f; @@ -345,13 +617,11 @@ int main(void) { if (burstResetTimer <= 0.0f) shotsInBurst = 0; - // recoil return { float k = 1.0f - expf(-recoilReturn * dt); recoilYaw += (0.0f - recoilYaw) * k; recoilPitch += (0.0f - recoilPitch) * k; } - // crosshair return { float k = 1.0f - expf(-crossReturn * dt); crossSpread += (0.0f - crossSpread) * k; @@ -359,7 +629,6 @@ int main(void) { crossSpread = 0.0f; } - // receive packets for (;;) { uint8_t buf[1400]; struct sockaddr_in from = {0}; @@ -395,21 +664,15 @@ int main(void) { p->present = 1; p->alive = ps->alive; p->hp = ps->hp; - p->armor = ps->armor; p->pos = v3(ps->x, ps->y, ps->z); p->yaw = ps->yaw; p->pitch = ps->pitch; p->weapon = ps->weapon; - - for (int w = 0; w < WEAPON_COUNT; w++) { - p->ammoMag[w] = ps->ammoMag[w]; - p->ammoRes[w] = ps->ammoRes[w]; - } - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - p->invType[sidx] = ps->invType[sidx]; - p->invQty[sidx] = ps->invQty[sidx]; - } - + p->pistolMag = ps->pistolMag; + p->rifleMag = ps->rifleMag; + p->pistolAmmo = ps->pistolAmmo; + p->rifleAmmo = ps->rifleAmmo; + p->medkits = ps->medkits; memset(p->username, 0, USERNAME_MAX); strncpy(p->username, ps->username, USERNAME_MAX - 1); } @@ -417,8 +680,8 @@ int main(void) { 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; - desiredWeapon = rp[myId].weapon; } } else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) { MsgItems *m = (MsgItems *)buf; @@ -435,25 +698,6 @@ int main(void) { } } - // toggle inventory UI on E - if (IsKeyPressed(KEY_E)) - invOpen = !invOpen; - - // select slot 1..6 - if (IsKeyPressed(KEY_ONE)) - selectedSlot = 0; - if (IsKeyPressed(KEY_TWO)) - selectedSlot = 1; - if (IsKeyPressed(KEY_THREE)) - selectedSlot = 2; - if (IsKeyPressed(KEY_FOUR)) - selectedSlot = 3; - if (IsKeyPressed(KEY_FIVE)) - selectedSlot = 4; - if (IsKeyPressed(KEY_SIX)) - selectedSlot = 5; - - // mouse look: yaw fixed, pitch inverted Vector2 md = GetMouseDelta(); const float sens = 0.0025f; yaw -= md.x * sens; @@ -463,7 +707,6 @@ int main(void) { if (pitch > 1.5f) pitch = 1.5f; - // apply recoil to view float viewYaw = yaw + recoilYaw; float viewPitch = pitch + recoilPitch; if (viewPitch < -1.5f) @@ -471,7 +714,6 @@ int main(void) { if (viewPitch > 1.5f) viewPitch = 1.5f; - // movement (A/D inverted) float moveX = 0.0f, moveZ = 0.0f; if (IsKeyDown(KEY_A)) moveX += 1.0f; @@ -482,32 +724,20 @@ int main(void) { if (IsKeyDown(KEY_S)) moveZ -= 1.0f; - // input buttons uint8_t buttons = 0; - - // weapon switch (separate from slot select) - if (IsKeyPressed(KEY_Z)) { - desiredWeapon = WEAPON_PISTOL; - buttons |= BTN_WPN1; - } - if (IsKeyPressed(KEY_X)) { - desiredWeapon = WEAPON_RIFLE; - buttons |= BTN_WPN2; - } - + 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_G)) - buttons |= BTN_DROP; - if (IsKeyPressed(KEY_C)) - buttons |= BTN_USE; // use armor/medkit depending on selected slot - if (IsKeyPressed(KEY_V)) - buttons |= - BTN_USE; // alternate use key (also triggers, server uses slot type) + if (IsKeyPressed(KEY_H)) + buttons |= BTN_USE_MEDKIT; + if (IsKeyPressed(KEY_SPACE)) + buttons |= BTN_JUMP; - // send INPUT if (myId != 255) { MsgInput in = {0}; in.type = MSG_INPUT; @@ -518,23 +748,20 @@ int main(void) { in.yaw = viewYaw; in.pitch = viewPitch; in.buttons = buttons; - in.slotIndex = (uint8_t)selectedSlot; sendto(sock, (const char *)&in, (int)sizeof(in), 0, (const struct sockaddr *)&srv, sizeof(srv)); } - // shoot (hold LMB) if (myId != 255 && IsMouseButtonDown(MOUSE_BUTTON_LEFT) && - fireCooldown <= 0.0f) { - float rate = - (desiredWeapon == WEAPON_PISTOL) ? pistolFireRate : rifleFireRate; - float interval = 1.0f / rate; - fireCooldown = interval; + 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 (desiredWeapon == WEAPON_PISTOL) { + if (wpn == WEAPON_PISTOL) { recoilPitch += pistolKickPitch; recoilYaw += (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * pistolKickYaw; @@ -554,7 +781,6 @@ int main(void) { (const struct sockaddr *)&srv, sizeof(srv)); } - // camera Vector3 forward = v3(sinf(viewYaw) * cosf(viewPitch), sinf(viewPitch), cosf(viewYaw) * cosf(viewPitch)); @@ -566,42 +792,42 @@ int main(void) { cam.projection = CAMERA_PERSPECTIVE; BeginDrawing(); - ClearBackground((Color){20, 24, 32, 255}); + ClearBackground((Color){135, 206, 235, 255}); BeginMode3D(cam); - // ground - for (int z = -16; z <= 16; z++) { - for (int x = -16; x <= 16; x++) { - DrawCubeV(v3((float)x, 0.0f, (float)z), v3(1, 1, 1), - (Color){50, 60, 75, 255}); - } - } + // Calculate terrain normal at camera position for lighting + Vector3 terrainNormal = v3(0, 1, 0); // Default up normal - // blocks - DrawCubeV(v3(2, 1, 2), v3(1, 1, 1), (Color){120, 90, 60, 255}); - DrawCubeV(v3(2, 2, 2), v3(1, 1, 1), (Color){120, 90, 60, 255}); - DrawCubeV(v3(-3, 1, -1), v3(1, 1, 1), (Color){90, 120, 60, 255}); - DrawCubeV(v3(0, 1, 4), v3(2, 1, 2), (Color){90, 90, 130, 255}); + // Draw smooth terrain with directional lighting + shadows + // For Raylib, we apply lighting by adjusting material colors + Color terrainBaseColor = (Color){80, 140, 70, 255}; + Color litTerrainColor = apply_lighting_with_shadows( + terrainBaseColor, terrainNormal, v3(0, 2, 0), dirLight); - // items (dropped/pickups) + terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color = + litTerrainColor; + DrawModel(terrainModel, (Vector3){0, 0, 0}, 1.0f, WHITE); + + // Items with lighting and shadows 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_ARMORPLATE) - ic = (Color){120, 180, 255, 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}; - DrawCubeV(v3(wi[i].pos.x, wi[i].pos.y + 0.15f, wi[i].pos.z), - v3(0.3f, 0.3f, 0.3f), ic); + + // Apply lighting with shadows to items + Color litColor = + apply_lighting_with_shadows(ic, terrainNormal, wi[i].pos, dirLight); + DrawSphere(wi[i].pos, 0.3f, litColor); } - // players + // Players with lighting and shadows for (int i = 0; i < MAX_PLAYERS; i++) { if (!rp[i].present) continue; @@ -610,29 +836,28 @@ int main(void) { (i == myId) ? (Color){80, 180, 255, 255} : (Color){255, 80, 80, 255}; if (!rp[i].alive) c = (Color){120, 120, 120, 255}; + + // Apply lighting with shadows to player models + Color litPlayerColor = + apply_lighting_with_shadows(c, terrainNormal, 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, c); + 8, litPlayerColor); } EndMode3D(); - // nameplates + // Nameplates for (int i = 0; i < MAX_PLAYERS; i++) { - if (!rp[i].present) + if (!rp[i].present || !rp[i].username[0]) continue; - if (!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; @@ -644,47 +869,49 @@ int main(void) { if (myId == 255 || !rp[myId].present) { DrawText("Connecting...", 10, 10, 20, RAYWHITE); } else { - const char *wname = + 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"; - DrawText(TextFormat("Name: %s | HP: %d | Armor: %d | Weapon: %s", myName, - rp[myId].hp, rp[myId].armor, wname), - 10, 10, 20, RAYWHITE); + 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; - DrawText(TextFormat("Ammo: P %d/%d | R %d/%d (E inv, F pick, G drop, " - "C/V use, R reload, Z/X switch)", - rp[myId].ammoMag[WEAPON_PISTOL], - rp[myId].ammoRes[WEAPON_PISTOL], - rp[myId].ammoMag[WEAPON_RIFLE], - rp[myId].ammoRes[WEAPON_RIFLE]), - 10, 36, 18, (Color){200, 200, 210, 255}); - - if (invOpen) { - DrawRectangle(20, 70, 420, 190, (Color){10, 10, 10, 180}); - DrawRectangleLines(20, 70, 420, 190, (Color){200, 200, 210, 210}); - DrawText("Inventory (slots 1-6)", 32, 80, 22, RAYWHITE); - - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - Color sc = (sidx == selectedSlot) ? (Color){80, 180, 255, 255} - : (Color){220, 220, 230, 255}; - const char *nm = ItemName(rp[myId].invType[sidx]); - int qty = rp[myId].invQty[sidx]; - DrawText(TextFormat("[%d] %s x%d", sidx + 1, nm, qty), 32, - 110 + sidx * 22, 18, sc); - } - DrawText("Use: C or V | Drop: G | Pick: F", 32, - 110 + INV_SLOTS * 22 + 6, 18, (Color){210, 210, 220, 255}); - } + 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 (currentMag == 0) + DrawText("RELOAD!", sw - 250, sh - 30, 20, (Color){255, 80, 80, 255}); } DrawFPS(sw - 90, 10); - // crosshair + // 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); @@ -695,6 +922,7 @@ int main(void) { EndDrawing(); } + UnloadModel(terrainModel); CloseWindow(); CLOSESOCK(sock); #ifdef _WIN32 diff --git a/game/output.md b/game/output.md deleted file mode 100644 index e69de29..0000000 diff --git a/game/run.sh b/game/run.sh new file mode 100755 index 0000000..ecc5d8d --- /dev/null +++ b/game/run.sh @@ -0,0 +1,7 @@ +gcc -O3 client.c -o client -lm -lraylib +gcc -O3 server.c -o server -lm -lraylib -lode + +./server & +server_pid=$! +./client +kill $server_pid diff --git a/game/server.c b/game/server.c index 9b2e6f7..c497376 100644 --- a/game/server.c +++ b/game/server.c @@ -1,7 +1,4 @@ -// server.c - UDP authoritative server + ODE physics + ODE raycast bullets (C99) -// Adds: true spray, inventory capacity (6 slots), world pickups, drop/pick/use, -// armor+medkits. Build (Linux example): gcc server.c -O2 -o server -lode -lm - +// game/server.c - SMOOTH TERRAIN VERSION #include #include #include @@ -26,7 +23,7 @@ typedef int socklen_t; #define CLOSESOCK close #endif -#define PROTOCOL_VERSION 4 +#define PROTOCOL_VERSION 5 #define SERVER_PORT 27015 #define MAX_PLAYERS 16 #define USERNAME_MAX 16 @@ -36,31 +33,141 @@ typedef int socklen_t; #define WEAPON_PISTOL 0 #define WEAPON_RIFLE 1 -#define WEAPON_COUNT 2 -#define INV_SLOTS 6 #define MAX_ITEMS 64 -// inventory item types #define ITEM_NONE 0 #define ITEM_MEDKIT 1 -#define ITEM_ARMORPLATE 2 -#define ITEM_AMMO_PISTOL 3 -#define ITEM_AMMO_RIFLE 4 +#define ITEM_AMMO_PISTOL 2 +#define ITEM_AMMO_RIFLE 3 -// MsgInput.buttons bits #define BTN_RELOAD (1u << 0) -#define BTN_WPN1 (1u << 1) -#define BTN_WPN2 (1u << 2) +#define BTN_SWITCH_PISTOL (1u << 1) +#define BTN_SWITCH_RIFLE (1u << 2) #define BTN_PICK (1u << 3) -#define BTN_DROP (1u << 4) -#define BTN_USE (1u << 5) +#define BTN_USE_MEDKIT (1u << 4) +#define BTN_JUMP (1u << 5) -// Collision categories #define CAT_WORLD (1u << 0) #define CAT_PLAYER (1u << 1) #define CAT_ITEM (1u << 2) +// ============== SIMPLEX NOISE ============== +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 64 +#define TERRAIN_SCALE 2.0f +static dReal *gHeightData = NULL; +static dGeomID gTerrainGeom = NULL; + +static void generate_smooth_terrain(void) { + int size = TERRAIN_SIZE; + gHeightData = (dReal *)malloc(size * size * sizeof(dReal)); + + for (int z = 0; z < size; z++) { + for (int x = 0; x < size; x++) { + float wx = ((float)x - size / 2.0f) * TERRAIN_SCALE; + float wz = ((float)z - size / 2.0f) * TERRAIN_SCALE; + gHeightData[z * size + x] = (dReal)get_terrain_height(wx, wz); + } + } + + printf("Generated smooth heightmap terrain %dx%d\n", size, size); +} +// ============== END SIMPLEX NOISE ============== + static double now_seconds(void) { #ifdef _WIN32 static LARGE_INTEGER freq; @@ -96,8 +203,7 @@ typedef enum MsgType : uint8_t { MSG_INPUT = 3, MSG_SNAPSHOT = 4, MSG_SHOOT = 5, - MSG_HIT = 6, - MSG_ITEMS = 7 + MSG_ITEMS = 6 } MsgType; typedef struct MsgHello { @@ -116,12 +222,8 @@ typedef struct MsgInput { uint8_t type; uint8_t playerId; uint32_t clientTick; - float moveX; - float moveZ; - float yaw; - float pitch; + float moveX, moveZ, yaw, pitch; uint8_t buttons; - uint8_t slotIndex; } MsgInput; typedef struct MsgShoot { @@ -131,20 +233,11 @@ typedef struct MsgShoot { } MsgShoot; typedef struct PlayerStateNet { - uint8_t id; - uint8_t alive; + uint8_t id, alive; int16_t hp; - int16_t armor; - float x, y, z; - float yaw, pitch; - + float x, y, z, yaw, pitch; uint8_t weapon; - int16_t ammoMag[WEAPON_COUNT]; - int16_t ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int16_t invQty[INV_SLOTS]; - + int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; char username[USERNAME_MAX]; } PlayerStateNet; @@ -155,13 +248,6 @@ typedef struct MsgSnapshot { PlayerStateNet p[MAX_PLAYERS]; } MsgSnapshot; -typedef struct MsgHit { - uint8_t type; - uint8_t shooterId; - uint8_t victimId; - int16_t victimHp; -} MsgHit; - typedef struct ItemNet { uint16_t id; uint8_t type; @@ -183,34 +269,20 @@ typedef struct Item { uint8_t type; int qty; float x, y, z; - dGeomID geom; // static pickup geom (no body) + dGeomID geom; } Item; typedef struct Player { int inUse; struct sockaddr_in addr; double lastHeard; - char username[USERNAME_MAX]; - - float moveX, moveZ; - float yaw, pitch; - - int alive; - int hp; - int armor; - - int weapon; - int ammoMag[WEAPON_COUNT]; - int ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int invQty[INV_SLOTS]; - + float moveX, moveZ, yaw, pitch; + int alive, hp, weapon; + int pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; double lastShotTime; int shotsInBurst; uint32_t rng; - dBodyID body; dGeomID geom; } Player; @@ -222,11 +294,7 @@ static int addr_equal(const struct sockaddr_in *a, } static float clampf(float v, float lo, float hi) { - if (v < lo) - return lo; - if (v > hi) - return hi; - return v; + return (v < lo) ? lo : (v > hi) ? hi : v; } static void sendto_player(int sock, const Player *pl, const void *data, @@ -234,6 +302,7 @@ static void sendto_player(int sock, const Player *pl, const void *data, sendto(sock, (const char *)data, len, 0, (const struct sockaddr *)&pl->addr, sizeof(pl->addr)); } + static void broadcast(int sock, const Player players[MAX_PLAYERS], const void *data, int len) { for (int i = 0; i < MAX_PLAYERS; i++) @@ -241,12 +310,9 @@ static void broadcast(int sock, const Player players[MAX_PLAYERS], sendto_player(sock, &players[i], data, len); } -// ----- ODE globals ----- static dWorldID gWorld; static dSpaceID gSpace; static dJointGroupID gContactGroup; - -// items static Item gItems[MAX_ITEMS]; static uint16_t gNextItemId = 1; @@ -257,7 +323,9 @@ static void item_destroy(Item *it) { } memset(it, 0, sizeof(*it)); } -static int item_spawn(uint8_t type, int qty, float x, float y, float z) { + +static int item_spawn(uint8_t type, int qty, float x, float z) { + float y = get_terrain_height(x, z) + 0.5f; for (int i = 0; i < MAX_ITEMS; i++) { if (!gItems[i].active) { Item *it = &gItems[i]; @@ -268,81 +336,23 @@ static int item_spawn(uint8_t type, int qty, float x, float y, float z) { it->x = x; it->y = y; it->z = z; - it->geom = dCreateSphere(gSpace, 0.30); - dGeomSetPosition(it->geom, x, y + 0.3f, z); - - // Mark as item and avoid physical contacts; also make raycasts ignore it. + dGeomSetPosition(it->geom, x, y, z); dGeomSetCategoryBits(it->geom, CAT_ITEM); dGeomSetCollideBits(it->geom, 0); - - // Attach "user data" pointing to Item for identification if needed. dGeomSetData(it->geom, it); - return i; } } return -1; } -static int inv_add(Player *p, uint8_t type, int qty) { - // stacking limits - int maxStack = 99; - if (type == ITEM_MEDKIT) - maxStack = 3; - if (type == ITEM_ARMORPLATE) - maxStack = 5; - - // try stack - for (int i = 0; i < INV_SLOTS; i++) { - if (p->invType[i] == type && p->invQty[i] < maxStack) { - int can = maxStack - p->invQty[i]; - int take = (qty < can) ? qty : can; - p->invQty[i] += take; - qty -= take; - if (qty <= 0) - return 1; - } - } - - // place into empty slots - for (int i = 0; i < INV_SLOTS; i++) { - if (p->invType[i] == ITEM_NONE) { - int take = (qty < maxStack) ? qty : maxStack; - p->invType[i] = type; - p->invQty[i] = take; - qty -= take; - if (qty <= 0) - return 1; - } - } - - // no space - return 0; -} - -static int inv_remove_one(Player *p, int slot) { - if (slot < 0 || slot >= INV_SLOTS) - return 0; - if (p->invType[slot] == ITEM_NONE || p->invQty[slot] <= 0) - return 0; - p->invQty[slot]--; - if (p->invQty[slot] <= 0) { - p->invQty[slot] = 0; - p->invType[slot] = ITEM_NONE; - } - return 1; -} - static void nearCallback(void *user, dGeomID o1, dGeomID o2) { (void)user; - if (dGeomIsSpace(o1) || dGeomIsSpace(o2)) { dSpaceCollide2(o1, o2, user, &nearCallback); return; } - - // Do not create contact joints involving item pickups unsigned long c1 = dGeomGetCategoryBits(o1); unsigned long c2 = dGeomGetCategoryBits(o2); if ((c1 & CAT_ITEM) || (c2 & CAT_ITEM)) @@ -351,19 +361,12 @@ static void nearCallback(void *user, dGeomID o1, dGeomID o2) { const int MAXC = 8; dContact c[MAXC]; int n = dCollide(o1, o2, MAXC, &c[0].geom, sizeof(dContact)); - if (n <= 0) - return; - for (int i = 0; i < n; i++) { c[i].surface.mode = dContactApprox1; - c[i].surface.mu = 1.0; + c[i].surface.mu = 1.5; c[i].surface.bounce = 0.0; - c[i].surface.bounce_vel = 0.0; - dJointID j = dJointCreateContact(gWorld, gContactGroup, &c[i]); - dBodyID b1 = dGeomGetBody(o1); - dBodyID b2 = dGeomGetBody(o2); - dJointAttach(j, b1, b2); + dJointAttach(j, dGeomGetBody(o1), dGeomGetBody(o2)); } } @@ -372,39 +375,35 @@ static void ode_init_world(void) { gWorld = dWorldCreate(); gSpace = dHashSpaceCreate(0); gContactGroup = dJointGroupCreate(0); - - dWorldSetGravity(gWorld, 0, -9.81, 0); + dWorldSetGravity(gWorld, 0, -20.0, 0); dWorldSetERP(gWorld, 0.2); dWorldSetCFM(gWorld, 1e-5); - dGeomID ground = dCreatePlane(gSpace, 0, 1, 0, 0); - dGeomSetCategoryBits(ground, CAT_WORLD); - dGeomSetCollideBits(ground, CAT_PLAYER); + generate_smooth_terrain(); - const struct { - float x, y, z, sx, sy, sz; - } blocks[] = { - {2, 0.5f, 2, 1, 1, 1}, - {2, 1.5f, 2, 1, 1, 1}, - {-3, 0.5f, -1, 1, 1, 1}, - {0, 0.5f, 4, 2, 1, 2}, - }; - for (int i = 0; i < (int)(sizeof(blocks) / sizeof(blocks[0])); i++) { - dGeomID b = dCreateBox(gSpace, blocks[i].sx, blocks[i].sy, blocks[i].sz); - dGeomSetPosition(b, blocks[i].x, blocks[i].y, blocks[i].z); - dGeomSetCategoryBits(b, CAT_WORLD); - dGeomSetCollideBits(b, CAT_PLAYER); - } + dHeightfieldDataID hfData = dGeomHeightfieldDataCreate(); + dGeomHeightfieldDataBuildDouble(hfData, gHeightData, 0, + TERRAIN_SCALE * TERRAIN_SIZE, + TERRAIN_SCALE * TERRAIN_SIZE, TERRAIN_SIZE, + TERRAIN_SIZE, 1.0, 0.0, 0.0, 0); + dGeomHeightfieldDataSetBounds(hfData, -10.0, 20.0); - // spawn some test pickups - item_spawn(ITEM_MEDKIT, 1, 1.0f, 0.0f, 1.0f); - item_spawn(ITEM_MEDKIT, 1, -2.0f, 0.0f, 3.0f); - item_spawn(ITEM_ARMORPLATE, 1, 3.0f, 0.0f, -1.0f); - item_spawn(ITEM_AMMO_RIFLE, 30, 0.0f, 0.0f, 2.0f); - item_spawn(ITEM_AMMO_PISTOL, 12, -1.0f, 0.0f, -2.0f); + gTerrainGeom = dCreateHeightfield(gSpace, hfData, 1); + dGeomSetPosition(gTerrainGeom, 0, 0, 0); + dGeomSetCategoryBits(gTerrainGeom, CAT_WORLD); + dGeomSetCollideBits(gTerrainGeom, CAT_PLAYER); + + item_spawn(ITEM_MEDKIT, 1, 10.0f, 10.0f); + item_spawn(ITEM_MEDKIT, 1, -15.0f, 20.0f); + item_spawn(ITEM_AMMO_RIFLE, 30, 5.0f, -10.0f); + item_spawn(ITEM_AMMO_PISTOL, 12, -10.0f, -15.0f); } static void ode_shutdown_world(void) { + if (gHeightData) { + free(gHeightData); + gHeightData = NULL; + } dJointGroupDestroy(gContactGroup); dSpaceDestroy(gSpace); dWorldDestroy(gWorld); @@ -412,24 +411,20 @@ static void ode_shutdown_world(void) { } static void player_ode_create(Player *p, int spawnIndex) { - const dReal radius = 0.35; - const dReal length = 1.0; - p->body = dBodyCreate(gWorld); - dMass m; - dMassSetCapsuleTotal(&m, 80.0, 3, radius, length); + dMassSetCapsuleTotal(&m, 80.0, 3, 0.35, 1.0); dBodySetMass(p->body, &m); - dBodySetLinearDamping(p->body, 0.05); dBodySetAngularDamping(p->body, 0.99); - dBodySetPosition(p->body, (dReal)(spawnIndex * 2), 2.0, 0.0); - dBodySetAngularVel(p->body, 0, 0, 0); + float sx = (float)(spawnIndex * 4 - 8); + float sz = (float)(spawnIndex * 3 - 6); + float sy = get_terrain_height(sx, sz) + 3.0f; + dBodySetPosition(p->body, sx, sy, sz); - p->geom = dCreateCapsule(gSpace, radius, length); + p->geom = dCreateCapsule(gSpace, 0.35, 1.0); dGeomSetBody(p->geom, p->body); - dGeomSetCategoryBits(p->geom, CAT_PLAYER); dGeomSetCollideBits(p->geom, CAT_WORLD | CAT_PLAYER); } @@ -445,10 +440,8 @@ static void player_ode_destroy(Player *p) { } } -// ---- ODE raycast ---- typedef struct RaycastCtx { - dGeomID ray; - dGeomID hitGeom; + dGeomID ray, hitGeom; dContactGeom hit; int hasHit; } RaycastCtx; @@ -459,7 +452,6 @@ static void rayCallback(void *data, dGeomID o1, dGeomID o2) { dGeomID other = (o1 == ray) ? o2 : o1; if (other == ray) return; - dContact c[4]; int n = dCollide(ray, other, 4, &c[0].geom, sizeof(dContact)); for (int i = 0; i < n; i++) { @@ -478,12 +470,10 @@ static int raycast_space(float ox, float oy, float oz, float dx, float dy, dGeomRaySet(ray, ox, oy, oz, dx, dy, dz); dGeomSetCategoryBits(ray, 0xFFFFFFFFu); dGeomSetCollideBits(ray, CAT_WORLD | CAT_PLAYER); - RaycastCtx rc = {0}; rc.ray = ray; dSpaceCollide2(ray, (dGeomID)gSpace, &rc, &rayCallback); dGeomDestroy(ray); - if (!rc.hasHit) return 0; if (outHit) @@ -493,7 +483,6 @@ static int raycast_space(float ox, float oy, float oz, float dx, float dy, return 1; } -// ---- spray math helpers ---- static uint32_t xorshift32(uint32_t *s) { uint32_t x = *s; x ^= x << 13; @@ -502,21 +491,25 @@ static uint32_t xorshift32(uint32_t *s) { *s = x; return x; } + static float frand_signed(uint32_t *s) { return ((xorshift32(s) & 0xFFFFFF) / (float)0x7FFFFF) - 1.0f; } + static void dir_from_yaw_pitch(float yaw, float pitch, float *dx, float *dy, float *dz) { *dx = sinf(yaw) * cosf(pitch); *dy = sinf(pitch); *dz = cosf(yaw) * cosf(pitch); } + static void v3_cross(float ax, float ay, float az, float bx, float by, float bz, float *ox, float *oy, float *oz) { *ox = ay * bz - az * by; *oy = az * bx - ax * bz; *oz = ax * by - ay * bx; } + static void v3_normf(float *x, float *y, float *z) { float l = sqrtf((*x) * (*x) + (*y) * (*y) + (*z) * (*z)); if (l < 1e-6f) { @@ -537,7 +530,7 @@ static int find_nearest_item(float x, float y, float z, float maxDist) { if (!gItems[i].active) continue; float dx = gItems[i].x - x; - float dy = (gItems[i].y + 0.3f) - y; + float dy = (gItems[i].y) - y; float dz = gItems[i].z - z; float d2 = dx * dx + dy * dy + dz * dz; if (d2 < bestD2) { @@ -575,19 +568,16 @@ int main(void) { } Player players[MAX_PLAYERS] = {0}; - uint32_t serverTick = 0; - uint32_t itemsTickDiv = 0; + uint32_t serverTick = 0, itemsTickDiv = 0; printf("Server listening UDP %d\n", SERVER_PORT); double nextTick = now_seconds(); for (;;) { - // receive 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) { @@ -602,7 +592,6 @@ int main(void) { } uint8_t type = buf[0]; - int pid = -1; for (int i = 0; i < MAX_PLAYERS; i++) { if (players[i].inUse && addr_equal(&players[i].addr, &from)) { @@ -626,26 +615,15 @@ int main(void) { p->addr = from; p->lastHeard = now_seconds(); strncpy(p->username, m->username, USERNAME_MAX - 1); - p->alive = 1; p->hp = 100; - p->armor = 0; - p->weapon = WEAPON_RIFLE; - p->ammoMag[WEAPON_PISTOL] = 12; - p->ammoRes[WEAPON_PISTOL] = 48; - p->ammoMag[WEAPON_RIFLE] = 30; - p->ammoRes[WEAPON_RIFLE] = 90; - - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - p->invType[sidx] = ITEM_NONE; - p->invQty[sidx] = 0; - } - - p->lastShotTime = 0.0; - p->shotsInBurst = 0; + p->pistolMag = 12; + p->rifleMag = 30; + p->pistolAmmo = 48; + p->rifleAmmo = 90; + p->medkits = 2; p->rng = 0xA341316Cu ^ (uint32_t)(i * 2654435761u); - player_ode_create(p, i); printf("Player %d joined as '%s'\n", pid, p->username); break; @@ -667,87 +645,76 @@ int main(void) { if (pid >= 0 && m->playerId == (uint8_t)pid) { Player *p = &players[pid]; p->lastHeard = now_seconds(); - p->moveX = clampf(m->moveX, -1.0f, 1.0f); p->moveZ = clampf(m->moveZ, -1.0f, 1.0f); p->yaw = m->yaw; p->pitch = clampf(m->pitch, -1.5f, 1.5f); - if (m->buttons & BTN_WPN1) + if (m->buttons & BTN_SWITCH_PISTOL) p->weapon = WEAPON_PISTOL; - if (m->buttons & BTN_WPN2) + if (m->buttons & BTN_SWITCH_RIFLE) p->weapon = WEAPON_RIFLE; - // reload if (m->buttons & BTN_RELOAD) { - int wpn = p->weapon; - int magSize = (wpn == WEAPON_PISTOL) ? 12 : 30; - int need = magSize - p->ammoMag[wpn]; - if (need > 0 && p->ammoRes[wpn] > 0) { - int take = (p->ammoRes[wpn] < need) ? p->ammoRes[wpn] : need; - p->ammoRes[wpn] -= take; - p->ammoMag[wpn] += take; + if (p->weapon == WEAPON_PISTOL) { + int need = 12 - p->pistolMag; + if (need > 0 && p->pistolAmmo > 0) { + int take = (p->pistolAmmo < need) ? p->pistolAmmo : need; + p->pistolAmmo -= take; + p->pistolMag += take; + } + } else { + int need = 30 - p->rifleMag; + if (need > 0 && p->rifleAmmo > 0) { + int take = (p->rifleAmmo < need) ? p->rifleAmmo : need; + p->rifleAmmo -= take; + p->rifleMag += take; + } } } - // pick if (m->buttons & BTN_PICK) { const dReal *pos = dBodyGetPosition(p->body); - int itIdx = find_nearest_item((float)pos[0], (float)pos[1] + 0.7f, - (float)pos[2], 1.6f); + int itIdx = find_nearest_item((float)pos[0], (float)pos[1], + (float)pos[2], 2.0f); if (itIdx >= 0) { Item *it = &gItems[itIdx]; int taken = 0; - if (it->type == ITEM_AMMO_PISTOL) { - p->ammoRes[WEAPON_PISTOL] += it->qty; + p->pistolAmmo += it->qty; + if (p->pistolAmmo > 120) + p->pistolAmmo = 120; taken = 1; } else if (it->type == ITEM_AMMO_RIFLE) { - p->ammoRes[WEAPON_RIFLE] += it->qty; + p->rifleAmmo += it->qty; + if (p->rifleAmmo > 300) + p->rifleAmmo = 300; + taken = 1; + } else if (it->type == ITEM_MEDKIT) { + p->medkits += it->qty; + if (p->medkits > 10) + p->medkits = 10; taken = 1; - } else { - taken = inv_add(p, it->type, it->qty); } - if (taken) item_destroy(it); } } - // drop selected slot - if (m->buttons & BTN_DROP) { - int slot = (m->slotIndex < INV_SLOTS) ? (int)m->slotIndex : 0; - uint8_t t = p->invType[slot]; - if (t != ITEM_NONE && p->invQty[slot] > 0) { - const dReal *pos = dBodyGetPosition(p->body); - float ox = (float)pos[0]; - float oy = (float)pos[1]; - float oz = (float)pos[2]; - - // drop 1 unit - if (inv_remove_one(p, slot)) { - item_spawn(t, 1, ox, oy, oz); - } - } - } - - // use selected slot (medkit/armor plate) - if (m->buttons & BTN_USE) { - int slot = (m->slotIndex < INV_SLOTS) ? (int)m->slotIndex : 0; - uint8_t t = p->invType[slot]; - if (t == ITEM_MEDKIT && p->hp < 100 && p->invQty[slot] > 0) { - inv_remove_one(p, slot); + if (m->buttons & BTN_USE_MEDKIT) { + if (p->medkits > 0 && p->hp < 100) { + p->medkits--; p->hp += 25; if (p->hp > 100) p->hp = 100; - } else if (t == ITEM_ARMORPLATE && p->armor < 100 && - p->invQty[slot] > 0) { - inv_remove_one(p, slot); - p->armor += 25; - if (p->armor > 100) - p->armor = 100; } } + + if (m->buttons & BTN_JUMP) { + const dReal *lvel = dBodyGetLinearVel(p->body); + dBodySetLinearVel(p->body, lvel[0], 12.0f, lvel[2]); // Set upward velocity + printf("Player %d jumped\n", pid); + } } } else if (type == MSG_SHOOT && n >= (int)sizeof(MsgShoot)) { const MsgShoot *m = (const MsgShoot *)buf; @@ -755,7 +722,6 @@ int main(void) { players[pid].alive) { Player *sh = &players[pid]; const double now = now_seconds(); - double fireInterval = (sh->weapon == WEAPON_PISTOL) ? (1.0 / 4.0) : (1.0 / 12.0); if (now - sh->lastShotTime < fireInterval) @@ -763,25 +729,24 @@ int main(void) { if (now - sh->lastShotTime > 0.18) sh->shotsInBurst = 0; - int wpn = sh->weapon; - if (sh->ammoMag[wpn] <= 0) + int *mag = + (sh->weapon == WEAPON_PISTOL) ? &sh->pistolMag : &sh->rifleMag; + if (*mag <= 0) continue; - sh->ammoMag[wpn]--; + (*mag)--; sh->shotsInBurst++; sh->lastShotTime = now; const dReal *pos = dBodyGetPosition(sh->body); - float ox = (float)pos[0]; - float oy = (float)pos[1] + 0.7f; - float oz = (float)pos[2]; - + float ox = (float)pos[0], oy = (float)pos[1] + 0.7f, + oz = (float)pos[2]; float fx, fy, fz; dir_from_yaw_pitch(sh->yaw, sh->pitch, &fx, &fy, &fz); v3_normf(&fx, &fy, &fz); - float base = (wpn == WEAPON_PISTOL) ? 0.0035f : 0.0080f; - float grow = (wpn == WEAPON_PISTOL) ? 0.0010f : 0.0025f; + float base = (sh->weapon == WEAPON_PISTOL) ? 0.0035f : 0.0080f; + float grow = (sh->weapon == WEAPON_PISTOL) ? 0.0010f : 0.0025f; float spread = base + grow * (float)sh->shotsInBurst; if (spread > 0.08f) spread = 0.08f; @@ -796,7 +761,6 @@ int main(void) { float ax = frand_signed(&sh->rng) * spread; float ay = frand_signed(&sh->rng) * spread; - float dx = fx + rx * ax + ux * ay; float dy = fy + ry * ax + uy * ay; float dz = fz + rz * ax + uz * ay; @@ -804,7 +768,7 @@ int main(void) { dContactGeom hit; dGeomID hitGeom = 0; - if (raycast_space(ox, oy, oz, dx, dy, dz, 80.0f, &hit, &hitGeom)) { + if (raycast_space(ox, oy, oz, dx, dy, dz, 100.0f, &hit, &hitGeom)) { int victim = -1; for (int i = 0; i < MAX_PLAYERS; i++) { if (!players[i].inUse) @@ -814,151 +778,116 @@ int main(void) { break; } } - if (victim >= 0 && victim != pid && players[victim].alive) { int dmg = 25; - - // armor absorbs first (simple model) - int absorb = - (players[victim].armor < dmg) ? players[victim].armor : dmg; - players[victim].armor -= absorb; - dmg -= absorb; - players[victim].hp -= dmg; if (players[victim].hp <= 0) { players[victim].hp = 0; players[victim].alive = 0; } - - MsgHit ev = {0}; - ev.type = MSG_HIT; - ev.shooterId = (uint8_t)pid; - ev.victimId = (uint8_t)victim; - ev.victimHp = (int16_t)players[victim].hp; - broadcast(sock, players, &ev, (int)sizeof(ev)); } } } } } - // fixed tick double t = now_seconds(); - if (t < nextTick) { -#ifdef _WIN32 - Sleep(1); -#else - struct timespec ts = {0, 1000000}; - nanosleep(&ts, NULL); -#endif - continue; - } - nextTick += (1.0 / (double)TICK_HZ); - serverTick++; + if (t >= nextTick) { + nextTick += DT; + serverTick++; - // timeouts + movement - const float speed = 6.0f; - for (int i = 0; i < MAX_PLAYERS; i++) { - Player *p = &players[i]; - if (!p->inUse) - continue; + dSpaceCollide(gSpace, 0, &nearCallback); + dWorldQuickStep(gWorld, DT); + dJointGroupEmpty(gContactGroup); - if (now_seconds() - p->lastHeard > 10.0) { - printf("Player %d timed out\n", i); - player_ode_destroy(p); - memset(p, 0, sizeof(*p)); - continue; - } - - if (!p->alive) { - dBodySetLinearVel(p->body, 0, 0, 0); - continue; - } - - float cy = cosf(p->yaw), sy = sinf(p->yaw); - float fwdx = sy, fwdz = cy; - float rgtx = cy, rgtz = -sy; - - float vx = (fwdx * p->moveZ + rgtx * p->moveX) * speed; - float vz = (fwdz * p->moveZ + rgtz * p->moveX) * speed; - - const dReal *vcur = dBodyGetLinearVel(p->body); - dBodySetLinearVel(p->body, vx, vcur[1], vz); - - dQuaternion q; - dQFromAxisAndAngle(q, 0, 1, 0, p->yaw); - dBodySetQuaternion(p->body, q); - dBodySetAngularVel(p->body, 0, 0, 0); - } - - // step ODE - dJointGroupEmpty(gContactGroup); - dSpaceCollide(gSpace, 0, &nearCallback); - dWorldQuickStep(gWorld, DT); - - // snapshot players - MsgSnapshot snap = {0}; - snap.type = MSG_SNAPSHOT; - snap.serverTick = serverTick; - snap.count = 0; - - for (int i = 0; i < MAX_PLAYERS; i++) { - if (!players[i].inUse) - continue; - - const dReal *pos = dBodyGetPosition(players[i].body); - PlayerStateNet *ps = &snap.p[snap.count++]; - - ps->id = (uint8_t)i; - ps->alive = (uint8_t)players[i].alive; - ps->hp = (int16_t)players[i].hp; - ps->armor = (int16_t)players[i].armor; - - ps->x = (float)pos[0]; - ps->y = (float)pos[1]; - ps->z = (float)pos[2]; - ps->yaw = players[i].yaw; - ps->pitch = players[i].pitch; - - ps->weapon = (uint8_t)players[i].weapon; - for (int w = 0; w < WEAPON_COUNT; w++) { - ps->ammoMag[w] = (int16_t)players[i].ammoMag[w]; - ps->ammoRes[w] = (int16_t)players[i].ammoRes[w]; - } - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - ps->invType[sidx] = players[i].invType[sidx]; - ps->invQty[sidx] = (int16_t)players[i].invQty[sidx]; - } - - memset(ps->username, 0, USERNAME_MAX); - strncpy(ps->username, players[i].username, USERNAME_MAX - 1); - } - - broadcast(sock, players, &snap, (int)sizeof(MsgSnapshot)); - - // broadcast items at ~10Hz - itemsTickDiv++; - if (itemsTickDiv >= 6) { - itemsTickDiv = 0; - - MsgItems im = {0}; - im.type = MSG_ITEMS; - im.serverTick = serverTick; - im.count = 0; - - for (int i = 0; i < MAX_ITEMS && im.count < MAX_ITEMS; i++) { - if (!gItems[i].active) + for (int i = 0; i < MAX_PLAYERS; i++) { + Player *p = &players[i]; + if (!p->inUse || !p->alive) continue; - ItemNet *on = &im.items[im.count++]; - on->id = gItems[i].id; - on->type = gItems[i].type; - on->qty = (int16_t)gItems[i].qty; - on->x = gItems[i].x; - on->y = gItems[i].y; - on->z = gItems[i].z; + + const dReal *lvel = dBodyGetLinearVel(p->body); + const dReal *avel = dBodyGetAngularVel(p->body); + + float dx = p->moveX, dz = p->moveZ; + float len = sqrtf(dx * dx + dz * dz); + if (len > 1e-6f) { + dx /= len; + dz /= len; + } + + float speed = 6.0f; + float fx = sinf(p->yaw) * dz - cosf(p->yaw) * dx; + float fz = cosf(p->yaw) * dz + sinf(p->yaw) * dx; + dBodySetLinearVel(p->body, fx * speed, lvel[1], fz * speed); + dBodySetAngularVel(p->body, avel[0], 0, avel[2]); } - broadcast(sock, players, &im, (int)sizeof(MsgItems)); + MsgSnapshot snap = {0}; + snap.type = MSG_SNAPSHOT; + snap.serverTick = serverTick; + snap.count = 0; + + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!players[i].inUse) + continue; + const dReal *pos = dBodyGetPosition(players[i].body); + PlayerStateNet *ps = &snap.p[snap.count++]; + ps->id = (uint8_t)i; + ps->alive = players[i].alive; + ps->hp = (int16_t)players[i].hp; + ps->x = (float)pos[0]; + ps->y = (float)pos[1]; + ps->z = (float)pos[2]; + ps->yaw = players[i].yaw; + ps->pitch = players[i].pitch; + ps->weapon = players[i].weapon; + ps->pistolMag = (int16_t)players[i].pistolMag; + ps->rifleMag = (int16_t)players[i].rifleMag; + ps->pistolAmmo = (int16_t)players[i].pistolAmmo; + ps->rifleAmmo = (int16_t)players[i].rifleAmmo; + ps->medkits = (int16_t)players[i].medkits; + strncpy(ps->username, players[i].username, USERNAME_MAX - 1); + } + broadcast(sock, players, &snap, (int)sizeof(snap)); + + itemsTickDiv++; + if (itemsTickDiv >= 6) { + itemsTickDiv = 0; + MsgItems im = {0}; + im.type = MSG_ITEMS; + im.serverTick = serverTick; + im.count = 0; + for (int i = 0; i < MAX_ITEMS; i++) { + if (!gItems[i].active) + continue; + ItemNet *it = &im.items[im.count++]; + it->id = gItems[i].id; + it->type = gItems[i].type; + it->qty = (int16_t)gItems[i].qty; + it->x = gItems[i].x; + it->y = gItems[i].y; + it->z = gItems[i].z; + } + broadcast(sock, players, &im, (int)sizeof(im)); + } + } + + double now = now_seconds(); + for (int i = 0; i < MAX_PLAYERS; i++) { + if (players[i].inUse && (now - players[i].lastHeard) > 10.0) { + printf("Player %d timed out\n", i); + player_ode_destroy(&players[i]); + memset(&players[i], 0, sizeof(Player)); + } + } + + double wait = nextTick - now_seconds(); + if (wait > 0.0) { +#ifdef _WIN32 + Sleep((DWORD)(wait * 1000.0)); +#else + usleep((useconds_t)(wait * 1e6)); +#endif } }