Compare commits
8 commits
0151eb65ef
...
57d8c7a2b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d8c7a2b7 | |||
| d1782e1265 | |||
| 7b8e798662 | |||
| de14cb8f14 | |||
| 7fc4e9cd3c | |||
| 6b8564cb0c | |||
| a092c10017 | |||
| 75ee5a6da8 |
7 changed files with 668 additions and 140 deletions
BIN
game/client
Executable file
BIN
game/client
Executable file
Binary file not shown.
405
game/client.c
405
game/client.c
|
|
@ -21,7 +21,7 @@ typedef int socklen_t;
|
||||||
#define CLOSESOCK close
|
#define CLOSESOCK close
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define PROTOCOL_VERSION 5
|
#define PROTOCOL_VERSION 67
|
||||||
#define SERVER_PORT 27015
|
#define SERVER_PORT 27015
|
||||||
#define MAX_PLAYERS 16
|
#define MAX_PLAYERS 16
|
||||||
#define USERNAME_MAX 16
|
#define USERNAME_MAX 16
|
||||||
|
|
@ -43,7 +43,6 @@ typedef int socklen_t;
|
||||||
#define BTN_USE_MEDKIT (1u << 4)
|
#define BTN_USE_MEDKIT (1u << 4)
|
||||||
#define BTN_JUMP (1u << 5)
|
#define BTN_JUMP (1u << 5)
|
||||||
|
|
||||||
// ============== DIRECTIONAL LIGHT ==============
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
Vector3 direction; // Normalized light direction
|
Vector3 direction; // Normalized light direction
|
||||||
Vector3 color; // RGB color (0-1 range)
|
Vector3 color; // RGB color (0-1 range)
|
||||||
|
|
@ -53,7 +52,38 @@ typedef struct {
|
||||||
float shadowIntensity; // How dark shadows are (0-1)
|
float shadowIntensity; // How dark shadows are (0-1)
|
||||||
} DirectionalLight;
|
} 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] = {
|
static const uint8_t perm[512] = {
|
||||||
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7,
|
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,
|
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 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;
|
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_SIZE 256
|
||||||
#define TERRAIN_SCALE 2.0f
|
#define TERRAIN_SCALE 1.0f
|
||||||
|
|
||||||
static Mesh generate_terrain_mesh(void) {
|
static Mesh generate_terrain_mesh(void) {
|
||||||
int size = TERRAIN_SIZE;
|
int size = TERRAIN_SIZE;
|
||||||
|
|
@ -205,69 +235,53 @@ static Mesh generate_terrain_mesh(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate normals
|
// Calculate normals using tangent plane approximation from terrain gradients
|
||||||
for (int i = 0; i < vertexCount; i++) {
|
// This preserves terrain curvature better than simple triangle averaging
|
||||||
mesh.normals[i * 3 + 0] = 0;
|
for (int z = 0; z < size; z++) {
|
||||||
mesh.normals[i * 3 + 1] = 1;
|
for (int x = 0; x < size; x++) {
|
||||||
mesh.normals[i * 3 + 2] = 0;
|
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++) {
|
// Sample height gradients to compute terrain normal
|
||||||
int i0 = mesh.indices[t * 3 + 0];
|
// Use neighboring vertices for finite difference approximation
|
||||||
int i1 = mesh.indices[t * 3 + 1];
|
float h_right = (x + 1 < size)
|
||||||
int i2 = mesh.indices[t * 3 + 2];
|
? 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];
|
// Compute finite differences
|
||||||
float v0y = mesh.vertices[i0 * 3 + 1];
|
float dh_dx = (h_right - h_left) / (2.0f * TERRAIN_SCALE);
|
||||||
float v0z = mesh.vertices[i0 * 3 + 2];
|
float dh_dz = (h_down - h_up) / (2.0f * TERRAIN_SCALE);
|
||||||
|
|
||||||
float v1x = mesh.vertices[i1 * 3 + 0];
|
// Normal from height field: (-dh/dx, 1, -dh/dz) then normalized
|
||||||
float v1y = mesh.vertices[i1 * 3 + 1];
|
float nx = -dh_dx;
|
||||||
float v1z = mesh.vertices[i1 * 3 + 2];
|
float ny = 1.0f;
|
||||||
|
float nz = -dh_dz;
|
||||||
|
|
||||||
float v2x = mesh.vertices[i2 * 3 + 0];
|
float len = sqrtf(nx * nx + ny * ny + nz * nz);
|
||||||
float v2y = mesh.vertices[i2 * 3 + 1];
|
if (len > 0.0001f) {
|
||||||
float v2z = mesh.vertices[i2 * 3 + 2];
|
mesh.normals[idx * 3 + 0] = nx / len;
|
||||||
|
mesh.normals[idx * 3 + 1] = ny / len;
|
||||||
float e1x = v1x - v0x, e1y = v1y - v0y, e1z = v1z - v0z;
|
mesh.normals[idx * 3 + 2] = nz / len;
|
||||||
float e2x = v2x - v0x, e2y = v2y - v0y, e2z = v2z - v0z;
|
} else {
|
||||||
|
mesh.normals[idx * 3 + 0] = 0;
|
||||||
float nx = e1y * e2z - e1z * e2y;
|
mesh.normals[idx * 3 + 1] = 1;
|
||||||
float ny = e1z * e2x - e1x * e2z;
|
mesh.normals[idx * 3 + 2] = 0;
|
||||||
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);
|
UploadMesh(&mesh, false);
|
||||||
return mesh;
|
return mesh;
|
||||||
}
|
}
|
||||||
// ============== END SIMPLEX NOISE ==============
|
|
||||||
|
|
||||||
// ============== LIGHTING HELPERS ==============
|
|
||||||
// apply_directional_lighting: Basic Lambertian diffuse lighting
|
// apply_directional_lighting: Basic Lambertian diffuse lighting
|
||||||
// Applies directional light with diffuse component based on surface normal
|
// Applies directional light with diffuse component based on surface normal
|
||||||
static Color apply_directional_lighting(Color baseColor, Vector3 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){0, 1, 0};
|
||||||
return (Vector3){v.x / len, v.y / len, v.z / len};
|
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) {
|
static void set_nonblocking(int sock) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
@ -388,7 +512,8 @@ typedef enum MsgType : uint8_t {
|
||||||
MSG_INPUT = 3,
|
MSG_INPUT = 3,
|
||||||
MSG_SNAPSHOT = 4,
|
MSG_SNAPSHOT = 4,
|
||||||
MSG_SHOOT = 5,
|
MSG_SHOOT = 5,
|
||||||
MSG_ITEMS = 6
|
MSG_ITEMS = 6,
|
||||||
|
MSG_ROOM_STATE = 7
|
||||||
} MsgType;
|
} MsgType;
|
||||||
|
|
||||||
typedef struct MsgHello {
|
typedef struct MsgHello {
|
||||||
|
|
@ -423,6 +548,7 @@ typedef struct PlayerStateNet {
|
||||||
float x, y, z, yaw, pitch;
|
float x, y, z, yaw, pitch;
|
||||||
uint8_t weapon;
|
uint8_t weapon;
|
||||||
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
|
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
|
||||||
|
int16_t reloadTimeLeft;
|
||||||
char username[USERNAME_MAX];
|
char username[USERNAME_MAX];
|
||||||
} PlayerStateNet;
|
} PlayerStateNet;
|
||||||
|
|
||||||
|
|
@ -446,6 +572,14 @@ typedef struct MsgItems {
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
ItemNet items[MAX_ITEMS];
|
ItemNet items[MAX_ITEMS];
|
||||||
} MsgItems;
|
} MsgItems;
|
||||||
|
|
||||||
|
typedef struct MsgRoomState {
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t state;
|
||||||
|
float countdownRemaining;
|
||||||
|
uint8_t winnerId;
|
||||||
|
char winnerName[USERNAME_MAX];
|
||||||
|
} MsgRoomState;
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
typedef struct RemotePlayer {
|
typedef struct RemotePlayer {
|
||||||
|
|
@ -454,6 +588,7 @@ typedef struct RemotePlayer {
|
||||||
float yaw, pitch;
|
float yaw, pitch;
|
||||||
uint8_t weapon;
|
uint8_t weapon;
|
||||||
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
|
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
|
||||||
|
int16_t reloadTimeLeft;
|
||||||
char username[USERNAME_MAX];
|
char username[USERNAME_MAX];
|
||||||
} RemotePlayer;
|
} RemotePlayer;
|
||||||
|
|
||||||
|
|
@ -465,29 +600,6 @@ typedef struct WorldItem {
|
||||||
Vector3 pos;
|
Vector3 pos;
|
||||||
} WorldItem;
|
} 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) {
|
static const char *ItemName(uint8_t t) {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case ITEM_MEDKIT:
|
case ITEM_MEDKIT:
|
||||||
|
|
@ -565,6 +677,20 @@ int main(void) {
|
||||||
dirLight.shadowBias = 0.005f;
|
dirLight.shadowBias = 0.005f;
|
||||||
dirLight.shadowIntensity = 0.4f;
|
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);
|
int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
if (sock < 0) {
|
if (sock < 0) {
|
||||||
perror("socket");
|
perror("socket");
|
||||||
|
|
@ -583,10 +709,16 @@ int main(void) {
|
||||||
RemotePlayer rp[MAX_PLAYERS] = {0};
|
RemotePlayer rp[MAX_PLAYERS] = {0};
|
||||||
WorldItem wi[MAX_ITEMS] = {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);
|
Vector3 camPos = v3(0, 5.0f, 6);
|
||||||
float yaw = 0.0f, pitch = 0.0f;
|
float yaw = 0.0f, pitch = 0.0f;
|
||||||
|
|
||||||
float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f;
|
float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f;
|
||||||
|
int scoped = 0;
|
||||||
float fireCooldown = 0.0f;
|
float fireCooldown = 0.0f;
|
||||||
int shotsInBurst = 0;
|
int shotsInBurst = 0;
|
||||||
float burstResetTimer = 0.0f;
|
float burstResetTimer = 0.0f;
|
||||||
|
|
@ -673,6 +805,7 @@ int main(void) {
|
||||||
p->pistolAmmo = ps->pistolAmmo;
|
p->pistolAmmo = ps->pistolAmmo;
|
||||||
p->rifleAmmo = ps->rifleAmmo;
|
p->rifleAmmo = ps->rifleAmmo;
|
||||||
p->medkits = ps->medkits;
|
p->medkits = ps->medkits;
|
||||||
|
p->reloadTimeLeft = ps->reloadTimeLeft;
|
||||||
memset(p->username, 0, USERNAME_MAX);
|
memset(p->username, 0, USERNAME_MAX);
|
||||||
strncpy(p->username, ps->username, USERNAME_MAX - 1);
|
strncpy(p->username, ps->username, USERNAME_MAX - 1);
|
||||||
}
|
}
|
||||||
|
|
@ -682,6 +815,10 @@ int main(void) {
|
||||||
camPos.x = body.x;
|
camPos.x = body.x;
|
||||||
camPos.y = body.y + 1.0f;
|
camPos.y = body.y + 1.0f;
|
||||||
camPos.z = body.z + 0.0001f;
|
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)) {
|
} else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) {
|
||||||
MsgItems *m = (MsgItems *)buf;
|
MsgItems *m = (MsgItems *)buf;
|
||||||
|
|
@ -695,6 +832,14 @@ int main(void) {
|
||||||
wi[i].qty = m->items[i].qty;
|
wi[i].qty = m->items[i].qty;
|
||||||
wi[i].pos = v3(m->items[i].x, m->items[i].y, m->items[i].z);
|
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;
|
buttons |= BTN_USE_MEDKIT;
|
||||||
if (IsKeyPressed(KEY_SPACE))
|
if (IsKeyPressed(KEY_SPACE))
|
||||||
buttons |= BTN_JUMP;
|
buttons |= BTN_JUMP;
|
||||||
|
if (IsKeyPressed(KEY_Z))
|
||||||
|
scoped = !scoped;
|
||||||
|
|
||||||
if (myId != 255) {
|
if (myId != 255) {
|
||||||
MsgInput in = {0};
|
MsgInput in = {0};
|
||||||
|
|
@ -788,7 +935,7 @@ int main(void) {
|
||||||
cam.position = camPos;
|
cam.position = camPos;
|
||||||
cam.target = v3_add(camPos, forward);
|
cam.target = v3_add(camPos, forward);
|
||||||
cam.up = v3(0, 1, 0);
|
cam.up = v3(0, 1, 0);
|
||||||
cam.fovy = 75.0f;
|
cam.fovy = scoped ? 30.0f : 75.0f;
|
||||||
cam.projection = CAMERA_PERSPECTIVE;
|
cam.projection = CAMERA_PERSPECTIVE;
|
||||||
|
|
||||||
BeginDrawing();
|
BeginDrawing();
|
||||||
|
|
@ -796,20 +943,41 @@ int main(void) {
|
||||||
|
|
||||||
BeginMode3D(cam);
|
BeginMode3D(cam);
|
||||||
|
|
||||||
// Calculate terrain normal at camera position for lighting
|
// Set up shader uniforms for terrain rendering
|
||||||
Vector3 terrainNormal = v3(0, 1, 0); // Default up normal
|
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
|
// Debug mode: 0=normal lighting, 1=show normals as colors, 2=show AO only
|
||||||
// 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);
|
|
||||||
|
|
||||||
|
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 =
|
terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color =
|
||||||
litTerrainColor;
|
(Color){80, 140, 70, 255};
|
||||||
DrawModel(terrainModel, (Vector3){0, 0, 0}, 1.0f, WHITE);
|
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++) {
|
for (int i = 0; i < MAX_ITEMS; i++) {
|
||||||
if (!wi[i].present)
|
if (!wi[i].present)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -821,13 +989,14 @@ int main(void) {
|
||||||
if (wi[i].type == ITEM_AMMO_RIFLE)
|
if (wi[i].type == ITEM_AMMO_RIFLE)
|
||||||
ic = (Color){255, 180, 120, 255};
|
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 =
|
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);
|
DrawSphere(wi[i].pos, 0.3f, litColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Players with lighting and shadows
|
// Players with basic lighting
|
||||||
for (int i = 0; i < MAX_PLAYERS; i++) {
|
for (int i = 0; i < MAX_PLAYERS; i++) {
|
||||||
if (!rp[i].present)
|
if (!rp[i].present)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -837,9 +1006,10 @@ int main(void) {
|
||||||
if (!rp[i].alive)
|
if (!rp[i].alive)
|
||||||
c = (Color){120, 120, 120, 255};
|
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 =
|
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,
|
DrawCapsule(v3(p.x, p.y - 0.5f, p.z), v3(p.x, p.y + 0.5f, p.z), 0.35f, 8,
|
||||||
8, litPlayerColor);
|
8, litPlayerColor);
|
||||||
}
|
}
|
||||||
|
|
@ -900,29 +1070,58 @@ int main(void) {
|
||||||
DrawText(TextFormat("%d", currentMag), sw - 250, sh - 75, 40, RAYWHITE);
|
DrawText(TextFormat("%d", currentMag), sw - 250, sh - 75, 40, RAYWHITE);
|
||||||
DrawText(TextFormat("/ %d", reserveAmmo), sw - 140, sh - 65, 24,
|
DrawText(TextFormat("/ %d", reserveAmmo), sw - 140, sh - 65, 24,
|
||||||
(Color){180, 180, 180, 255});
|
(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});
|
DrawText("RELOAD!", sw - 250, sh - 30, 20, (Color){255, 80, 80, 255});
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawFPS(sw - 90, 10);
|
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
|
// Crosshair
|
||||||
{
|
{
|
||||||
int cx = sw / 2, cy = sh / 2;
|
int cx = sw / 2, cy = sh / 2;
|
||||||
int gap = 6 + (int)crossSpread;
|
int gap = scoped ? 2 : 6 + (int)crossSpread;
|
||||||
int len = 10, thick = 2;
|
int len = scoped ? 5 : 10, thick = 2;
|
||||||
Color col = (Color){240, 240, 245, 220};
|
Color col = (Color){240, 240, 245, 220};
|
||||||
DrawRectangle(cx - gap - len, cy - thick / 2, len, thick, col);
|
DrawRectangle(cx - gap - len, cy - thick / 2, len, thick, col);
|
||||||
DrawRectangle(cx + gap, 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 - len, thick, len, col);
|
||||||
DrawRectangle(cx - thick / 2, cy + gap, 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();
|
EndDrawing();
|
||||||
}
|
}
|
||||||
|
|
||||||
UnloadModel(terrainModel);
|
UnloadModel(terrainModel);
|
||||||
|
unload_terrain_shader(&terrainShader);
|
||||||
CloseWindow();
|
CloseWindow();
|
||||||
CLOSESOCK(sock);
|
CLOSESOCK(sock);
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
|
||||||
1
game/libfishsoup
Symbolic link
1
game/libfishsoup
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../libfishsoup
|
||||||
BIN
game/server
Executable file
BIN
game/server
Executable file
Binary file not shown.
277
game/server.c
277
game/server.c
|
|
@ -2,6 +2,7 @@
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
|
@ -23,12 +24,12 @@ typedef int socklen_t;
|
||||||
#define CLOSESOCK close
|
#define CLOSESOCK close
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define PROTOCOL_VERSION 5
|
#define PROTOCOL_VERSION 67
|
||||||
#define SERVER_PORT 27015
|
#define SERVER_PORT 27015
|
||||||
#define MAX_PLAYERS 16
|
#define MAX_PLAYERS 16
|
||||||
#define USERNAME_MAX 16
|
#define USERNAME_MAX 16
|
||||||
|
|
||||||
#define TICK_HZ 60
|
#define TICK_HZ 20
|
||||||
#define DT (1.0f / (float)TICK_HZ)
|
#define DT (1.0f / (float)TICK_HZ)
|
||||||
|
|
||||||
#define WEAPON_PISTOL 0
|
#define WEAPON_PISTOL 0
|
||||||
|
|
@ -48,11 +49,15 @@ typedef int socklen_t;
|
||||||
#define BTN_USE_MEDKIT (1u << 4)
|
#define BTN_USE_MEDKIT (1u << 4)
|
||||||
#define BTN_JUMP (1u << 5)
|
#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_WORLD (1u << 0)
|
||||||
#define CAT_PLAYER (1u << 1)
|
#define CAT_PLAYER (1u << 1)
|
||||||
#define CAT_ITEM (1u << 2)
|
#define CAT_ITEM (1u << 2)
|
||||||
|
|
||||||
// ============== SIMPLEX NOISE ==============
|
|
||||||
static const uint8_t perm[512] = {
|
static const uint8_t perm[512] = {
|
||||||
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7,
|
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,
|
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 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;
|
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_SIZE 256
|
||||||
#define TERRAIN_SCALE 2.0f
|
#define TERRAIN_SCALE 1.0f
|
||||||
static dReal *gHeightData = NULL;
|
static dReal *gHeightData = NULL;
|
||||||
static dGeomID gTerrainGeom = 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);
|
printf("Generated smooth heightmap terrain %dx%d\n", size, size);
|
||||||
}
|
}
|
||||||
// ============== END SIMPLEX NOISE ==============
|
|
||||||
|
|
||||||
static double now_seconds(void) {
|
static double now_seconds(void) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
@ -203,7 +207,8 @@ typedef enum MsgType : uint8_t {
|
||||||
MSG_INPUT = 3,
|
MSG_INPUT = 3,
|
||||||
MSG_SNAPSHOT = 4,
|
MSG_SNAPSHOT = 4,
|
||||||
MSG_SHOOT = 5,
|
MSG_SHOOT = 5,
|
||||||
MSG_ITEMS = 6
|
MSG_ITEMS = 6,
|
||||||
|
MSG_ROOM_STATE = 7
|
||||||
} MsgType;
|
} MsgType;
|
||||||
|
|
||||||
typedef struct MsgHello {
|
typedef struct MsgHello {
|
||||||
|
|
@ -238,6 +243,7 @@ typedef struct PlayerStateNet {
|
||||||
float x, y, z, yaw, pitch;
|
float x, y, z, yaw, pitch;
|
||||||
uint8_t weapon;
|
uint8_t weapon;
|
||||||
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
|
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
|
||||||
|
int16_t reloadTimeLeft;
|
||||||
char username[USERNAME_MAX];
|
char username[USERNAME_MAX];
|
||||||
} PlayerStateNet;
|
} PlayerStateNet;
|
||||||
|
|
||||||
|
|
@ -261,6 +267,14 @@ typedef struct MsgItems {
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
ItemNet items[MAX_ITEMS];
|
ItemNet items[MAX_ITEMS];
|
||||||
} MsgItems;
|
} MsgItems;
|
||||||
|
|
||||||
|
typedef struct MsgRoomState {
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t state;
|
||||||
|
float countdownRemaining;
|
||||||
|
uint8_t winnerId;
|
||||||
|
char winnerName[USERNAME_MAX];
|
||||||
|
} MsgRoomState;
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
typedef struct Item {
|
typedef struct Item {
|
||||||
|
|
@ -283,10 +297,19 @@ typedef struct Player {
|
||||||
double lastShotTime;
|
double lastShotTime;
|
||||||
int shotsInBurst;
|
int shotsInBurst;
|
||||||
uint32_t rng;
|
uint32_t rng;
|
||||||
|
float reloadTimer;
|
||||||
|
int reloadWeapon;
|
||||||
dBodyID body;
|
dBodyID body;
|
||||||
dGeomID geom;
|
dGeomID geom;
|
||||||
} Player;
|
} Player;
|
||||||
|
|
||||||
|
typedef struct RoomState {
|
||||||
|
int state;
|
||||||
|
double countdownEndTime;
|
||||||
|
int winnerId;
|
||||||
|
int playerCount;
|
||||||
|
} RoomState;
|
||||||
|
|
||||||
static int addr_equal(const struct sockaddr_in *a,
|
static int addr_equal(const struct sockaddr_in *a,
|
||||||
const struct sockaddr_in *b) {
|
const struct sockaddr_in *b) {
|
||||||
return (a->sin_family == b->sin_family) && (a->sin_port == b->sin_port) &&
|
return (a->sin_family == b->sin_family) && (a->sin_port == b->sin_port) &&
|
||||||
|
|
@ -315,6 +338,7 @@ static dSpaceID gSpace;
|
||||||
static dJointGroupID gContactGroup;
|
static dJointGroupID gContactGroup;
|
||||||
static Item gItems[MAX_ITEMS];
|
static Item gItems[MAX_ITEMS];
|
||||||
static uint16_t gNextItemId = 1;
|
static uint16_t gNextItemId = 1;
|
||||||
|
static RoomState gRoomState;
|
||||||
|
|
||||||
static void item_destroy(Item *it) {
|
static void item_destroy(Item *it) {
|
||||||
if (it->geom) {
|
if (it->geom) {
|
||||||
|
|
@ -393,10 +417,25 @@ static void ode_init_world(void) {
|
||||||
dGeomSetCategoryBits(gTerrainGeom, CAT_WORLD);
|
dGeomSetCategoryBits(gTerrainGeom, CAT_WORLD);
|
||||||
dGeomSetCollideBits(gTerrainGeom, CAT_PLAYER);
|
dGeomSetCollideBits(gTerrainGeom, CAT_PLAYER);
|
||||||
|
|
||||||
item_spawn(ITEM_MEDKIT, 1, 10.0f, 10.0f);
|
// Spawn initial random loot
|
||||||
item_spawn(ITEM_MEDKIT, 1, -15.0f, 20.0f);
|
for (int i = 0; i < 4; i++) {
|
||||||
item_spawn(ITEM_AMMO_RIFLE, 30, 5.0f, -10.0f);
|
float x = (float)(rand() % 200 - 100);
|
||||||
item_spawn(ITEM_AMMO_PISTOL, 12, -10.0f, -15.0f);
|
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) {
|
static void ode_shutdown_world(void) {
|
||||||
|
|
@ -418,8 +457,9 @@ static void player_ode_create(Player *p, int spawnIndex) {
|
||||||
dBodySetLinearDamping(p->body, 0.05);
|
dBodySetLinearDamping(p->body, 0.05);
|
||||||
dBodySetAngularDamping(p->body, 0.99);
|
dBodySetAngularDamping(p->body, 0.99);
|
||||||
|
|
||||||
float sx = (float)(spawnIndex * 4 - 8);
|
// Random spawn position
|
||||||
float sz = (float)(spawnIndex * 3 - 6);
|
float sx = (float)(rand() % 200 - 100); // -100 to 99
|
||||||
|
float sz = (float)(rand() % 200 - 100);
|
||||||
float sy = get_terrain_height(sx, sz) + 3.0f;
|
float sy = get_terrain_height(sx, sz) + 3.0f;
|
||||||
dBodySetPosition(p->body, sx, sy, sz);
|
dBodySetPosition(p->body, sx, sy, sz);
|
||||||
|
|
||||||
|
|
@ -547,6 +587,14 @@ int main(void) {
|
||||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||||||
#endif
|
#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();
|
ode_init_world();
|
||||||
|
|
||||||
int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
|
|
@ -568,7 +616,7 @@ int main(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Player players[MAX_PLAYERS] = {0};
|
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);
|
printf("Server listening UDP %d\n", SERVER_PORT);
|
||||||
double nextTick = now_seconds();
|
double nextTick = now_seconds();
|
||||||
|
|
@ -606,6 +654,9 @@ int main(void) {
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (pid < 0) {
|
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++)
|
for (int i = 0; i < MAX_PLAYERS; i++)
|
||||||
if (!players[i].inUse) {
|
if (!players[i].inUse) {
|
||||||
pid = i;
|
pid = i;
|
||||||
|
|
@ -625,6 +676,7 @@ int main(void) {
|
||||||
p->medkits = 2;
|
p->medkits = 2;
|
||||||
p->rng = 0xA341316Cu ^ (uint32_t)(i * 2654435761u);
|
p->rng = 0xA341316Cu ^ (uint32_t)(i * 2654435761u);
|
||||||
player_ode_create(p, i);
|
player_ode_create(p, i);
|
||||||
|
gRoomState.playerCount++;
|
||||||
printf("Player %d joined as '%s'\n", pid, p->username);
|
printf("Player %d joined as '%s'\n", pid, p->username);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -656,20 +708,9 @@ int main(void) {
|
||||||
p->weapon = WEAPON_RIFLE;
|
p->weapon = WEAPON_RIFLE;
|
||||||
|
|
||||||
if (m->buttons & BTN_RELOAD) {
|
if (m->buttons & BTN_RELOAD) {
|
||||||
if (p->weapon == WEAPON_PISTOL) {
|
if (p->reloadTimer <= 0.0f) {
|
||||||
int need = 12 - p->pistolMag;
|
p->reloadTimer = 2.0f; // 2 second reload time
|
||||||
if (need > 0 && p->pistolAmmo > 0) {
|
p->reloadWeapon = p->weapon;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -712,14 +753,15 @@ int main(void) {
|
||||||
|
|
||||||
if (m->buttons & BTN_JUMP) {
|
if (m->buttons & BTN_JUMP) {
|
||||||
const dReal *lvel = dBodyGetLinearVel(p->body);
|
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);
|
printf("Player %d jumped\n", pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type == MSG_SHOOT && n >= (int)sizeof(MsgShoot)) {
|
} else if (type == MSG_SHOOT && n >= (int)sizeof(MsgShoot)) {
|
||||||
const MsgShoot *m = (const MsgShoot *)buf;
|
const MsgShoot *m = (const MsgShoot *)buf;
|
||||||
if (pid >= 0 && m->playerId == (uint8_t)pid && players[pid].inUse &&
|
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];
|
Player *sh = &players[pid];
|
||||||
const double now = now_seconds();
|
const double now = now_seconds();
|
||||||
double fireInterval =
|
double fireInterval =
|
||||||
|
|
@ -729,6 +771,9 @@ int main(void) {
|
||||||
if (now - sh->lastShotTime > 0.18)
|
if (now - sh->lastShotTime > 0.18)
|
||||||
sh->shotsInBurst = 0;
|
sh->shotsInBurst = 0;
|
||||||
|
|
||||||
|
if (sh->reloadTimer > 0.0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
int *mag =
|
int *mag =
|
||||||
(sh->weapon == WEAPON_PISTOL) ? &sh->pistolMag : &sh->rifleMag;
|
(sh->weapon == WEAPON_PISTOL) ? &sh->pistolMag : &sh->rifleMag;
|
||||||
if (*mag <= 0)
|
if (*mag <= 0)
|
||||||
|
|
@ -784,6 +829,37 @@ int main(void) {
|
||||||
if (players[victim].hp <= 0) {
|
if (players[victim].hp <= 0) {
|
||||||
players[victim].hp = 0;
|
players[victim].hp = 0;
|
||||||
players[victim].alive = 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();
|
double t = now_seconds();
|
||||||
if (t >= nextTick) {
|
if (t >= nextTick) {
|
||||||
nextTick += DT;
|
nextTick += DT;
|
||||||
serverTick++;
|
serverTick++;
|
||||||
|
spawnCounter++;
|
||||||
|
|
||||||
dSpaceCollide(gSpace, 0, &nearCallback);
|
dSpaceCollide(gSpace, 0, &nearCallback);
|
||||||
dWorldQuickStep(gWorld, DT);
|
dWorldQuickStep(gWorld, DT);
|
||||||
dJointGroupEmpty(gContactGroup);
|
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++) {
|
for (int i = 0; i < MAX_PLAYERS; i++) {
|
||||||
Player *p = &players[i];
|
Player *p = &players[i];
|
||||||
if (!p->inUse || !p->alive)
|
if (!p->inUse || !p->alive)
|
||||||
|
|
@ -815,7 +1004,7 @@ int main(void) {
|
||||||
dz /= len;
|
dz /= len;
|
||||||
}
|
}
|
||||||
|
|
||||||
float speed = 6.0f;
|
float speed = 10.0f;
|
||||||
float fx = sinf(p->yaw) * dz - cosf(p->yaw) * dx;
|
float fx = sinf(p->yaw) * dz - cosf(p->yaw) * dx;
|
||||||
float fz = cosf(p->yaw) * dz + sinf(p->yaw) * dx;
|
float fz = cosf(p->yaw) * dz + sinf(p->yaw) * dx;
|
||||||
dBodySetLinearVel(p->body, fx * speed, lvel[1], fz * speed);
|
dBodySetLinearVel(p->body, fx * speed, lvel[1], fz * speed);
|
||||||
|
|
@ -846,6 +1035,7 @@ int main(void) {
|
||||||
ps->pistolAmmo = (int16_t)players[i].pistolAmmo;
|
ps->pistolAmmo = (int16_t)players[i].pistolAmmo;
|
||||||
ps->rifleAmmo = (int16_t)players[i].rifleAmmo;
|
ps->rifleAmmo = (int16_t)players[i].rifleAmmo;
|
||||||
ps->medkits = (int16_t)players[i].medkits;
|
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);
|
strncpy(ps->username, players[i].username, USERNAME_MAX - 1);
|
||||||
}
|
}
|
||||||
broadcast(sock, players, &snap, (int)sizeof(snap));
|
broadcast(sock, players, &snap, (int)sizeof(snap));
|
||||||
|
|
@ -870,6 +1060,27 @@ int main(void) {
|
||||||
}
|
}
|
||||||
broadcast(sock, players, &im, (int)sizeof(im));
|
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();
|
double now = now_seconds();
|
||||||
|
|
|
||||||
86
game/terrain.fs
Normal file
86
game/terrain.fs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
31
game/terrain.vs
Normal file
31
game/terrain.vs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue