From c880f9dff0d05ff2553d7b2839ccb52e3432f3e6 Mon Sep 17 00:00:00 2001 From: Masashi Date: Thu, 18 Dec 2025 03:29:37 +0530 Subject: [PATCH] gam --- output.md | 1690 ----------------------------------------------------- 1 file changed, 1690 deletions(-) delete mode 100644 output.md diff --git a/output.md b/output.md deleted file mode 100644 index 8011e98..0000000 --- a/output.md +++ /dev/null @@ -1,1690 +0,0 @@ -```c -// game/server.c -// server.c - UDP authoritative server + ODE physics + ODE raycast bullets (C99) -// Adds: true spray, inventory capacity (6 slots), world pickups, drop/pick/use, -// armor+medkits. Build (Linux example): gcc server.c -O2 -o server -lode -lm - -#include -#include -#include -#include -#include - -#include - -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#pragma comment(lib, "ws2_32.lib") -typedef int socklen_t; -#define CLOSESOCK closesocket -#else -#include -#include -#include -#include -#include -#define CLOSESOCK close -#endif - -#define PROTOCOL_VERSION 4 -#define SERVER_PORT 27015 -#define MAX_PLAYERS 16 -#define USERNAME_MAX 16 - -#define TICK_HZ 60 -#define DT (1.0f / (float)TICK_HZ) - -#define WEAPON_PISTOL 0 -#define WEAPON_RIFLE 1 -#define WEAPON_COUNT 2 - -#define INV_SLOTS 6 -#define MAX_ITEMS 64 - -// inventory item types -#define ITEM_NONE 0 -#define ITEM_MEDKIT 1 -#define ITEM_ARMORPLATE 2 -#define ITEM_AMMO_PISTOL 3 -#define ITEM_AMMO_RIFLE 4 - -// MsgInput.buttons bits -#define BTN_RELOAD (1u << 0) -#define BTN_WPN1 (1u << 1) -#define BTN_WPN2 (1u << 2) -#define BTN_PICK (1u << 3) -#define BTN_DROP (1u << 4) -#define BTN_USE (1u << 5) - -// Collision categories -#define CAT_WORLD (1u << 0) -#define CAT_PLAYER (1u << 1) -#define CAT_ITEM (1u << 2) - -static double now_seconds(void) { -#ifdef _WIN32 - static LARGE_INTEGER freq; - static int init = 0; - LARGE_INTEGER t; - if (!init) { - QueryPerformanceFrequency(&freq); - init = 1; - } - QueryPerformanceCounter(&t); - return (double)t.QuadPart / (double)freq.QuadPart; -#else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; -#endif -} - -static void set_nonblocking(int sock) { -#ifdef _WIN32 - u_long mode = 1; - ioctlsocket(sock, FIONBIO, &mode); -#else - int flags = fcntl(sock, F_GETFL, 0); - fcntl(sock, F_SETFL, flags | O_NONBLOCK); -#endif -} - -#pragma pack(push, 1) -typedef enum MsgType : uint8_t { - MSG_HELLO = 1, - MSG_WELCOME = 2, - MSG_INPUT = 3, - MSG_SNAPSHOT = 4, - MSG_SHOOT = 5, - MSG_HIT = 6, - MSG_ITEMS = 7 -} MsgType; - -typedef struct MsgHello { - uint8_t type; - uint32_t protocol; - char username[USERNAME_MAX]; -} MsgHello; - -typedef struct MsgWelcome { - uint8_t type; - uint8_t playerId; - uint32_t serverTick; -} MsgWelcome; - -typedef struct MsgInput { - uint8_t type; - uint8_t playerId; - uint32_t clientTick; - float moveX; - float moveZ; - float yaw; - float pitch; - uint8_t buttons; - uint8_t slotIndex; -} MsgInput; - -typedef struct MsgShoot { - uint8_t type; - uint8_t playerId; - uint32_t clientTick; -} MsgShoot; - -typedef struct PlayerStateNet { - uint8_t id; - uint8_t alive; - int16_t hp; - int16_t armor; - float x, y, z; - float yaw, pitch; - - uint8_t weapon; - int16_t ammoMag[WEAPON_COUNT]; - int16_t ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int16_t invQty[INV_SLOTS]; - - char username[USERNAME_MAX]; -} PlayerStateNet; - -typedef struct MsgSnapshot { - uint8_t type; - uint32_t serverTick; - uint8_t count; - PlayerStateNet p[MAX_PLAYERS]; -} MsgSnapshot; - -typedef struct MsgHit { - uint8_t type; - uint8_t shooterId; - uint8_t victimId; - int16_t victimHp; -} MsgHit; - -typedef struct ItemNet { - uint16_t id; - uint8_t type; - int16_t qty; - float x, y, z; -} ItemNet; - -typedef struct MsgItems { - uint8_t type; - uint32_t serverTick; - uint8_t count; - ItemNet items[MAX_ITEMS]; -} MsgItems; -#pragma pack(pop) - -typedef struct Item { - int active; - uint16_t id; - uint8_t type; - int qty; - float x, y, z; - dGeomID geom; // static pickup geom (no body) -} Item; - -typedef struct Player { - int inUse; - struct sockaddr_in addr; - double lastHeard; - - char username[USERNAME_MAX]; - - float moveX, moveZ; - float yaw, pitch; - - int alive; - int hp; - int armor; - - int weapon; - int ammoMag[WEAPON_COUNT]; - int ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int invQty[INV_SLOTS]; - - double lastShotTime; - int shotsInBurst; - uint32_t rng; - - dBodyID body; - dGeomID geom; -} Player; - -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) && - (a->sin_addr.s_addr == b->sin_addr.s_addr); -} - -static float clampf(float v, float lo, float hi) { - if (v < lo) - return lo; - if (v > hi) - return hi; - return v; -} - -static void sendto_player(int sock, const Player *pl, const void *data, - int len) { - sendto(sock, (const char *)data, len, 0, (const struct sockaddr *)&pl->addr, - sizeof(pl->addr)); -} -static void broadcast(int sock, const Player players[MAX_PLAYERS], - const void *data, int len) { - for (int i = 0; i < MAX_PLAYERS; i++) - if (players[i].inUse) - sendto_player(sock, &players[i], data, len); -} - -// ----- ODE globals ----- -static dWorldID gWorld; -static dSpaceID gSpace; -static dJointGroupID gContactGroup; - -// items -static Item gItems[MAX_ITEMS]; -static uint16_t gNextItemId = 1; - -static void item_destroy(Item *it) { - if (it->geom) { - dGeomDestroy(it->geom); - it->geom = 0; - } - memset(it, 0, sizeof(*it)); -} -static int item_spawn(uint8_t type, int qty, float x, float y, float z) { - for (int i = 0; i < MAX_ITEMS; i++) { - if (!gItems[i].active) { - Item *it = &gItems[i]; - it->active = 1; - it->id = gNextItemId++; - it->type = type; - it->qty = qty; - it->x = x; - it->y = y; - it->z = z; - - it->geom = dCreateSphere(gSpace, 0.30); - dGeomSetPosition(it->geom, x, y + 0.3f, z); - - // Mark as item and avoid physical contacts; also make raycasts ignore it. - dGeomSetCategoryBits(it->geom, CAT_ITEM); - dGeomSetCollideBits(it->geom, 0); - - // Attach "user data" pointing to Item for identification if needed. - dGeomSetData(it->geom, it); - - return i; - } - } - return -1; -} - -static int inv_add(Player *p, uint8_t type, int qty) { - // stacking limits - int maxStack = 99; - if (type == ITEM_MEDKIT) - maxStack = 3; - if (type == ITEM_ARMORPLATE) - maxStack = 5; - - // try stack - for (int i = 0; i < INV_SLOTS; i++) { - if (p->invType[i] == type && p->invQty[i] < maxStack) { - int can = maxStack - p->invQty[i]; - int take = (qty < can) ? qty : can; - p->invQty[i] += take; - qty -= take; - if (qty <= 0) - return 1; - } - } - - // place into empty slots - for (int i = 0; i < INV_SLOTS; i++) { - if (p->invType[i] == ITEM_NONE) { - int take = (qty < maxStack) ? qty : maxStack; - p->invType[i] = type; - p->invQty[i] = take; - qty -= take; - if (qty <= 0) - return 1; - } - } - - // no space - return 0; -} - -static int inv_remove_one(Player *p, int slot) { - if (slot < 0 || slot >= INV_SLOTS) - return 0; - if (p->invType[slot] == ITEM_NONE || p->invQty[slot] <= 0) - return 0; - p->invQty[slot]--; - if (p->invQty[slot] <= 0) { - p->invQty[slot] = 0; - p->invType[slot] = ITEM_NONE; - } - return 1; -} - -static void nearCallback(void *user, dGeomID o1, dGeomID o2) { - (void)user; - - if (dGeomIsSpace(o1) || dGeomIsSpace(o2)) { - dSpaceCollide2(o1, o2, user, &nearCallback); - return; - } - - // Do not create contact joints involving item pickups - unsigned long c1 = dGeomGetCategoryBits(o1); - unsigned long c2 = dGeomGetCategoryBits(o2); - if ((c1 & CAT_ITEM) || (c2 & CAT_ITEM)) - return; - - const int MAXC = 8; - dContact c[MAXC]; - int n = dCollide(o1, o2, MAXC, &c[0].geom, sizeof(dContact)); - if (n <= 0) - return; - - for (int i = 0; i < n; i++) { - c[i].surface.mode = dContactApprox1; - c[i].surface.mu = 1.0; - c[i].surface.bounce = 0.0; - c[i].surface.bounce_vel = 0.0; - - dJointID j = dJointCreateContact(gWorld, gContactGroup, &c[i]); - dBodyID b1 = dGeomGetBody(o1); - dBodyID b2 = dGeomGetBody(o2); - dJointAttach(j, b1, b2); - } -} - -static void ode_init_world(void) { - dInitODE(); - gWorld = dWorldCreate(); - gSpace = dHashSpaceCreate(0); - gContactGroup = dJointGroupCreate(0); - - dWorldSetGravity(gWorld, 0, -9.81, 0); - dWorldSetERP(gWorld, 0.2); - dWorldSetCFM(gWorld, 1e-5); - - dGeomID ground = dCreatePlane(gSpace, 0, 1, 0, 0); - dGeomSetCategoryBits(ground, CAT_WORLD); - dGeomSetCollideBits(ground, CAT_PLAYER); - - const struct { - float x, y, z, sx, sy, sz; - } blocks[] = { - {2, 0.5f, 2, 1, 1, 1}, - {2, 1.5f, 2, 1, 1, 1}, - {-3, 0.5f, -1, 1, 1, 1}, - {0, 0.5f, 4, 2, 1, 2}, - }; - for (int i = 0; i < (int)(sizeof(blocks) / sizeof(blocks[0])); i++) { - dGeomID b = dCreateBox(gSpace, blocks[i].sx, blocks[i].sy, blocks[i].sz); - dGeomSetPosition(b, blocks[i].x, blocks[i].y, blocks[i].z); - dGeomSetCategoryBits(b, CAT_WORLD); - dGeomSetCollideBits(b, CAT_PLAYER); - } - - // spawn some test pickups - item_spawn(ITEM_MEDKIT, 1, 1.0f, 0.0f, 1.0f); - item_spawn(ITEM_MEDKIT, 1, -2.0f, 0.0f, 3.0f); - item_spawn(ITEM_ARMORPLATE, 1, 3.0f, 0.0f, -1.0f); - item_spawn(ITEM_AMMO_RIFLE, 30, 0.0f, 0.0f, 2.0f); - item_spawn(ITEM_AMMO_PISTOL, 12, -1.0f, 0.0f, -2.0f); -} - -static void ode_shutdown_world(void) { - dJointGroupDestroy(gContactGroup); - dSpaceDestroy(gSpace); - dWorldDestroy(gWorld); - dCloseODE(); -} - -static void player_ode_create(Player *p, int spawnIndex) { - const dReal radius = 0.35; - const dReal length = 1.0; - - p->body = dBodyCreate(gWorld); - - dMass m; - dMassSetCapsuleTotal(&m, 80.0, 3, radius, length); - dBodySetMass(p->body, &m); - - dBodySetLinearDamping(p->body, 0.05); - dBodySetAngularDamping(p->body, 0.99); - - dBodySetPosition(p->body, (dReal)(spawnIndex * 2), 2.0, 0.0); - dBodySetAngularVel(p->body, 0, 0, 0); - - p->geom = dCreateCapsule(gSpace, radius, length); - dGeomSetBody(p->geom, p->body); - - dGeomSetCategoryBits(p->geom, CAT_PLAYER); - dGeomSetCollideBits(p->geom, CAT_WORLD | CAT_PLAYER); -} - -static void player_ode_destroy(Player *p) { - if (p->geom) { - dGeomDestroy(p->geom); - p->geom = 0; - } - if (p->body) { - dBodyDestroy(p->body); - p->body = 0; - } -} - -// ---- ODE raycast ---- -typedef struct RaycastCtx { - dGeomID ray; - dGeomID hitGeom; - dContactGeom hit; - int hasHit; -} RaycastCtx; - -static void rayCallback(void *data, dGeomID o1, dGeomID o2) { - RaycastCtx *rc = (RaycastCtx *)data; - dGeomID ray = rc->ray; - dGeomID other = (o1 == ray) ? o2 : o1; - if (other == ray) - return; - - dContact c[4]; - int n = dCollide(ray, other, 4, &c[0].geom, sizeof(dContact)); - for (int i = 0; i < n; i++) { - if (!rc->hasHit || c[i].geom.depth < rc->hit.depth) { - rc->hasHit = 1; - rc->hit = c[i].geom; - rc->hitGeom = other; - } - } -} - -static int raycast_space(float ox, float oy, float oz, float dx, float dy, - float dz, float maxDist, dContactGeom *outHit, - dGeomID *outGeom) { - dGeomID ray = dCreateRay(0, maxDist); - dGeomRaySet(ray, ox, oy, oz, dx, dy, dz); - dGeomSetCategoryBits(ray, 0xFFFFFFFFu); - dGeomSetCollideBits(ray, CAT_WORLD | CAT_PLAYER); - - RaycastCtx rc = {0}; - rc.ray = ray; - dSpaceCollide2(ray, (dGeomID)gSpace, &rc, &rayCallback); - dGeomDestroy(ray); - - if (!rc.hasHit) - return 0; - if (outHit) - *outHit = rc.hit; - if (outGeom) - *outGeom = rc.hitGeom; - return 1; -} - -// ---- spray math helpers ---- -static uint32_t xorshift32(uint32_t *s) { - uint32_t x = *s; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - *s = x; - return x; -} -static float frand_signed(uint32_t *s) { - return ((xorshift32(s) & 0xFFFFFF) / (float)0x7FFFFF) - 1.0f; -} -static void dir_from_yaw_pitch(float yaw, float pitch, float *dx, float *dy, - float *dz) { - *dx = sinf(yaw) * cosf(pitch); - *dy = sinf(pitch); - *dz = cosf(yaw) * cosf(pitch); -} -static void v3_cross(float ax, float ay, float az, float bx, float by, float bz, - float *ox, float *oy, float *oz) { - *ox = ay * bz - az * by; - *oy = az * bx - ax * bz; - *oz = ax * by - ay * bx; -} -static void v3_normf(float *x, float *y, float *z) { - float l = sqrtf((*x) * (*x) + (*y) * (*y) + (*z) * (*z)); - if (l < 1e-6f) { - *x = 0; - *y = 0; - *z = 1; - return; - } - *x /= l; - *y /= l; - *z /= l; -} - -static int find_nearest_item(float x, float y, float z, float maxDist) { - int best = -1; - float bestD2 = maxDist * maxDist; - for (int i = 0; i < MAX_ITEMS; i++) { - if (!gItems[i].active) - continue; - float dx = gItems[i].x - x; - float dy = (gItems[i].y + 0.3f) - y; - float dz = gItems[i].z - z; - float d2 = dx * dx + dy * dy + dz * dz; - if (d2 < bestD2) { - bestD2 = d2; - best = i; - } - } - return best; -} - -int main(void) { -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(2, 2), &wsa); -#endif - - ode_init_world(); - - int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sock < 0) { - perror("socket"); - return 1; - } - set_nonblocking(sock); - - struct sockaddr_in srv = {0}; - srv.sin_family = AF_INET; - srv.sin_addr.s_addr = htonl(INADDR_ANY); - srv.sin_port = htons(SERVER_PORT); - - if (bind(sock, (struct sockaddr *)&srv, sizeof(srv)) < 0) { - perror("bind"); - CLOSESOCK(sock); - return 1; - } - - Player players[MAX_PLAYERS] = {0}; - uint32_t serverTick = 0; - uint32_t itemsTickDiv = 0; - - printf("Server listening UDP %d\n", SERVER_PORT); - double nextTick = now_seconds(); - - for (;;) { - // receive - for (;;) { - uint8_t buf[1400]; - struct sockaddr_in from = {0}; - socklen_t fromLen = sizeof(from); - - int n = (int)recvfrom(sock, (char *)buf, (int)sizeof(buf), 0, - (struct sockaddr *)&from, &fromLen); - if (n <= 0) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) - break; -#else - if (errno == EWOULDBLOCK || errno == EAGAIN) - break; -#endif - break; - } - - uint8_t type = buf[0]; - - int pid = -1; - for (int i = 0; i < MAX_PLAYERS; i++) { - if (players[i].inUse && addr_equal(&players[i].addr, &from)) { - pid = i; - break; - } - } - - if (type == MSG_HELLO && n >= (int)sizeof(MsgHello)) { - const MsgHello *m = (const MsgHello *)buf; - if (m->protocol != PROTOCOL_VERSION) - continue; - - if (pid < 0) { - for (int i = 0; i < MAX_PLAYERS; i++) - if (!players[i].inUse) { - pid = i; - Player *p = &players[i]; - memset(p, 0, sizeof(*p)); - p->inUse = 1; - p->addr = from; - p->lastHeard = now_seconds(); - strncpy(p->username, m->username, USERNAME_MAX - 1); - - p->alive = 1; - p->hp = 100; - p->armor = 0; - - p->weapon = WEAPON_RIFLE; - p->ammoMag[WEAPON_PISTOL] = 12; - p->ammoRes[WEAPON_PISTOL] = 48; - p->ammoMag[WEAPON_RIFLE] = 30; - p->ammoRes[WEAPON_RIFLE] = 90; - - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - p->invType[sidx] = ITEM_NONE; - p->invQty[sidx] = 0; - } - - p->lastShotTime = 0.0; - p->shotsInBurst = 0; - p->rng = 0xA341316Cu ^ (uint32_t)(i * 2654435761u); - - player_ode_create(p, i); - printf("Player %d joined as '%s'\n", pid, p->username); - break; - } - } else { - players[pid].lastHeard = now_seconds(); - } - - if (pid >= 0) { - MsgWelcome w = {0}; - w.type = MSG_WELCOME; - w.playerId = (uint8_t)pid; - w.serverTick = serverTick; - sendto(sock, (const char *)&w, (int)sizeof(w), 0, - (struct sockaddr *)&from, sizeof(from)); - } - } else if (type == MSG_INPUT && n >= (int)sizeof(MsgInput)) { - const MsgInput *m = (const MsgInput *)buf; - if (pid >= 0 && m->playerId == (uint8_t)pid) { - Player *p = &players[pid]; - p->lastHeard = now_seconds(); - - p->moveX = clampf(m->moveX, -1.0f, 1.0f); - p->moveZ = clampf(m->moveZ, -1.0f, 1.0f); - p->yaw = m->yaw; - p->pitch = clampf(m->pitch, -1.5f, 1.5f); - - if (m->buttons & BTN_WPN1) - p->weapon = WEAPON_PISTOL; - if (m->buttons & BTN_WPN2) - p->weapon = WEAPON_RIFLE; - - // reload - if (m->buttons & BTN_RELOAD) { - int wpn = p->weapon; - int magSize = (wpn == WEAPON_PISTOL) ? 12 : 30; - int need = magSize - p->ammoMag[wpn]; - if (need > 0 && p->ammoRes[wpn] > 0) { - int take = (p->ammoRes[wpn] < need) ? p->ammoRes[wpn] : need; - p->ammoRes[wpn] -= take; - p->ammoMag[wpn] += take; - } - } - - // pick - if (m->buttons & BTN_PICK) { - const dReal *pos = dBodyGetPosition(p->body); - int itIdx = find_nearest_item((float)pos[0], (float)pos[1] + 0.7f, - (float)pos[2], 1.6f); - if (itIdx >= 0) { - Item *it = &gItems[itIdx]; - int taken = 0; - - if (it->type == ITEM_AMMO_PISTOL) { - p->ammoRes[WEAPON_PISTOL] += it->qty; - taken = 1; - } else if (it->type == ITEM_AMMO_RIFLE) { - p->ammoRes[WEAPON_RIFLE] += it->qty; - taken = 1; - } else { - taken = inv_add(p, it->type, it->qty); - } - - if (taken) - item_destroy(it); - } - } - - // drop selected slot - if (m->buttons & BTN_DROP) { - int slot = (m->slotIndex < INV_SLOTS) ? (int)m->slotIndex : 0; - uint8_t t = p->invType[slot]; - if (t != ITEM_NONE && p->invQty[slot] > 0) { - const dReal *pos = dBodyGetPosition(p->body); - float ox = (float)pos[0]; - float oy = (float)pos[1]; - float oz = (float)pos[2]; - - // drop 1 unit - if (inv_remove_one(p, slot)) { - item_spawn(t, 1, ox, oy, oz); - } - } - } - - // use selected slot (medkit/armor plate) - if (m->buttons & BTN_USE) { - int slot = (m->slotIndex < INV_SLOTS) ? (int)m->slotIndex : 0; - uint8_t t = p->invType[slot]; - if (t == ITEM_MEDKIT && p->hp < 100 && p->invQty[slot] > 0) { - inv_remove_one(p, slot); - p->hp += 25; - if (p->hp > 100) - p->hp = 100; - } else if (t == ITEM_ARMORPLATE && p->armor < 100 && - p->invQty[slot] > 0) { - inv_remove_one(p, slot); - p->armor += 25; - if (p->armor > 100) - p->armor = 100; - } - } - } - } 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) { - Player *sh = &players[pid]; - const double now = now_seconds(); - - double fireInterval = - (sh->weapon == WEAPON_PISTOL) ? (1.0 / 4.0) : (1.0 / 12.0); - if (now - sh->lastShotTime < fireInterval) - continue; - if (now - sh->lastShotTime > 0.18) - sh->shotsInBurst = 0; - - int wpn = sh->weapon; - if (sh->ammoMag[wpn] <= 0) - continue; - - sh->ammoMag[wpn]--; - sh->shotsInBurst++; - sh->lastShotTime = now; - - const dReal *pos = dBodyGetPosition(sh->body); - float ox = (float)pos[0]; - float oy = (float)pos[1] + 0.7f; - float oz = (float)pos[2]; - - float fx, fy, fz; - dir_from_yaw_pitch(sh->yaw, sh->pitch, &fx, &fy, &fz); - v3_normf(&fx, &fy, &fz); - - float base = (wpn == WEAPON_PISTOL) ? 0.0035f : 0.0080f; - float grow = (wpn == WEAPON_PISTOL) ? 0.0010f : 0.0025f; - float spread = base + grow * (float)sh->shotsInBurst; - if (spread > 0.08f) - spread = 0.08f; - - float upx = 0, upy = 1, upz = 0; - float rx, ry, rz; - v3_cross(fx, fy, fz, upx, upy, upz, &rx, &ry, &rz); - v3_normf(&rx, &ry, &rz); - float ux, uy, uz; - v3_cross(rx, ry, rz, fx, fy, fz, &ux, &uy, &uz); - v3_normf(&ux, &uy, &uz); - - float ax = frand_signed(&sh->rng) * spread; - float ay = frand_signed(&sh->rng) * spread; - - float dx = fx + rx * ax + ux * ay; - float dy = fy + ry * ax + uy * ay; - float dz = fz + rz * ax + uz * ay; - v3_normf(&dx, &dy, &dz); - - dContactGeom hit; - dGeomID hitGeom = 0; - if (raycast_space(ox, oy, oz, dx, dy, dz, 80.0f, &hit, &hitGeom)) { - int victim = -1; - for (int i = 0; i < MAX_PLAYERS; i++) { - if (!players[i].inUse) - continue; - if (players[i].geom == hitGeom) { - victim = i; - break; - } - } - - if (victim >= 0 && victim != pid && players[victim].alive) { - int dmg = 25; - - // armor absorbs first (simple model) - int absorb = - (players[victim].armor < dmg) ? players[victim].armor : dmg; - players[victim].armor -= absorb; - dmg -= absorb; - - players[victim].hp -= dmg; - if (players[victim].hp <= 0) { - players[victim].hp = 0; - players[victim].alive = 0; - } - - MsgHit ev = {0}; - ev.type = MSG_HIT; - ev.shooterId = (uint8_t)pid; - ev.victimId = (uint8_t)victim; - ev.victimHp = (int16_t)players[victim].hp; - broadcast(sock, players, &ev, (int)sizeof(ev)); - } - } - } - } - } - - // fixed tick - double t = now_seconds(); - if (t < nextTick) { -#ifdef _WIN32 - Sleep(1); -#else - struct timespec ts = {0, 1000000}; - nanosleep(&ts, NULL); -#endif - continue; - } - nextTick += (1.0 / (double)TICK_HZ); - serverTick++; - - // timeouts + movement - const float speed = 6.0f; - for (int i = 0; i < MAX_PLAYERS; i++) { - Player *p = &players[i]; - if (!p->inUse) - continue; - - if (now_seconds() - p->lastHeard > 10.0) { - printf("Player %d timed out\n", i); - player_ode_destroy(p); - memset(p, 0, sizeof(*p)); - continue; - } - - if (!p->alive) { - dBodySetLinearVel(p->body, 0, 0, 0); - continue; - } - - float cy = cosf(p->yaw), sy = sinf(p->yaw); - float fwdx = sy, fwdz = cy; - float rgtx = cy, rgtz = -sy; - - float vx = (fwdx * p->moveZ + rgtx * p->moveX) * speed; - float vz = (fwdz * p->moveZ + rgtz * p->moveX) * speed; - - const dReal *vcur = dBodyGetLinearVel(p->body); - dBodySetLinearVel(p->body, vx, vcur[1], vz); - - dQuaternion q; - dQFromAxisAndAngle(q, 0, 1, 0, p->yaw); - dBodySetQuaternion(p->body, q); - dBodySetAngularVel(p->body, 0, 0, 0); - } - - // step ODE - dJointGroupEmpty(gContactGroup); - dSpaceCollide(gSpace, 0, &nearCallback); - dWorldQuickStep(gWorld, DT); - - // snapshot players - MsgSnapshot snap = {0}; - snap.type = MSG_SNAPSHOT; - snap.serverTick = serverTick; - snap.count = 0; - - for (int i = 0; i < MAX_PLAYERS; i++) { - if (!players[i].inUse) - continue; - - const dReal *pos = dBodyGetPosition(players[i].body); - PlayerStateNet *ps = &snap.p[snap.count++]; - - ps->id = (uint8_t)i; - ps->alive = (uint8_t)players[i].alive; - ps->hp = (int16_t)players[i].hp; - ps->armor = (int16_t)players[i].armor; - - ps->x = (float)pos[0]; - ps->y = (float)pos[1]; - ps->z = (float)pos[2]; - ps->yaw = players[i].yaw; - ps->pitch = players[i].pitch; - - ps->weapon = (uint8_t)players[i].weapon; - for (int w = 0; w < WEAPON_COUNT; w++) { - ps->ammoMag[w] = (int16_t)players[i].ammoMag[w]; - ps->ammoRes[w] = (int16_t)players[i].ammoRes[w]; - } - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - ps->invType[sidx] = players[i].invType[sidx]; - ps->invQty[sidx] = (int16_t)players[i].invQty[sidx]; - } - - memset(ps->username, 0, USERNAME_MAX); - strncpy(ps->username, players[i].username, USERNAME_MAX - 1); - } - - broadcast(sock, players, &snap, (int)sizeof(MsgSnapshot)); - - // broadcast items at ~10Hz - itemsTickDiv++; - if (itemsTickDiv >= 6) { - itemsTickDiv = 0; - - MsgItems im = {0}; - im.type = MSG_ITEMS; - im.serverTick = serverTick; - im.count = 0; - - for (int i = 0; i < MAX_ITEMS && im.count < MAX_ITEMS; i++) { - if (!gItems[i].active) - continue; - ItemNet *on = &im.items[im.count++]; - on->id = gItems[i].id; - on->type = gItems[i].type; - on->qty = (int16_t)gItems[i].qty; - on->x = gItems[i].x; - on->y = gItems[i].y; - on->z = gItems[i].z; - } - - broadcast(sock, players, &im, (int)sizeof(MsgItems)); - } - } - - CLOSESOCK(sock); - ode_shutdown_world(); -#ifdef _WIN32 - WSACleanup(); -#endif - return 0; -} - -``` - -```c -// game/client.c -// client.c - raylib client (C99) -// Features: username prompt, inverted A/D, inverted pitch, recoil, crosshair -// widening, spray, inventory UI toggle on E, pick/drop/use items, renders -// dropped items from server. Build (Linux example): -// gcc client.c -O2 -o client -lraylib -lm -lpthread -ldl -lrt -lX11 - -#include "raylib.h" -#include -#include -#include -#include - -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#pragma comment(lib, "ws2_32.lib") -typedef int socklen_t; -#define CLOSESOCK closesocket -#else -#include -#include -#include -#include -#include -#define CLOSESOCK close -#endif - -#define PROTOCOL_VERSION 4 -#define SERVER_PORT 27015 -#define MAX_PLAYERS 16 -#define USERNAME_MAX 16 - -#define WEAPON_PISTOL 0 -#define WEAPON_RIFLE 1 -#define WEAPON_COUNT 2 - -#define INV_SLOTS 6 -#define MAX_ITEMS 64 - -// Inventory item types (must match server) -#define ITEM_NONE 0 -#define ITEM_MEDKIT 1 -#define ITEM_ARMORPLATE 2 -#define ITEM_AMMO_PISTOL 3 -#define ITEM_AMMO_RIFLE 4 - -// MsgInput.buttons bits -#define BTN_RELOAD (1u << 0) -#define BTN_WPN1 (1u << 1) -#define BTN_WPN2 (1u << 2) -#define BTN_PICK (1u << 3) -#define BTN_DROP (1u << 4) -#define BTN_USE (1u << 5) - -static void set_nonblocking(int sock) { -#ifdef _WIN32 - u_long mode = 1; - ioctlsocket(sock, FIONBIO, &mode); -#else - int flags = fcntl(sock, F_GETFL, 0); - fcntl(sock, F_SETFL, flags | O_NONBLOCK); -#endif -} - -#pragma pack(push, 1) -typedef enum MsgType : uint8_t { - MSG_HELLO = 1, - MSG_WELCOME = 2, - MSG_INPUT = 3, - MSG_SNAPSHOT = 4, - MSG_SHOOT = 5, - MSG_HIT = 6, - MSG_ITEMS = 7 -} MsgType; - -typedef struct MsgHello { - uint8_t type; - uint32_t protocol; - char username[USERNAME_MAX]; -} MsgHello; - -typedef struct MsgWelcome { - uint8_t type; - uint8_t playerId; - uint32_t serverTick; -} MsgWelcome; - -typedef struct MsgInput { - uint8_t type; - uint8_t playerId; - uint32_t clientTick; - float moveX; - float moveZ; - float yaw; - float pitch; - uint8_t buttons; // BTN_* - uint8_t slotIndex; // 0..INV_SLOTS-1 (for DROP/USE) -} MsgInput; - -typedef struct MsgShoot { - uint8_t type; - uint8_t playerId; - uint32_t clientTick; -} MsgShoot; - -typedef struct PlayerStateNet { - uint8_t id; - uint8_t alive; - int16_t hp; - int16_t armor; // NEW - float x, y, z; - float yaw, pitch; - - uint8_t weapon; - int16_t ammoMag[WEAPON_COUNT]; - int16_t ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int16_t invQty[INV_SLOTS]; - - char username[USERNAME_MAX]; -} PlayerStateNet; - -typedef struct MsgSnapshot { - uint8_t type; - uint32_t serverTick; - uint8_t count; - PlayerStateNet p[MAX_PLAYERS]; -} MsgSnapshot; - -typedef struct MsgHit { - uint8_t type; - uint8_t shooterId; - uint8_t victimId; - int16_t victimHp; -} MsgHit; - -typedef struct ItemNet { - uint16_t id; - uint8_t type; - int16_t qty; - float x, y, z; -} ItemNet; - -typedef struct MsgItems { - uint8_t type; - uint32_t serverTick; - uint8_t count; - ItemNet items[MAX_ITEMS]; -} MsgItems; -#pragma pack(pop) - -typedef struct RemotePlayer { - int present; - int alive; - int hp; - int armor; - Vector3 pos; - float yaw, pitch; - - uint8_t weapon; - int16_t ammoMag[WEAPON_COUNT]; - int16_t ammoRes[WEAPON_COUNT]; - - uint8_t invType[INV_SLOTS]; - int16_t invQty[INV_SLOTS]; - - char username[USERNAME_MAX]; -} RemotePlayer; - -typedef struct WorldItem { - int present; - uint16_t id; - uint8_t type; - int16_t qty; - 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_scale(Vector3 a, float s) { - return v3(a.x * s, a.y * s, a.z * s); -} -static Vector3 v3_norm(Vector3 a) { - float l = v3_len(a); - if (l <= 1e-6f) - return v3(0, 0, 1); - return v3_scale(a, 1.0f / l); -} - -static const char *ItemName(uint8_t t) { - switch (t) { - case ITEM_MEDKIT: - return "Medkit"; - case ITEM_ARMORPLATE: - return "Armor plate"; - case ITEM_AMMO_PISTOL: - return "Pistol ammo"; - case ITEM_AMMO_RIFLE: - return "Rifle ammo"; - default: - return "-"; - } -} - -// Simple username input (ASCII-ish) -static void ui_username_prompt(char outName[USERNAME_MAX]) { - memset(outName, 0, USERNAME_MAX); - while (!WindowShouldClose()) { - int ch = GetCharPressed(); - while (ch > 0) { - int len = (int)strlen(outName); - if (ch >= 32 && ch <= 126) { - if (len < USERNAME_MAX - 1) { - outName[len] = (char)ch; - outName[len + 1] = '\0'; - } - } - ch = GetCharPressed(); - } - if (IsKeyPressed(KEY_BACKSPACE)) { - int len = (int)strlen(outName); - if (len > 0) - outName[len - 1] = '\0'; - } - if (IsKeyPressed(KEY_ENTER) && strlen(outName) > 0) - return; - - BeginDrawing(); - ClearBackground((Color){20, 24, 32, 255}); - DrawText("Enter username (press ENTER):", 60, 80, 28, RAYWHITE); - DrawRectangle(60, 130, 420, 48, (Color){40, 48, 64, 255}); - DrawRectangleLines(60, 130, 420, 48, (Color){120, 140, 170, 255}); - DrawText(outName[0] ? outName : "_", 72, 142, 24, - (Color){230, 230, 240, 255}); - EndDrawing(); - } -} - -int main(void) { -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(2, 2), &wsa); -#endif - - const int sw = 1280, sh = 720; - InitWindow(sw, sh, "Voxel Shooter - Client"); - SetTargetFPS(120); - - char myName[USERNAME_MAX]; - ui_username_prompt(myName); - - DisableCursor(); - - int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sock < 0) { - perror("socket"); - return 1; - } - set_nonblocking(sock); - - struct sockaddr_in srv = {0}; - srv.sin_family = AF_INET; - srv.sin_port = htons(SERVER_PORT); - inet_pton(AF_INET, "127.0.0.1", &srv.sin_addr); - - uint8_t myId = 255; - uint32_t clientTick = 0; - - RemotePlayer rp[MAX_PLAYERS] = {0}; - WorldItem wi[MAX_ITEMS] = {0}; - - // view/camera - Vector3 camPos = v3(0, 1.7f, 6); - float yaw = 0.0f; - float pitch = 0.0f; - - // recoil/crosshair - float recoilYaw = 0.0f, recoilPitch = 0.0f; - float crossSpread = 0.0f; - - // firing - float fireCooldown = 0.0f; - int shotsInBurst = 0; - float burstResetTimer = 0.0f; - - // inventory UI - int invOpen = 0; - int selectedSlot = 0; - - // desired weapon - int desiredWeapon = WEAPON_RIFLE; - - // tuning - const float pistolFireRate = 4.0f; - const float rifleFireRate = 12.0f; - const float recoilReturn = 18.0f; - const float crossReturn = 14.0f; - - const float pistolKickPitch = 0.010f; - const float pistolKickYaw = 0.004f; - const float rifleKickPitch = 0.018f; - const float rifleKickYaw = 0.010f; - - const float pistolCrossKick = 2.0f; - const float rifleCrossKick = 4.0f; - - const float pistolSprayGrow = 0.4f; - const float rifleSprayGrow = 1.2f; - - const float burstResetTime = 0.18f; - - // HELLO - MsgHello hello = {0}; - hello.type = MSG_HELLO; - hello.protocol = PROTOCOL_VERSION; - strncpy(hello.username, myName, USERNAME_MAX - 1); - sendto(sock, (const char *)&hello, (int)sizeof(hello), 0, - (const struct sockaddr *)&srv, sizeof(srv)); - - while (!WindowShouldClose()) { - clientTick++; - float dt = GetFrameTime(); - - // timers - fireCooldown -= dt; - if (fireCooldown < 0.0f) - fireCooldown = 0.0f; - burstResetTimer -= dt; - if (burstResetTimer <= 0.0f) - shotsInBurst = 0; - - // recoil return - { - float k = 1.0f - expf(-recoilReturn * dt); - recoilYaw += (0.0f - recoilYaw) * k; - recoilPitch += (0.0f - recoilPitch) * k; - } - // crosshair return - { - float k = 1.0f - expf(-crossReturn * dt); - crossSpread += (0.0f - crossSpread) * k; - if (crossSpread < 0.01f) - crossSpread = 0.0f; - } - - // receive packets - for (;;) { - uint8_t buf[1400]; - struct sockaddr_in from = {0}; - socklen_t fromLen = sizeof(from); - int n = (int)recvfrom(sock, (char *)buf, (int)sizeof(buf), 0, - (struct sockaddr *)&from, &fromLen); - if (n <= 0) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) - break; -#else - if (errno == EWOULDBLOCK || errno == EAGAIN) - break; -#endif - break; - } - - uint8_t type = buf[0]; - if (type == MSG_WELCOME && n >= (int)sizeof(MsgWelcome)) { - MsgWelcome *w = (MsgWelcome *)buf; - myId = w->playerId; - } else if (type == MSG_SNAPSHOT && n >= (int)sizeof(MsgSnapshot)) { - MsgSnapshot *s = (MsgSnapshot *)buf; - for (int i = 0; i < MAX_PLAYERS; i++) - rp[i].present = 0; - - for (int i = 0; i < (int)s->count && i < MAX_PLAYERS; i++) { - PlayerStateNet *ps = &s->p[i]; - if (ps->id >= MAX_PLAYERS) - continue; - - RemotePlayer *p = &rp[ps->id]; - p->present = 1; - p->alive = ps->alive; - p->hp = ps->hp; - p->armor = ps->armor; - p->pos = v3(ps->x, ps->y, ps->z); - p->yaw = ps->yaw; - p->pitch = ps->pitch; - p->weapon = ps->weapon; - - for (int w = 0; w < WEAPON_COUNT; w++) { - p->ammoMag[w] = ps->ammoMag[w]; - p->ammoRes[w] = ps->ammoRes[w]; - } - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - p->invType[sidx] = ps->invType[sidx]; - p->invQty[sidx] = ps->invQty[sidx]; - } - - memset(p->username, 0, USERNAME_MAX); - strncpy(p->username, ps->username, USERNAME_MAX - 1); - } - - if (myId != 255 && rp[myId].present) { - Vector3 body = rp[myId].pos; - camPos.x = body.x; - camPos.z = body.z + 0.0001f; - desiredWeapon = rp[myId].weapon; - } - } else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) { - MsgItems *m = (MsgItems *)buf; - for (int i = 0; i < MAX_ITEMS; i++) - wi[i].present = 0; - - for (int i = 0; i < (int)m->count && i < MAX_ITEMS; i++) { - wi[i].present = 1; - wi[i].id = m->items[i].id; - wi[i].type = m->items[i].type; - wi[i].qty = m->items[i].qty; - wi[i].pos = v3(m->items[i].x, m->items[i].y, m->items[i].z); - } - } - } - - // toggle inventory UI on E - if (IsKeyPressed(KEY_E)) - invOpen = !invOpen; - - // select slot 1..6 - if (IsKeyPressed(KEY_ONE)) - selectedSlot = 0; - if (IsKeyPressed(KEY_TWO)) - selectedSlot = 1; - if (IsKeyPressed(KEY_THREE)) - selectedSlot = 2; - if (IsKeyPressed(KEY_FOUR)) - selectedSlot = 3; - if (IsKeyPressed(KEY_FIVE)) - selectedSlot = 4; - if (IsKeyPressed(KEY_SIX)) - selectedSlot = 5; - - // mouse look: yaw fixed, pitch inverted - Vector2 md = GetMouseDelta(); - const float sens = 0.0025f; - yaw -= md.x * sens; - pitch -= md.y * sens; - if (pitch < -1.5f) - pitch = -1.5f; - if (pitch > 1.5f) - pitch = 1.5f; - - // apply recoil to view - float viewYaw = yaw + recoilYaw; - float viewPitch = pitch + recoilPitch; - if (viewPitch < -1.5f) - viewPitch = -1.5f; - if (viewPitch > 1.5f) - viewPitch = 1.5f; - - // movement (A/D inverted) - float moveX = 0.0f, moveZ = 0.0f; - if (IsKeyDown(KEY_A)) - moveX += 1.0f; - if (IsKeyDown(KEY_D)) - moveX -= 1.0f; - if (IsKeyDown(KEY_W)) - moveZ += 1.0f; - if (IsKeyDown(KEY_S)) - moveZ -= 1.0f; - - // input buttons - uint8_t buttons = 0; - - // weapon switch (separate from slot select) - if (IsKeyPressed(KEY_Z)) { - desiredWeapon = WEAPON_PISTOL; - buttons |= BTN_WPN1; - } - if (IsKeyPressed(KEY_X)) { - desiredWeapon = WEAPON_RIFLE; - buttons |= BTN_WPN2; - } - - if (IsKeyPressed(KEY_R)) - buttons |= BTN_RELOAD; - if (IsKeyPressed(KEY_F)) - buttons |= BTN_PICK; - if (IsKeyPressed(KEY_G)) - buttons |= BTN_DROP; - if (IsKeyPressed(KEY_C)) - buttons |= BTN_USE; // use armor/medkit depending on selected slot - if (IsKeyPressed(KEY_V)) - buttons |= - BTN_USE; // alternate use key (also triggers, server uses slot type) - - // send INPUT - if (myId != 255) { - MsgInput in = {0}; - in.type = MSG_INPUT; - in.playerId = myId; - in.clientTick = clientTick; - in.moveX = moveX; - in.moveZ = moveZ; - in.yaw = viewYaw; - in.pitch = viewPitch; - in.buttons = buttons; - in.slotIndex = (uint8_t)selectedSlot; - sendto(sock, (const char *)&in, (int)sizeof(in), 0, - (const struct sockaddr *)&srv, sizeof(srv)); - } - - // shoot (hold LMB) - if (myId != 255 && IsMouseButtonDown(MOUSE_BUTTON_LEFT) && - fireCooldown <= 0.0f) { - float rate = - (desiredWeapon == WEAPON_PISTOL) ? pistolFireRate : rifleFireRate; - float interval = 1.0f / rate; - fireCooldown = interval; - - shotsInBurst++; - burstResetTimer = burstResetTime; - - if (desiredWeapon == WEAPON_PISTOL) { - recoilPitch += pistolKickPitch; - recoilYaw += - (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * pistolKickYaw; - crossSpread += pistolCrossKick + shotsInBurst * pistolSprayGrow; - } else { - recoilPitch += rifleKickPitch; - recoilYaw += - (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * rifleKickYaw; - crossSpread += rifleCrossKick + shotsInBurst * rifleSprayGrow; - } - - MsgShoot shmsg = {0}; - shmsg.type = MSG_SHOOT; - shmsg.playerId = myId; - shmsg.clientTick = clientTick; - sendto(sock, (const char *)&shmsg, (int)sizeof(shmsg), 0, - (const struct sockaddr *)&srv, sizeof(srv)); - } - - // camera - Vector3 forward = v3(sinf(viewYaw) * cosf(viewPitch), sinf(viewPitch), - cosf(viewYaw) * cosf(viewPitch)); - - Camera3D cam = {0}; - cam.position = camPos; - cam.target = v3_add(camPos, forward); - cam.up = v3(0, 1, 0); - cam.fovy = 75.0f; - cam.projection = CAMERA_PERSPECTIVE; - - BeginDrawing(); - ClearBackground((Color){20, 24, 32, 255}); - - BeginMode3D(cam); - - // ground - for (int z = -16; z <= 16; z++) { - for (int x = -16; x <= 16; x++) { - DrawCubeV(v3((float)x, 0.0f, (float)z), v3(1, 1, 1), - (Color){50, 60, 75, 255}); - } - } - - // blocks - DrawCubeV(v3(2, 1, 2), v3(1, 1, 1), (Color){120, 90, 60, 255}); - DrawCubeV(v3(2, 2, 2), v3(1, 1, 1), (Color){120, 90, 60, 255}); - DrawCubeV(v3(-3, 1, -1), v3(1, 1, 1), (Color){90, 120, 60, 255}); - DrawCubeV(v3(0, 1, 4), v3(2, 1, 2), (Color){90, 90, 130, 255}); - - // items (dropped/pickups) - for (int i = 0; i < MAX_ITEMS; i++) { - if (!wi[i].present) - continue; - Color ic = (Color){220, 220, 220, 255}; - if (wi[i].type == ITEM_MEDKIT) - ic = (Color){120, 255, 120, 255}; - if (wi[i].type == ITEM_ARMORPLATE) - ic = (Color){120, 180, 255, 255}; - if (wi[i].type == ITEM_AMMO_PISTOL) - ic = (Color){255, 220, 120, 255}; - if (wi[i].type == ITEM_AMMO_RIFLE) - ic = (Color){255, 180, 120, 255}; - DrawCubeV(v3(wi[i].pos.x, wi[i].pos.y + 0.15f, wi[i].pos.z), - v3(0.3f, 0.3f, 0.3f), ic); - } - - // players - for (int i = 0; i < MAX_PLAYERS; i++) { - if (!rp[i].present) - continue; - Vector3 p = rp[i].pos; - Color c = - (i == myId) ? (Color){80, 180, 255, 255} : (Color){255, 80, 80, 255}; - if (!rp[i].alive) - c = (Color){120, 120, 120, 255}; - DrawCapsule(v3(p.x, p.y - 0.5f, p.z), v3(p.x, p.y + 0.5f, p.z), 0.35f, 8, - 8, c); - } - - EndMode3D(); - - // nameplates - for (int i = 0; i < MAX_PLAYERS; i++) { - if (!rp[i].present) - continue; - if (!rp[i].username[0]) - continue; - - Vector3 head = v3(rp[i].pos.x, rp[i].pos.y + 1.2f, rp[i].pos.z); - Vector3 camForward = v3_norm(v3_sub(cam.target, cam.position)); - Vector3 toHead = v3_sub(head, cam.position); - if (v3_dot(camForward, toHead) <= 0.0f) - continue; - - Vector2 s = GetWorldToScreen(head, cam); - if (s.x < -200 || s.x > sw + 200 || s.y < -200 || s.y > sh + 200) - continue; - - int fontSize = 18; - int w = MeasureText(rp[i].username, fontSize); - Color tc = (i == myId) ? (Color){180, 230, 255, 255} : RAYWHITE; - DrawText(rp[i].username, (int)(s.x - w / 2), (int)(s.y - fontSize), - fontSize, tc); - } - - // HUD - if (myId == 255 || !rp[myId].present) { - DrawText("Connecting...", 10, 10, 20, RAYWHITE); - } else { - const char *wname = - (rp[myId].weapon == WEAPON_PISTOL) ? "PISTOL" : "RIFLE"; - DrawText(TextFormat("Name: %s | HP: %d | Armor: %d | Weapon: %s", myName, - rp[myId].hp, rp[myId].armor, wname), - 10, 10, 20, RAYWHITE); - - DrawText(TextFormat("Ammo: P %d/%d | R %d/%d (E inv, F pick, G drop, " - "C/V use, R reload, Z/X switch)", - rp[myId].ammoMag[WEAPON_PISTOL], - rp[myId].ammoRes[WEAPON_PISTOL], - rp[myId].ammoMag[WEAPON_RIFLE], - rp[myId].ammoRes[WEAPON_RIFLE]), - 10, 36, 18, (Color){200, 200, 210, 255}); - - if (invOpen) { - DrawRectangle(20, 70, 420, 190, (Color){10, 10, 10, 180}); - DrawRectangleLines(20, 70, 420, 190, (Color){200, 200, 210, 210}); - DrawText("Inventory (slots 1-6)", 32, 80, 22, RAYWHITE); - - for (int sidx = 0; sidx < INV_SLOTS; sidx++) { - Color sc = (sidx == selectedSlot) ? (Color){80, 180, 255, 255} - : (Color){220, 220, 230, 255}; - const char *nm = ItemName(rp[myId].invType[sidx]); - int qty = rp[myId].invQty[sidx]; - DrawText(TextFormat("[%d] %s x%d", sidx + 1, nm, qty), 32, - 110 + sidx * 22, 18, sc); - } - DrawText("Use: C or V | Drop: G | Pick: F", 32, - 110 + INV_SLOTS * 22 + 6, 18, (Color){210, 210, 220, 255}); - } - } - - DrawFPS(sw - 90, 10); - - // crosshair - { - int cx = sw / 2, cy = sh / 2; - int gap = 6 + (int)crossSpread; - int len = 10, thick = 2; - Color col = (Color){240, 240, 245, 220}; - - DrawRectangle(cx - gap - len, cy - thick / 2, len, thick, col); - DrawRectangle(cx + gap, cy - thick / 2, len, thick, col); - DrawRectangle(cx - thick / 2, cy - gap - len, thick, len, col); - DrawRectangle(cx - thick / 2, cy + gap, thick, len, col); - DrawCircleLines(cx, cy, 3.0f, (Color){240, 240, 245, 160}); - } - - EndDrawing(); - } - - CloseWindow(); - CLOSESOCK(sock); -#ifdef _WIN32 - WSACleanup(); -#endif - return 0; -} - -``` - -```plaintext -// game/output.md - -``` -