diff --git a/game/client b/game/client new file mode 100755 index 0000000..0c15c78 Binary files /dev/null and b/game/client differ diff --git a/game/client.c b/game/client.c index c7eb9e5..316bb36 100644 --- a/game/client.c +++ b/game/client.c @@ -21,7 +21,7 @@ typedef int socklen_t; #define CLOSESOCK close #endif -#define PROTOCOL_VERSION 5 +#define PROTOCOL_VERSION 67 #define SERVER_PORT 27015 #define MAX_PLAYERS 16 #define USERNAME_MAX 16 @@ -43,7 +43,6 @@ typedef int socklen_t; #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) @@ -53,7 +52,38 @@ typedef struct { float shadowIntensity; // How dark shadows are (0-1) } DirectionalLight; -// ============== SIMPLEX NOISE ============== +typedef struct { + Shader shader; + int locViewPos; + int locLightDir; + int locLightColor; + int locLightIntensity; + int locAmbientIntensity; + int locTerrainColor; +} TerrainShader; + +static TerrainShader load_terrain_shader(void) { + TerrainShader ts = {0}; + ts.shader = LoadShader("terrain.vs", "terrain.fs"); + + // Get uniform locations + ts.locViewPos = GetShaderLocation(ts.shader, "viewPos"); + ts.locLightDir = GetShaderLocation(ts.shader, "lightDir"); + ts.locLightColor = GetShaderLocation(ts.shader, "lightColor"); + ts.locLightIntensity = GetShaderLocation(ts.shader, "lightIntensity"); + ts.locAmbientIntensity = GetShaderLocation(ts.shader, "ambientIntensity"); + ts.locTerrainColor = GetShaderLocation(ts.shader, "terrainColor"); + + return ts; +} + +static void unload_terrain_shader(TerrainShader *ts) { + if (ts && ts->shader.id != 0) { + UnloadShader(ts->shader); + ts->shader.id = 0; + } +} + 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, @@ -143,13 +173,13 @@ static float fbm_noise(float x, float y, int octaves) { } float get_terrain_height(float x, float z) { - float height = fbm_noise(x * 0.03f, z * 0.03f, 4); + 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 * 6.0f + 2.0f; + return height * 4.0f + 2.0f; } -#define TERRAIN_SIZE 64 -#define TERRAIN_SCALE 2.0f +#define TERRAIN_SIZE 256 +#define TERRAIN_SCALE 1.0f static Mesh generate_terrain_mesh(void) { int size = TERRAIN_SIZE; @@ -205,69 +235,53 @@ static Mesh generate_terrain_mesh(void) { } } - // 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; - } + // 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; - 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]; + // 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]; - float v0x = mesh.vertices[i0 * 3 + 0]; - float v0y = mesh.vertices[i0 * 3 + 1]; - float v0z = mesh.vertices[i0 * 3 + 2]; + // 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); - float v1x = mesh.vertices[i1 * 3 + 0]; - float v1y = mesh.vertices[i1 * 3 + 1]; - float v1z = mesh.vertices[i1 * 3 + 2]; + // Normal from height field: (-dh/dx, 1, -dh/dz) then normalized + float nx = -dh_dx; + float ny = 1.0f; + float nz = -dh_dz; - 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; + float len = sqrtf(nx * nx + ny * ny + nz * nz); + if (len > 0.0001f) { + mesh.normals[idx * 3 + 0] = nx / len; + mesh.normals[idx * 3 + 1] = ny / len; + mesh.normals[idx * 3 + 2] = nz / len; + } else { + mesh.normals[idx * 3 + 0] = 0; + mesh.normals[idx * 3 + 1] = 1; + mesh.normals[idx * 3 + 2] = 0; + } } } UploadMesh(&mesh, false); return mesh; } -// ============== END SIMPLEX NOISE ============== -// ============== LIGHTING HELPERS ============== // apply_directional_lighting: Basic Lambertian diffuse lighting // Applies directional light with diffuse component based on surface normal static Color apply_directional_lighting(Color baseColor, Vector3 normal, @@ -369,7 +383,117 @@ static Vector3 v3_normalize(Vector3 v) { return (Vector3){0, 1, 0}; return (Vector3){v.x / len, v.y / len, v.z / len}; } -// ============== END LIGHTING HELPERS ============== + +static Vector3 v3(float x, float y, float z) { + Vector3 v = {x, y, z}; + return v; +} +static Vector3 v3_add(Vector3 a, Vector3 b) { + return v3(a.x + b.x, a.y + b.y, a.z + b.z); +} +static Vector3 v3_sub(Vector3 a, Vector3 b) { + return v3(a.x - b.x, a.y - b.y, a.z - b.z); +} +static float v3_dot(Vector3 a, Vector3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} +static float v3_len(Vector3 a) { + return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z); +} +static Vector3 v3_norm(Vector3 a) { + float l = v3_len(a); + if (l <= 1e-6f) + return v3(0, 0, 1); + return v3(a.x / l, a.y / l, a.z / l); +} + +// 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 @@ -388,7 +512,8 @@ typedef enum MsgType : uint8_t { MSG_INPUT = 3, MSG_SNAPSHOT = 4, MSG_SHOOT = 5, - MSG_ITEMS = 6 + MSG_ITEMS = 6, + MSG_ROOM_STATE = 7 } MsgType; typedef struct MsgHello { @@ -423,6 +548,7 @@ typedef struct PlayerStateNet { float x, y, z, yaw, pitch; uint8_t weapon; int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; + int16_t reloadTimeLeft; char username[USERNAME_MAX]; } PlayerStateNet; @@ -446,6 +572,14 @@ typedef struct MsgItems { 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 { @@ -454,6 +588,7 @@ typedef struct RemotePlayer { float yaw, pitch; uint8_t weapon; int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; + int16_t reloadTimeLeft; char username[USERNAME_MAX]; } RemotePlayer; @@ -465,29 +600,6 @@ typedef struct WorldItem { Vector3 pos; } WorldItem; -static Vector3 v3(float x, float y, float z) { - Vector3 v = {x, y, z}; - return v; -} -static Vector3 v3_add(Vector3 a, Vector3 b) { - return v3(a.x + b.x, a.y + b.y, a.z + b.z); -} -static Vector3 v3_sub(Vector3 a, Vector3 b) { - return v3(a.x - b.x, a.y - b.y, a.z - b.z); -} -static float v3_dot(Vector3 a, Vector3 b) { - return a.x * b.x + a.y * b.y + a.z * b.z; -} -static float v3_len(Vector3 a) { - return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z); -} -static Vector3 v3_norm(Vector3 a) { - float l = v3_len(a); - if (l <= 1e-6f) - return v3(0, 0, 1); - return v3(a.x / l, a.y / l, a.z / l); -} - static const char *ItemName(uint8_t t) { switch (t) { case ITEM_MEDKIT: @@ -565,6 +677,20 @@ int main(void) { 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"); @@ -583,10 +709,16 @@ int main(void) { 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; float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f; + int scoped = 0; float fireCooldown = 0.0f; int shotsInBurst = 0; float burstResetTimer = 0.0f; @@ -673,6 +805,7 @@ int main(void) { 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); } @@ -682,6 +815,10 @@ int main(void) { camPos.x = body.x; camPos.y = body.y + 1.0f; camPos.z = body.z + 0.0001f; + + // Prevent camera from clipping into terrain + float terrain_h = get_terrain_height(camPos.x, camPos.z); + camPos.y = fmaxf(camPos.y, terrain_h + 1.5f); } } else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) { MsgItems *m = (MsgItems *)buf; @@ -695,6 +832,14 @@ int main(void) { 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); + } } } @@ -737,6 +882,8 @@ int main(void) { buttons |= BTN_USE_MEDKIT; if (IsKeyPressed(KEY_SPACE)) buttons |= BTN_JUMP; + if (IsKeyPressed(KEY_Z)) + scoped = !scoped; if (myId != 255) { MsgInput in = {0}; @@ -788,7 +935,7 @@ int main(void) { cam.position = camPos; cam.target = v3_add(camPos, forward); cam.up = v3(0, 1, 0); - cam.fovy = 75.0f; + cam.fovy = scoped ? 30.0f : 75.0f; cam.projection = CAMERA_PERSPECTIVE; BeginDrawing(); @@ -796,20 +943,41 @@ int main(void) { BeginMode3D(cam); - // Calculate terrain normal at camera position for lighting - Vector3 terrainNormal = v3(0, 1, 0); // Default up normal + // 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}; - // 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); + // 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 = - litTerrainColor; + (Color){80, 140, 70, 255}; DrawModel(terrainModel, (Vector3){0, 0, 0}, 1.0f, WHITE); - // Items with lighting and shadows + // 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; @@ -821,13 +989,14 @@ int main(void) { if (wi[i].type == ITEM_AMMO_RIFLE) ic = (Color){255, 180, 120, 255}; - // Apply lighting with shadows to items + // Apply basic lighting to items + Vector3 itemNormal = v3(0, 1, 0); Color litColor = - apply_lighting_with_shadows(ic, terrainNormal, wi[i].pos, dirLight); + apply_lighting_with_shadows(ic, itemNormal, wi[i].pos, dirLight); DrawSphere(wi[i].pos, 0.3f, litColor); } - // Players with lighting and shadows + // Players with basic lighting for (int i = 0; i < MAX_PLAYERS; i++) { if (!rp[i].present) continue; @@ -837,9 +1006,10 @@ int main(void) { if (!rp[i].alive) c = (Color){120, 120, 120, 255}; - // Apply lighting with shadows to player models + // Apply basic lighting to players + Vector3 playerNormal = v3(0, 1, 0); Color litPlayerColor = - apply_lighting_with_shadows(c, terrainNormal, p, dirLight); + 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); } @@ -900,29 +1070,58 @@ int main(void) { 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) + 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, 300, 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, 300, 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, 400, 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 = 6 + (int)crossSpread; - int len = 10, thick = 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, 3.0f, (Color){240, 240, 245, 160}); + 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 diff --git a/game/libfishsoup b/game/libfishsoup new file mode 120000 index 0000000..9b31e29 --- /dev/null +++ b/game/libfishsoup @@ -0,0 +1 @@ +../libfishsoup \ No newline at end of file diff --git a/game/server b/game/server new file mode 100755 index 0000000..03a0748 Binary files /dev/null and b/game/server differ diff --git a/game/server.c b/game/server.c index c497376..b3c61b9 100644 --- a/game/server.c +++ b/game/server.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -23,12 +24,12 @@ typedef int socklen_t; #define CLOSESOCK close #endif -#define PROTOCOL_VERSION 5 +#define PROTOCOL_VERSION 67 #define SERVER_PORT 27015 #define MAX_PLAYERS 16 #define USERNAME_MAX 16 -#define TICK_HZ 60 +#define TICK_HZ 20 #define DT (1.0f / (float)TICK_HZ) #define WEAPON_PISTOL 0 @@ -48,11 +49,15 @@ typedef int socklen_t; #define BTN_USE_MEDKIT (1u << 4) #define BTN_JUMP (1u << 5) +#define ROOM_STATE_WAITING 0 +#define ROOM_STATE_COUNTING_DOWN 1 +#define ROOM_STATE_RUNNING 2 +#define ROOM_STATE_FINISHED 3 + #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, @@ -142,13 +147,13 @@ static float fbm_noise(float x, float y, int octaves) { } float get_terrain_height(float x, float z) { - float height = fbm_noise(x * 0.03f, z * 0.03f, 4); + 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 * 6.0f + 2.0f; + return height * 4.0f + 2.0f; } -#define TERRAIN_SIZE 64 -#define TERRAIN_SCALE 2.0f +#define TERRAIN_SIZE 256 +#define TERRAIN_SCALE 1.0f static dReal *gHeightData = NULL; static dGeomID gTerrainGeom = NULL; @@ -166,7 +171,6 @@ static void generate_smooth_terrain(void) { printf("Generated smooth heightmap terrain %dx%d\n", size, size); } -// ============== END SIMPLEX NOISE ============== static double now_seconds(void) { #ifdef _WIN32 @@ -203,7 +207,8 @@ typedef enum MsgType : uint8_t { MSG_INPUT = 3, MSG_SNAPSHOT = 4, MSG_SHOOT = 5, - MSG_ITEMS = 6 + MSG_ITEMS = 6, + MSG_ROOM_STATE = 7 } MsgType; typedef struct MsgHello { @@ -238,6 +243,7 @@ typedef struct PlayerStateNet { float x, y, z, yaw, pitch; uint8_t weapon; int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits; + int16_t reloadTimeLeft; char username[USERNAME_MAX]; } PlayerStateNet; @@ -261,6 +267,14 @@ typedef struct MsgItems { 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 Item { @@ -283,10 +297,19 @@ typedef struct Player { double lastShotTime; int shotsInBurst; uint32_t rng; + float reloadTimer; + int reloadWeapon; dBodyID body; dGeomID geom; } Player; +typedef struct RoomState { + int state; + double countdownEndTime; + int winnerId; + int playerCount; +} RoomState; + static int addr_equal(const struct sockaddr_in *a, const struct sockaddr_in *b) { return (a->sin_family == b->sin_family) && (a->sin_port == b->sin_port) && @@ -315,6 +338,7 @@ static dSpaceID gSpace; static dJointGroupID gContactGroup; static Item gItems[MAX_ITEMS]; static uint16_t gNextItemId = 1; +static RoomState gRoomState; static void item_destroy(Item *it) { if (it->geom) { @@ -393,10 +417,25 @@ static void ode_init_world(void) { 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); + // Spawn initial random loot + for (int i = 0; i < 4; i++) { + float x = (float)(rand() % 200 - 100); + float z = (float)(rand() % 200 - 100); + uint8_t type; + int qty; + int type_idx = rand() % 3; + if (type_idx == 0) { + type = ITEM_MEDKIT; + qty = 1; + } else if (type_idx == 1) { + type = ITEM_AMMO_PISTOL; + qty = 10 + (rand() % 6); + } else { + type = ITEM_AMMO_RIFLE; + qty = 20 + (rand() % 11); + } + item_spawn(type, qty, x, z); + } } static void ode_shutdown_world(void) { @@ -418,8 +457,9 @@ static void player_ode_create(Player *p, int spawnIndex) { dBodySetLinearDamping(p->body, 0.05); dBodySetAngularDamping(p->body, 0.99); - float sx = (float)(spawnIndex * 4 - 8); - float sz = (float)(spawnIndex * 3 - 6); + // Random spawn position + float sx = (float)(rand() % 200 - 100); // -100 to 99 + float sz = (float)(rand() % 200 - 100); float sy = get_terrain_height(sx, sz) + 3.0f; dBodySetPosition(p->body, sx, sy, sz); @@ -547,6 +587,14 @@ int main(void) { WSAStartup(MAKEWORD(2, 2), &wsa); #endif + srand((unsigned)time(NULL)); + + // Initialize room state + gRoomState.state = ROOM_STATE_WAITING; + gRoomState.countdownEndTime = 0.0; + gRoomState.winnerId = -1; + gRoomState.playerCount = 0; + ode_init_world(); int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); @@ -568,7 +616,7 @@ int main(void) { } Player players[MAX_PLAYERS] = {0}; - uint32_t serverTick = 0, itemsTickDiv = 0; + uint32_t serverTick = 0, itemsTickDiv = 0, spawnCounter = 0; printf("Server listening UDP %d\n", SERVER_PORT); double nextTick = now_seconds(); @@ -606,6 +654,9 @@ int main(void) { continue; if (pid < 0) { + // Only allow joining if room is not running + if (gRoomState.state == ROOM_STATE_RUNNING) + continue; for (int i = 0; i < MAX_PLAYERS; i++) if (!players[i].inUse) { pid = i; @@ -625,6 +676,7 @@ int main(void) { p->medkits = 2; p->rng = 0xA341316Cu ^ (uint32_t)(i * 2654435761u); player_ode_create(p, i); + gRoomState.playerCount++; printf("Player %d joined as '%s'\n", pid, p->username); break; } @@ -656,20 +708,9 @@ int main(void) { p->weapon = WEAPON_RIFLE; if (m->buttons & BTN_RELOAD) { - 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; - } + if (p->reloadTimer <= 0.0f) { + p->reloadTimer = 2.0f; // 2 second reload time + p->reloadWeapon = p->weapon; } } @@ -712,14 +753,15 @@ int main(void) { if (m->buttons & BTN_JUMP) { const dReal *lvel = dBodyGetLinearVel(p->body); - dBodySetLinearVel(p->body, lvel[0], 12.0f, lvel[2]); // Set upward velocity + 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; if (pid >= 0 && m->playerId == (uint8_t)pid && players[pid].inUse && - players[pid].alive) { + players[pid].alive && gRoomState.state == ROOM_STATE_RUNNING) { Player *sh = &players[pid]; const double now = now_seconds(); double fireInterval = @@ -729,6 +771,9 @@ int main(void) { if (now - sh->lastShotTime > 0.18) sh->shotsInBurst = 0; + if (sh->reloadTimer > 0.0f) + continue; + int *mag = (sh->weapon == WEAPON_PISTOL) ? &sh->pistolMag : &sh->rifleMag; if (*mag <= 0) @@ -784,6 +829,37 @@ int main(void) { if (players[victim].hp <= 0) { players[victim].hp = 0; players[victim].alive = 0; + // Drop loot + const dReal *pos = dBodyGetPosition(players[victim].body); + float x = (float)pos[0], z = (float)pos[2]; + if (players[victim].pistolAmmo > 0) { + int qty = players[victim].pistolAmmo; + if (qty > 12) + qty = 12; // Max per drop + item_spawn(ITEM_AMMO_PISTOL, qty, + x + frand_signed(&players[victim].rng) * 2.0f, + z + frand_signed(&players[victim].rng) * 2.0f); + players[victim].pistolAmmo -= qty; + } + if (players[victim].rifleAmmo > 0) { + int qty = players[victim].rifleAmmo; + if (qty > 30) + qty = 30; + item_spawn(ITEM_AMMO_RIFLE, qty, + x + frand_signed(&players[victim].rng) * 2.0f, + z + frand_signed(&players[victim].rng) * 2.0f); + players[victim].rifleAmmo -= qty; + } + if (players[victim].medkits > 0) { + int qty = players[victim].medkits; + if (qty > 2) + qty = 2; + item_spawn(ITEM_MEDKIT, qty, + x + frand_signed(&players[victim].rng) * 2.0f, + z + frand_signed(&players[victim].rng) * 2.0f); + players[victim].medkits -= qty; + } + printf("Player %d died and dropped loot\n", victim); } } } @@ -791,15 +867,128 @@ int main(void) { } } + // Room state management + { + double now = now_seconds(); + if (gRoomState.state == ROOM_STATE_WAITING) { + // Count active players + int activePlayers = 0; + for (int i = 0; i < MAX_PLAYERS; i++) { + if (players[i].inUse) + activePlayers++; + } + gRoomState.playerCount = activePlayers; + if (activePlayers >= 2) { + gRoomState.state = ROOM_STATE_COUNTING_DOWN; + gRoomState.countdownEndTime = now + 10.0; // 10 seconds + printf("Starting countdown for game start\n"); + } + } else if (gRoomState.state == ROOM_STATE_COUNTING_DOWN) { + if (now >= gRoomState.countdownEndTime) { + gRoomState.state = ROOM_STATE_RUNNING; + // Respawn and heal all players + for (int i = 0; i < MAX_PLAYERS; i++) { + if (players[i].inUse) { + players[i].alive = 1; + players[i].hp = 100; + player_ode_create(&players[i], i); // Recreate body + } + } + printf("Game started!\n"); + } + } else if (gRoomState.state == ROOM_STATE_RUNNING) { + // Check win condition + int aliveCount = 0; + int lastAlive = -1; + for (int i = 0; i < MAX_PLAYERS; i++) { + if (players[i].inUse && players[i].alive) { + aliveCount++; + lastAlive = i; + } + } + if (aliveCount <= 1) { + gRoomState.state = ROOM_STATE_FINISHED; + gRoomState.winnerId = lastAlive; + printf("Game finished! Winner: %s\n", + lastAlive >= 0 ? players[lastAlive].username : "None"); + } + } else if (gRoomState.state == ROOM_STATE_FINISHED) { + // Wait a bit, then reset + static double finishTime = 0.0; + if (finishTime == 0.0) + finishTime = now; + if (now - finishTime > 5.0) { // 5 seconds after finish + gRoomState.state = ROOM_STATE_WAITING; + gRoomState.winnerId = -1; + finishTime = 0.0; + // Remove dead players or something? For now, keep them + printf("Room resetting to waiting\n"); + } + } + } + double t = now_seconds(); if (t >= nextTick) { nextTick += DT; serverTick++; + spawnCounter++; dSpaceCollide(gSpace, 0, &nearCallback); dWorldQuickStep(gWorld, DT); dJointGroupEmpty(gContactGroup); + // Handle reload timers + for (int i = 0; i < MAX_PLAYERS; i++) { + Player *p = &players[i]; + if (!p->inUse || !p->alive) + continue; + if (p->reloadTimer > 0.0f) { + p->reloadTimer -= DT; + if (p->reloadTimer <= 0.0f) { + p->reloadTimer = 0.0f; + // Perform reload + if (p->reloadWeapon == 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; + } + } + } + } + } + + // Spawn random items every 30 seconds (900 ticks at 30Hz) + if (spawnCounter >= 900) { + spawnCounter = 0; + for (int i = 0; i < 2; i++) { + int type_idx = rand() % 3; + uint8_t type; + int qty; + if (type_idx == 0) { + type = ITEM_MEDKIT; + qty = 1; + } else if (type_idx == 1) { + type = ITEM_AMMO_PISTOL; + qty = 10 + (rand() % 6); + } else { + type = ITEM_AMMO_RIFLE; + qty = 20 + (rand() % 11); + } + float x = (float)(rand() % 200 - 100); + float z = (float)(rand() % 200 - 100); + item_spawn(type, qty, x, z); + } + } + for (int i = 0; i < MAX_PLAYERS; i++) { Player *p = &players[i]; if (!p->inUse || !p->alive) @@ -815,7 +1004,7 @@ int main(void) { dz /= len; } - float speed = 6.0f; + float speed = 10.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); @@ -846,6 +1035,7 @@ int main(void) { ps->pistolAmmo = (int16_t)players[i].pistolAmmo; ps->rifleAmmo = (int16_t)players[i].rifleAmmo; ps->medkits = (int16_t)players[i].medkits; + ps->reloadTimeLeft = (int16_t)(players[i].reloadTimer * 1000.0f); strncpy(ps->username, players[i].username, USERNAME_MAX - 1); } broadcast(sock, players, &snap, (int)sizeof(snap)); @@ -870,6 +1060,27 @@ int main(void) { } broadcast(sock, players, &im, (int)sizeof(im)); } + + // Send room state every few ticks + static uint32_t roomStateTickDiv = 0; + roomStateTickDiv++; + if (roomStateTickDiv >= 30) { // Every 30 ticks, about 0.5s + roomStateTickDiv = 0; + MsgRoomState rs = {0}; + rs.type = MSG_ROOM_STATE; + rs.state = (uint8_t)gRoomState.state; + rs.countdownRemaining = + (gRoomState.state == ROOM_STATE_COUNTING_DOWN) + ? (float)(gRoomState.countdownEndTime - now_seconds()) + : 0.0f; + rs.winnerId = + (gRoomState.winnerId >= 0) ? (uint8_t)gRoomState.winnerId : 255; + if (gRoomState.winnerId >= 0) { + strncpy(rs.winnerName, players[gRoomState.winnerId].username, + USERNAME_MAX - 1); + } + broadcast(sock, players, &rs, (int)sizeof(rs)); + } } double now = now_seconds(); diff --git a/game/terrain.fs b/game/terrain.fs new file mode 100644 index 0000000..fae6101 --- /dev/null +++ b/game/terrain.fs @@ -0,0 +1,86 @@ +#version 330 + +// Input from vertex shader +in vec3 fragPosition; +in vec3 fragNormal; +in vec2 fragTexCoord; + +// Uniforms +uniform sampler2D texture0; +uniform vec3 viewPos; +uniform vec3 lightDir; +uniform vec3 lightColor; +uniform float lightIntensity; +uniform float ambientIntensity; +uniform vec3 terrainColor; + +// Output +out vec4 finalColor; + +// Simplex noise for AO calculation +float hash(float n) { + return fract(sin(n) * 43758.5453123); +} + +float noise(vec3 p) { + vec3 i = floor(p); + vec3 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + + float n = i.x + i.y * 157.0 + i.z * 113.0; + return mix( + mix(mix(hash(n), hash(n + 1.0), f.x), + mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y), + mix(mix(hash(n + 113.0), hash(n + 114.0), f.x), + mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), + f.z + ); +} + +// Calculate ambient occlusion with more contrast +float calculateAO(vec3 position, vec3 normal) { + // Sample 8 directions around the surface + float ao = 0.0; + float radius = 2.5; + int samples = 8; + + for (int i = 0; i < samples; i++) { + float angle = 6.28318530718 * float(i) / float(samples); + vec3 offset = vec3(cos(angle) * radius, noise(position * 0.5 + float(i)), sin(angle) * radius); + + // Sample height variation (approximated via noise) + float heightVariation = noise((position + offset) * 0.3); + + // Calculate occlusion based on height and normal + float dotProduct = max(0.0, dot(normal, normalize(offset))); + ao += mix(0.0, dotProduct * heightVariation, 0.7); + } + + // Increased contrast: 0.8 multiplier instead of 0.6 + ao = 1.0 - (ao / float(samples)) * 0.8; + // Min 0.1 (darker shadows) instead of 0.2 + return max(0.1, ao); +} + +void main() +{ + // Normalize interpolated normal + vec3 norm = normalize(fragNormal); + + // Use normal Y component to create geometry-revealing green shading + // Higher Y normal (upward-facing) = brighter green (peaks/flat areas) + // Lower Y normal (sloped) = darker green (slopes/valleys) + float brightness = (norm.y + 1.0) * 0.5; // Map -1..1 to 0..1 + + // Increase contrast: darker minimum, brighter maximum + // Old range: 0.4 to 1.0 (0.6 total range) + // New range: 0.2 to 1.2 (1.0 total range) - much more contrast + float contrastMultiplier = 0.2 + brightness * 1.0; + + // Create base terrain color modulated by geometry + vec3 baseColor = terrainColor; + vec3 finalColorRGB = baseColor * contrastMultiplier; + + finalColor = vec4(finalColorRGB, 1.0); +} + diff --git a/game/terrain.vs b/game/terrain.vs new file mode 100644 index 0000000..6764ee3 --- /dev/null +++ b/game/terrain.vs @@ -0,0 +1,31 @@ +#version 330 + +// Input vertex attributes +in vec3 vertexPosition; +in vec3 vertexNormal; +in vec2 vertexTexCoord; + +// Uniforms +uniform mat4 mvp; +uniform mat4 matModel; +uniform mat4 matNormal; + +// Output to fragment shader +out vec3 fragPosition; +out vec3 fragNormal; +out vec2 fragTexCoord; + +void main() +{ + // Transform vertex position to world space + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); + + // Transform normal to world space + fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 0.0))); + + // Pass texture coordinates + fragTexCoord = vertexTexCoord; + + // Transform to clip space + gl_Position = mvp * vec4(vertexPosition, 1.0); +}