diff --git a/game/client b/game/client index 87018d3..0c15c78 100755 Binary files a/game/client and b/game/client differ diff --git a/game/client.c b/game/client.c index 8b5bddd..316bb36 100644 --- a/game/client.c +++ b/game/client.c @@ -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,6 @@ typedef struct { float shadowIntensity; // How dark shadows are (0-1) } DirectionalLight; -// ============== TERRAIN SHADER ============== typedef struct { Shader shader; int locViewPos; @@ -86,7 +84,6 @@ static void unload_terrain_shader(TerrainShader *ts) { } } -// ============== END TERRAIN SHADER ============== static const uint8_t perm[512] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, @@ -284,9 +281,7 @@ static Mesh generate_terrain_mesh(void) { 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, @@ -389,7 +384,6 @@ static Vector3 v3_normalize(Vector3 v) { return (Vector3){v.x / len, v.y / len, v.z / len}; } -// ============== VECTOR3 HELPERS ============== static Vector3 v3(float x, float y, float z) { Vector3 v = {x, y, z}; return v; @@ -412,9 +406,6 @@ static Vector3 v3_norm(Vector3 a) { return v3(0, 0, 1); return v3(a.x / l, a.y / l, a.z / l); } -// ============== END VECTOR3 HELPERS ============== - -// ============== ENHANCED LIGHTING & AMBIENT OCCLUSION ============== // calculate_ambient_occlusion: Estimates how occluded a point is based on // terrain curvature Samples heights in cardinal directions and computes horizon @@ -504,8 +495,6 @@ static Color apply_phong_lighting_per_pixel(Color baseColor, Vector3 normal, return (Color){r, g, b, baseColor.a}; } -// ============== END ENHANCED LIGHTING & AMBIENT OCCLUSION ============== - static void set_nonblocking(int sock) { #ifdef _WIN32 u_long mode = 1; @@ -523,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 { @@ -582,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 { @@ -711,12 +709,17 @@ 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; + float recoilYaw = 0.0f, recoilPitch = 0.0f, crossSpread = 0.0f; + int scoped = 0; + float fireCooldown = 0.0f; int shotsInBurst = 0; float burstResetTimer = 0.0f; @@ -829,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); + } } } @@ -1068,6 +1079,30 @@ int main(void) { 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; @@ -1078,7 +1113,8 @@ int main(void) { DrawRectangle(cx + gap, cy - thick / 2, len, thick, col); DrawRectangle(cx - thick / 2, cy - gap - len, thick, len, col); DrawRectangle(cx - thick / 2, cy + gap, thick, len, col); - DrawCircleLines(cx, cy, scoped ? 1.5f : 3.0f, (Color){240, 240, 245, 160}); + DrawCircleLines(cx, cy, scoped ? 1.5f : 3.0f, + (Color){240, 240, 245, 160}); } EndDrawing(); diff --git a/game/server b/game/server index 8a2b689..03a0748 100755 Binary files a/game/server and b/game/server differ diff --git a/game/server.c b/game/server.c index 28bceea..b3c61b9 100644 --- a/game/server.c +++ b/game/server.c @@ -29,7 +29,7 @@ typedef int socklen_t; #define MAX_PLAYERS 16 #define USERNAME_MAX 16 -#define TICK_HZ 30 +#define TICK_HZ 20 #define DT (1.0f / (float)TICK_HZ) #define WEAPON_PISTOL 0 @@ -49,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, @@ -167,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 @@ -204,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 { @@ -263,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 { @@ -291,6 +303,13 @@ typedef struct Player { 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) && @@ -319,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) { @@ -397,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) { @@ -422,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); @@ -553,6 +589,12 @@ int main(void) { 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); @@ -612,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; @@ -631,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; } @@ -715,7 +761,7 @@ int main(void) { } 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 = @@ -783,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); } } } @@ -790,6 +867,66 @@ 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; @@ -923,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();