YAYAYYAYAYAYAYAYY IT WORKS AFTER SPLIITTING IT INTO FILES

This commit is contained in:
flameyosflow 2025-12-20 01:16:32 +02:00
commit e9e7d938ef
23 changed files with 1568 additions and 0 deletions

28
game/build_client.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
# Build the game client using the split modules
set -e
cd "$(dirname "$0")"
# Get raylib compile and link flags
RAYLIB_CFLAGS=$(pkg-config --cflags raylib 2>/dev/null || echo "-I/usr/include")
RAYLIB_LIBS=$(pkg-config --libs raylib 2>/dev/null || echo "-lraylib -lm")
echo "Compiling game modules..."
gcc -c suic_math.c -I../libsuicmez $RAYLIB_CFLAGS -o suic_math.o
gcc -c suic_terrain.c -I../libsuicmez $RAYLIB_CFLAGS -o suic_terrain.o
gcc -c suic_lighting.c -I../libsuicmez $RAYLIB_CFLAGS -o suic_lighting.o
gcc -c suic_net.c -I../libsuicmez $RAYLIB_CFLAGS -o suic_net.o
gcc -c suic_ui.c -I../libsuicmez $RAYLIB_CFLAGS -o suic_ui.o
gcc -c suic_game_client.c -I../libsuicmez $RAYLIB_CFLAGS -o suic_game_client.o
echo "Compiling main entry point..."
gcc -c main.c -I../libsuicmez $RAYLIB_CFLAGS -o main.o
echo "Linking..."
gcc main.o suic_math.o suic_terrain.o suic_lighting.o suic_net.o suic_ui.o suic_game_client.o \
$RAYLIB_LIBS -lm -o game_client
echo "✓ Successfully built: game_client"
echo " Run with: ./game_client"

BIN
game/game_client Executable file

Binary file not shown.

41
game/main.c Normal file
View file

@ -0,0 +1,41 @@
/**
* Game Client Main Entry Point
* Uses the split modules: suic_game_client, suic_terrain, suic_lighting, suic_net, suic_ui
*/
#include "suic_game_client.h"
#include "suic_ui.h"
#include "raylib.h"
int main(void) {
const int screenW = 1280, screenH = 720;
InitWindow(screenW, screenH, "Voxel Shooter - Client");
SetTargetFPS(120);
/* Get username */
char username[SUIC_NET_USERNAME_MAX] = {0};
suic_ui_username_prompt(username, SUIC_NET_USERNAME_MAX);
/* Initialize game subsystems */
suic_game_init(username, "127.0.0.1", 27015);
/* Main loop */
while (!WindowShouldClose()) {
float dt = GetFrameTime();
suic_game_handle_input(dt);
suic_game_update(dt);
BeginDrawing();
ClearBackground((Color){135, 206, 235, 255});
suic_game_render();
EndDrawing();
}
suic_game_shutdown();
CloseWindow();
return 0;
}

BIN
game/main.o Normal file

Binary file not shown.

BIN
game/runtime.h.gch Normal file

Binary file not shown.

280
game/suic_game_client.c Normal file
View file

@ -0,0 +1,280 @@
#include "suic_game_client.h"
#include "raylib.h"
#include <math.h>
#include <string.h>
/* ===== GAME STATE ===== */
static SuicVec3 g_camPos = {0, 5.0f, 6};
static float g_yaw = 0.0f, g_pitch = 0.0f;
static SuicVec3 g_prevBodyPos = {0, 5.0f, 6};
static SuicVec3 g_currentTargetBody = {0, 5.0f, 6};
static float g_interpTimer = 0.0f;
static float g_recoilYaw = 0.0f, g_recoilPitch = 0.0f, g_crossSpread = 0.0f;
static bool g_scoped = false;
static float g_fireCooldown = 0.0f;
static int g_shotsInBurst = 0;
static float g_burstResetTimer = 0.0f;
static uint32_t g_clientTick = 0;
static SuicDirectionalLight g_light;
/* Weapon constants */
static const float PISTOL_FIRE_RATE = 4.0f, RIFLE_FIRE_RATE = 12.0f;
static const float RECOIL_RETURN = 18.0f, CROSS_RETURN = 14.0f;
static const float PISTOL_KICK_PITCH = 0.010f, PISTOL_KICK_YAW = 0.004f;
static const float RIFLE_KICK_PITCH = 0.018f, RIFLE_KICK_YAW = 0.010f;
static const float PISTOL_CROSS_KICK = 2.0f, RIFLE_CROSS_KICK = 4.0f;
static const float PISTOL_SPRAY_GROW = 0.4f, RIFLE_SPRAY_GROW = 1.2f;
static const float BURST_RESET_TIME = 0.18f;
/* ===== LIFECYCLE ===== */
void suic_game_init(const char* username, const char* serverIp, int port) {
suic_terrain_init();
suic_net_init(serverIp, port);
suic_net_send_hello(username);
g_light = suic_light_create_default();
g_camPos = suic_v3(0, 5.0f, 6);
g_yaw = 0.0f;
g_pitch = 0.0f;
DisableCursor();
}
void suic_game_shutdown(void) {
suic_terrain_cleanup();
suic_net_shutdown();
}
/* ===== INPUT HANDLING ===== */
void suic_game_handle_input(float dt) {
g_clientTick++;
/* Cooldowns */
g_fireCooldown -= dt;
if (g_fireCooldown < 0.0f) g_fireCooldown = 0.0f;
g_burstResetTimer -= dt;
if (g_burstResetTimer <= 0.0f) g_shotsInBurst = 0;
/* Recoil recovery */
float k = 1.0f - expf(-RECOIL_RETURN * dt);
g_recoilYaw += (0.0f - g_recoilYaw) * k;
g_recoilPitch += (0.0f - g_recoilPitch) * k;
float k2 = 1.0f - expf(-CROSS_RETURN * dt);
g_crossSpread += (0.0f - g_crossSpread) * k2;
if (g_crossSpread < 0.01f) g_crossSpread = 0.0f;
/* Mouse look */
Vector2 md = GetMouseDelta();
const float sens = 0.0025f;
g_yaw -= md.x * sens;
g_pitch -= md.y * sens;
if (g_pitch < -1.5f) g_pitch = -1.5f;
if (g_pitch > 1.5f) g_pitch = 1.5f;
/* Movement input */
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;
/* Buttons */
uint8_t buttons = 0;
if (IsKeyPressed(KEY_ONE)) buttons |= SUIC_BTN_SWITCH_PISTOL;
if (IsKeyPressed(KEY_TWO)) buttons |= SUIC_BTN_SWITCH_RIFLE;
if (IsKeyPressed(KEY_R)) buttons |= SUIC_BTN_RELOAD;
if (IsKeyPressed(KEY_F)) buttons |= SUIC_BTN_PICK;
if (IsKeyPressed(KEY_H)) buttons |= SUIC_BTN_USE_MEDKIT;
if (IsKeyPressed(KEY_SPACE)) buttons |= SUIC_BTN_JUMP;
if (IsKeyPressed(KEY_Z)) g_scoped = !g_scoped;
uint8_t myId = suic_net_get_my_id();
float viewYaw = g_yaw + g_recoilYaw;
float viewPitch = g_pitch + g_recoilPitch;
if (viewPitch < -1.5f) viewPitch = -1.5f;
if (viewPitch > 1.5f) viewPitch = 1.5f;
if (myId != 255) {
suic_net_send_input(myId, g_clientTick, moveX, moveZ, viewYaw, viewPitch, buttons);
}
/* Shooting */
if (myId != 255 && IsMouseButtonDown(MOUSE_BUTTON_LEFT) && g_fireCooldown <= 0.0f &&
suic_net_player_present(myId)) {
int wpn = suic_net_player_weapon(myId);
float rate = (wpn == SUIC_WEAPON_PISTOL) ? PISTOL_FIRE_RATE : RIFLE_FIRE_RATE;
g_fireCooldown = 1.0f / rate;
g_shotsInBurst++;
g_burstResetTimer = BURST_RESET_TIME;
if (wpn == SUIC_WEAPON_PISTOL) {
g_recoilPitch += PISTOL_KICK_PITCH;
g_recoilYaw += (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * PISTOL_KICK_YAW;
g_crossSpread += PISTOL_CROSS_KICK + g_shotsInBurst * PISTOL_SPRAY_GROW;
} else {
g_recoilPitch += RIFLE_KICK_PITCH;
g_recoilYaw += (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * RIFLE_KICK_YAW;
g_crossSpread += RIFLE_CROSS_KICK + g_shotsInBurst * RIFLE_SPRAY_GROW;
}
suic_net_send_shoot(myId, g_clientTick);
}
}
/* ===== UPDATE ===== */
void suic_game_update(float dt) {
suic_net_poll();
uint8_t myId = suic_net_get_my_id();
if (myId != 255 && suic_net_player_present(myId)) {
g_prevBodyPos = g_currentTargetBody;
g_currentTargetBody = suic_v3(suic_net_player_x(myId),
suic_net_player_y(myId),
suic_net_player_z(myId));
g_interpTimer = 0.0f;
}
/* Interpolate camera */
if (myId != 255 && suic_net_player_present(myId)) {
float interpFactor = g_interpTimer / (1.0f / 20.0f);
if (interpFactor > 1.0f) interpFactor = 1.0f;
SuicVec3 interpBody = suic_v3_add(suic_v3_mul(g_prevBodyPos, 1.0f - interpFactor),
suic_v3_mul(g_currentTargetBody, interpFactor));
g_camPos.x = interpBody.x;
g_camPos.y = interpBody.y + 1.0f;
g_camPos.z = interpBody.z + 0.0001f;
float terrain_h = suic_terrain_height(g_camPos.x, g_camPos.z);
g_camPos.y = fmaxf(g_camPos.y, terrain_h + 1.5f);
}
g_interpTimer += dt;
/* Forward offset */
float viewYaw = g_yaw + g_recoilYaw;
float viewPitch = g_pitch + g_recoilPitch;
SuicVec3 forward = suic_v3(sinf(viewYaw) * cosf(viewPitch), sinf(viewPitch),
cosf(viewYaw) * cosf(viewPitch));
g_camPos = suic_v3_add(g_camPos, suic_v3_mul(forward, 0.3f));
g_camPos.y -= 0.2f;
float terrain_h = suic_terrain_height(g_camPos.x, g_camPos.z);
g_camPos.y = fmaxf(g_camPos.y, terrain_h + 1.5f);
}
/* ===== RENDERING ===== */
void suic_game_render(void) {
int sw = GetScreenWidth();
int sh = GetScreenHeight();
uint8_t myId = suic_net_get_my_id();
float viewYaw = g_yaw + g_recoilYaw;
float viewPitch = g_pitch + g_recoilPitch;
if (viewPitch < -1.5f) viewPitch = -1.5f;
if (viewPitch > 1.5f) viewPitch = 1.5f;
SuicVec3 forward = suic_v3(sinf(viewYaw) * cosf(viewPitch), sinf(viewPitch),
cosf(viewYaw) * cosf(viewPitch));
Camera3D cam = {0};
cam.position = (Vector3){g_camPos.x, g_camPos.y, g_camPos.z};
cam.target = (Vector3){g_camPos.x + forward.x, g_camPos.y + forward.y, g_camPos.z + forward.z};
cam.up = (Vector3){0, 1, 0};
cam.fovy = g_scoped ? 30.0f : 75.0f;
cam.projection = CAMERA_PERSPECTIVE;
BeginMode3D(cam);
/* Terrain */
suic_terrain_draw(g_camPos);
/* Border */
float borderSize = SUIC_TERRAIN_MAX - SUIC_TERRAIN_MIN;
DrawCube((Vector3){0, 0, 0}, borderSize, 1000, borderSize, (Color){255, 0, 0, 100});
/* Items */
for (int i = 0; i < SUIC_NET_MAX_ITEMS; i++) {
if (!suic_net_item_present(i)) continue;
Color ic = (Color){220, 220, 220, 255};
uint8_t itemType = suic_net_item_type(i);
if (itemType == SUIC_ITEM_MEDKIT) ic = (Color){120, 255, 120, 255};
if (itemType == SUIC_ITEM_AMMO_PISTOL) ic = (Color){255, 220, 120, 255};
if (itemType == SUIC_ITEM_AMMO_RIFLE) ic = (Color){255, 180, 120, 255};
SuicVec3 itemPos = suic_v3(suic_net_item_x(i), suic_net_item_y(i), suic_net_item_z(i));
SuicVec3 itemNormal = suic_v3(0, 1, 0);
SuicColor litColor = suic_light_apply_shadows((SuicColor){ic.r, ic.g, ic.b, ic.a},
itemNormal, itemPos, &g_light);
DrawSphere((Vector3){itemPos.x, itemPos.y, itemPos.z}, 0.3f,
(Color){litColor.r, litColor.g, litColor.b, litColor.a});
}
/* Players */
for (int i = 0; i < SUIC_NET_MAX_PLAYERS; i++) {
if (!suic_net_player_present(i)) continue;
float interpFactor = g_interpTimer / (1.0f / 20.0f);
if (interpFactor > 1.0f) interpFactor = 1.0f;
SuicVec3 prev = suic_v3(suic_net_player_prev_x(i), suic_net_player_prev_y(i), suic_net_player_prev_z(i));
SuicVec3 curr = suic_v3(suic_net_player_x(i), suic_net_player_y(i), suic_net_player_z(i));
SuicVec3 p = suic_v3_add(suic_v3_mul(prev, 1.0f - interpFactor), suic_v3_mul(curr, interpFactor));
Color c = (i == myId) ? (Color){80, 180, 255, 255} : (Color){255, 80, 80, 255};
if (!suic_net_player_alive(i)) c = (Color){120, 120, 120, 255};
SuicVec3 playerNormal = suic_v3(0, 1, 0);
SuicColor litColor = suic_light_apply_shadows((SuicColor){c.r, c.g, c.b, c.a},
playerNormal, p, &g_light);
DrawCapsule((Vector3){p.x, p.y - 0.5f, p.z}, (Vector3){p.x, p.y + 0.5f, p.z}, 0.35f, 8, 8,
(Color){litColor.r, litColor.g, litColor.b, litColor.a});
}
EndMode3D();
/* Nameplates */
for (int i = 0; i < SUIC_NET_MAX_PLAYERS; i++) {
if (!suic_net_player_present(i)) continue;
float interpFactor = g_interpTimer / (1.0f / 20.0f);
if (interpFactor > 1.0f) interpFactor = 1.0f;
SuicVec3 prev = suic_v3(suic_net_player_prev_x(i), suic_net_player_prev_y(i), suic_net_player_prev_z(i));
SuicVec3 curr = suic_v3(suic_net_player_x(i), suic_net_player_y(i), suic_net_player_z(i));
SuicVec3 p = suic_v3_add(suic_v3_mul(prev, 1.0f - interpFactor), suic_v3_mul(curr, interpFactor));
suic_ui_draw_nameplate(sw, sh, p.x, p.y, p.z, suic_net_player_username(i), i == myId,
g_camPos.x, g_camPos.y, g_camPos.z,
g_camPos.x + forward.x, g_camPos.y + forward.y, g_camPos.z + forward.z);
}
/* HUD */
if (myId == 255 || !suic_net_player_present(myId)) {
DrawText("Connecting...", 10, 10, 20, RAYWHITE);
} else {
int weapon = suic_net_player_weapon(myId);
int mag = (weapon == SUIC_WEAPON_PISTOL) ? suic_net_player_pistol_mag(myId)
: suic_net_player_rifle_mag(myId);
int reserve = (weapon == SUIC_WEAPON_PISTOL) ? suic_net_player_pistol_ammo(myId)
: suic_net_player_rifle_ammo(myId);
suic_ui_draw_hud(suic_net_player_hp(myId), weapon, mag, reserve,
suic_net_player_medkits(myId), suic_net_player_reload_time(myId));
}
DrawFPS(sw - 90, 10);
/* Room state */
suic_ui_draw_room_state(sw, sh, suic_net_room_state(), suic_net_countdown(), suic_net_winner_name());
/* Crosshair */
suic_ui_draw_crosshair(sw, sh, g_crossSpread, g_scoped);
}
/* ===== ACCESSORS ===== */
SuicVec3 suic_game_get_camera_pos(void) { return g_camPos; }
float suic_game_get_yaw(void) { return g_yaw; }
float suic_game_get_pitch(void) { return g_pitch; }
bool suic_game_is_scoped(void) { return g_scoped; }

27
game/suic_game_client.h Normal file
View file

@ -0,0 +1,27 @@
#pragma once
#include "suic_math.h"
#include "suic_terrain.h"
#include "suic_lighting.h"
#include "suic_net.h"
#include "suic_ui.h"
#include <stdbool.h>
/**
* High-level game client API.
* Ties together terrain, networking, lighting, and rendering.
*/
/* Game lifecycle */
void suic_game_init(const char* username, const char* serverIp, int port);
void suic_game_update(float dt);
void suic_game_render(void);
void suic_game_shutdown(void);
/* Camera state */
SuicVec3 suic_game_get_camera_pos(void);
float suic_game_get_yaw(void);
float suic_game_get_pitch(void);
bool suic_game_is_scoped(void);
/* Input state */
void suic_game_handle_input(float dt);

BIN
game/suic_game_client.o Normal file

Binary file not shown.

150
game/suic_lighting.c Normal file
View file

@ -0,0 +1,150 @@
#include "suic_lighting.h"
#include "suic_terrain.h"
#include <math.h>
/* ===== LIGHT CREATION ===== */
SuicDirectionalLight suic_light_create(float dir_x, float dir_y, float dir_z,
float intensity, float ambient) {
SuicDirectionalLight light = {0};
light.direction = suic_v3_norm(suic_v3(dir_x, dir_y, dir_z));
light.color = suic_v3(1.0f, 1.0f, 1.0f);
light.intensity = intensity;
light.ambientIntensity = ambient;
light.shadowBias = 0.005f;
light.shadowIntensity = 0.4f;
return light;
}
SuicDirectionalLight suic_light_create_default(void) {
return suic_light_create(-0.8f, -1.0f, -0.6f, 1.2f, 0.3f);
}
/* ===== BASIC LIGHTING ===== */
SuicColor suic_light_apply(SuicColor base, SuicVec3 normal, SuicDirectionalLight* light) {
SuicVec3 lightDir = light->direction;
/* Diffuse: Lambertian */
float diff = fmaxf(0.0f, -(lightDir.x * normal.x + lightDir.y * normal.y +
lightDir.z * normal.z));
float brightness = light->ambientIntensity +
(diff * light->intensity * (1.0f - light->ambientIntensity));
int r = (int)(base.r * brightness);
int g = (int)(base.g * brightness);
int b = (int)(base.b * brightness);
if (r > 255) r = 255;
if (g > 255) g = 255;
if (b > 255) b = 255;
return (SuicColor){r, g, b, base.a};
}
/* ===== SHADOW CALCULATIONS ===== */
float suic_shadow_factor(SuicVec3 worldPos, SuicVec3 lightDir, float maxDist) {
float distFromLight = fmaxf(0.0f, worldPos.y - 2.0f);
float shadowIntensity = fmaxf(0.0f, 1.0f - (distFromLight / maxDist));
return 1.0f - (shadowIntensity * 0.15f);
}
float suic_shadow_temporal(SuicVec3 worldPos, float timePhase) {
float noiseVal = sinf(worldPos.x * 0.5f + timePhase) *
cosf(worldPos.z * 0.5f + timePhase);
return 1.0f + (noiseVal * 0.02f);
}
/* ===== LIGHTING WITH SHADOWS ===== */
SuicColor suic_light_apply_shadows(SuicColor base, SuicVec3 normal, SuicVec3 worldPos,
SuicDirectionalLight* light) {
float shadowFactor = suic_shadow_factor(worldPos, light->direction, 10.0f);
float adjustedIntensity = light->intensity * shadowFactor;
SuicVec3 lightDir = light->direction;
float diff = fmaxf(0.0f, -(lightDir.x * normal.x + lightDir.y * normal.y +
lightDir.z * normal.z));
float brightness = light->ambientIntensity +
(diff * adjustedIntensity * (1.0f - light->ambientIntensity));
int r = (int)(base.r * brightness);
int g = (int)(base.g * brightness);
int b = (int)(base.b * brightness);
if (r > 255) r = 255;
if (g > 255) g = 255;
if (b > 255) b = 255;
return (SuicColor){r, g, b, base.a};
}
/* ===== AMBIENT OCCLUSION ===== */
float suic_ao_calculate(SuicVec3 worldPos, SuicVec3 normal) {
float sampleRadius = 3.0f;
float aoAccum = 0.0f;
int numSamples = 8;
float centerHeight = worldPos.y;
float pi = 3.14159265f;
for (int i = 0; i < numSamples; i++) {
float angle = (2.0f * pi * (float)i) / (float)numSamples;
float sx = worldPos.x + cosf(angle) * sampleRadius;
float sz = worldPos.z + sinf(angle) * sampleRadius;
float sh = suic_terrain_height(sx, sz);
float heightDiff = sh - centerHeight;
if (heightDiff > 0.1f) {
float aoAmount = fminf(1.0f, heightDiff / 2.0f);
aoAccum += aoAmount;
}
}
float aoFactor = 1.0f - (aoAccum / (float)numSamples) * 0.6f;
return fmaxf(0.2f, aoFactor);
}
/* ===== PHONG LIGHTING ===== */
SuicColor suic_light_apply_phong(SuicColor base, SuicVec3 normal, SuicVec3 worldPos,
SuicVec3 camPos, SuicDirectionalLight* light) {
SuicVec3 lightDir = suic_v3_norm(light->direction);
SuicVec3 normal_n = suic_v3_norm(normal);
SuicVec3 viewDir = suic_v3_norm(suic_v3_sub(camPos, worldPos));
/* Diffuse */
float diffuse = fmaxf(0.0f, -suic_v3_dot(lightDir, normal_n));
/* Specular: Blinn-Phong */
SuicVec3 negLight = suic_v3(-lightDir.x, -lightDir.y, -lightDir.z);
SuicVec3 halfVec = suic_v3_norm(suic_v3_add(suic_v3_norm(negLight), viewDir));
float specular = powf(fmaxf(0.0f, suic_v3_dot(halfVec, normal_n)), 32.0f) * 0.5f;
/* Ambient occlusion */
float ao = suic_ao_calculate(worldPos, normal_n);
/* Shadow */
float shadowFactor = suic_shadow_factor(worldPos, light->direction, 10.0f);
/* Combine */
float brightness = light->ambientIntensity * ao;
brightness += diffuse * light->intensity * shadowFactor *
(1.0f - light->ambientIntensity) * ao;
brightness += specular * light->intensity * shadowFactor * 0.6f;
brightness = fminf(1.0f, brightness);
int r = (int)(base.r * brightness);
int g = (int)(base.g * brightness);
int b = (int)(base.b * brightness);
if (r > 255) r = 255;
if (g > 255) g = 255;
if (b > 255) b = 255;
return (SuicColor){r, g, b, base.a};
}

44
game/suic_lighting.h Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include "suic_math.h"
#include <stdint.h>
/**
* Lighting system API.
* Supports directional lighting, shadows, ambient occlusion, and Phong shading.
*/
typedef struct SuicColor {
uint8_t r, g, b, a;
} SuicColor;
typedef struct SuicDirectionalLight {
SuicVec3 direction;
SuicVec3 color;
float intensity;
float ambientIntensity;
float shadowBias;
float shadowIntensity;
} SuicDirectionalLight;
/* Light creation */
SuicDirectionalLight suic_light_create(float dir_x, float dir_y, float dir_z,
float intensity, float ambient);
SuicDirectionalLight suic_light_create_default(void);
/* Basic lighting */
SuicColor suic_light_apply(SuicColor base, SuicVec3 normal, SuicDirectionalLight* light);
/* Lighting with shadows */
SuicColor suic_light_apply_shadows(SuicColor base, SuicVec3 normal, SuicVec3 worldPos,
SuicDirectionalLight* light);
/* Phong lighting (with specular) */
SuicColor suic_light_apply_phong(SuicColor base, SuicVec3 normal, SuicVec3 worldPos,
SuicVec3 camPos, SuicDirectionalLight* light);
/* Shadow calculations */
float suic_shadow_factor(SuicVec3 worldPos, SuicVec3 lightDir, float maxDist);
float suic_shadow_temporal(SuicVec3 worldPos, float timePhase);
/* Ambient occlusion */
float suic_ao_calculate(SuicVec3 worldPos, SuicVec3 normal);

BIN
game/suic_lighting.o Normal file

Binary file not shown.

38
game/suic_math.c Normal file
View file

@ -0,0 +1,38 @@
#include "suic_math.h"
#include <math.h>
SuicVec3 suic_v3(float x, float y, float z) {
return (SuicVec3){x, y, z};
}
SuicVec3 suic_v3_add(SuicVec3 a, SuicVec3 b) {
return suic_v3(a.x + b.x, a.y + b.y, a.z + b.z);
}
SuicVec3 suic_v3_sub(SuicVec3 a, SuicVec3 b) {
return suic_v3(a.x - b.x, a.y - b.y, a.z - b.z);
}
SuicVec3 suic_v3_mul(SuicVec3 a, float s) {
return suic_v3(a.x * s, a.y * s, a.z * s);
}
float suic_v3_dot(SuicVec3 a, SuicVec3 b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
float suic_v3_len(SuicVec3 a) {
return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z);
}
SuicVec3 suic_v3_norm(SuicVec3 a) {
float l = suic_v3_len(a);
if (l <= 1e-6f) return suic_v3(0, 0, 1);
return suic_v3(a.x / l, a.y / l, a.z / l);
}
SuicVec3 suic_v3_normalize(SuicVec3 v) {
float len = sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);
if (len < 0.0001f) return suic_v3(0, 1, 0);
return suic_v3(v.x / len, v.y / len, v.z / len);
}

25
game/suic_math.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <stdbool.h>
/**
* Vector3 math utilities for game code.
* Designed to be callable from .sui via transpiler bindings.
*/
typedef struct SuicVec3 {
float x, y, z;
} SuicVec3;
/* Construction */
SuicVec3 suic_v3(float x, float y, float z);
/* Arithmetic */
SuicVec3 suic_v3_add(SuicVec3 a, SuicVec3 b);
SuicVec3 suic_v3_sub(SuicVec3 a, SuicVec3 b);
SuicVec3 suic_v3_mul(SuicVec3 a, float s);
/* Dot/Length/Normalize */
float suic_v3_dot(SuicVec3 a, SuicVec3 b);
float suic_v3_len(SuicVec3 a);
SuicVec3 suic_v3_norm(SuicVec3 a);
SuicVec3 suic_v3_normalize(SuicVec3 v);

BIN
game/suic_math.o Normal file

Binary file not shown.

337
game/suic_net.c Normal file
View file

@ -0,0 +1,337 @@
#include "suic_net.h"
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#define CLOSESOCK closesocket
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#define CLOSESOCK close
#endif
#define PROTOCOL_VERSION 67
/* ===== MESSAGE TYPES ===== */
#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_ITEMS = 6,
MSG_ROOM_STATE = 7
} MsgType;
typedef struct MsgHello {
uint8_t type;
uint32_t protocol;
char username[SUIC_NET_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, moveZ, yaw, pitch;
uint8_t buttons;
} MsgInput;
typedef struct MsgShoot {
uint8_t type;
uint8_t playerId;
uint32_t clientTick;
} MsgShoot;
typedef struct PlayerStateNet {
uint8_t id, alive;
int16_t hp;
float x, y, z, yaw, pitch;
uint8_t weapon;
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
int16_t reloadTimeLeft;
char username[SUIC_NET_USERNAME_MAX];
} PlayerStateNet;
typedef struct MsgSnapshot {
uint8_t type;
uint32_t serverTick;
uint8_t count;
PlayerStateNet p[SUIC_NET_MAX_PLAYERS];
} MsgSnapshot;
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[SUIC_NET_MAX_ITEMS];
} MsgItems;
typedef struct MsgRoomState {
uint8_t type;
uint8_t state;
float countdownRemaining;
uint8_t winnerId;
char winnerName[SUIC_NET_USERNAME_MAX];
} MsgRoomState;
#pragma pack(pop)
/* ===== LOCAL PLAYER DATA ===== */
typedef struct RemotePlayer {
int present, alive, hp;
float x, y, z;
float prevX, prevY, prevZ;
float yaw, pitch;
uint8_t weapon;
int16_t pistolMag, rifleMag, pistolAmmo, rifleAmmo, medkits;
int16_t reloadTimeLeft;
char username[SUIC_NET_USERNAME_MAX];
} RemotePlayer;
typedef struct WorldItem {
int present;
uint16_t id;
uint8_t type;
int16_t qty;
float x, y, z;
} WorldItem;
/* ===== STATIC STATE ===== */
static int g_sock = -1;
static struct sockaddr_in g_server = {0};
static uint8_t g_myId = 255;
static RemotePlayer g_players[SUIC_NET_MAX_PLAYERS] = {0};
static WorldItem g_items[SUIC_NET_MAX_ITEMS] = {0};
static int g_roomState = 0;
static float g_countdown = 0.0f;
static char g_winnerName[SUIC_NET_USERNAME_MAX] = {0};
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
}
/* ===== PUBLIC API ===== */
int suic_net_init(const char* server_ip, int port) {
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
g_sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (g_sock < 0) {
perror("socket");
return -1;
}
set_nonblocking(g_sock);
memset(&g_server, 0, sizeof(g_server));
g_server.sin_family = AF_INET;
g_server.sin_port = htons(port);
inet_pton(AF_INET, server_ip, &g_server.sin_addr);
g_myId = 255;
memset(g_players, 0, sizeof(g_players));
memset(g_items, 0, sizeof(g_items));
return 0;
}
void suic_net_shutdown(void) {
if (g_sock >= 0) {
CLOSESOCK(g_sock);
g_sock = -1;
}
#ifdef _WIN32
WSACleanup();
#endif
}
bool suic_net_is_connected(void) {
return g_myId != 255;
}
void suic_net_send_hello(const char* username) {
MsgHello hello = {0};
hello.type = MSG_HELLO;
hello.protocol = PROTOCOL_VERSION;
strncpy(hello.username, username, SUIC_NET_USERNAME_MAX - 1);
sendto(g_sock, (const char*)&hello, (int)sizeof(hello), 0,
(const struct sockaddr*)&g_server, sizeof(g_server));
}
void suic_net_send_input(uint8_t playerId, uint32_t clientTick,
float moveX, float moveZ, float yaw, float pitch,
uint8_t buttons) {
MsgInput in = {0};
in.type = MSG_INPUT;
in.playerId = playerId;
in.clientTick = clientTick;
in.moveX = moveX;
in.moveZ = moveZ;
in.yaw = yaw;
in.pitch = pitch;
in.buttons = buttons;
sendto(g_sock, (const char*)&in, (int)sizeof(in), 0,
(const struct sockaddr*)&g_server, sizeof(g_server));
}
void suic_net_send_shoot(uint8_t playerId, uint32_t clientTick) {
MsgShoot msg = {0};
msg.type = MSG_SHOOT;
msg.playerId = playerId;
msg.clientTick = clientTick;
sendto(g_sock, (const char*)&msg, (int)sizeof(msg), 0,
(const struct sockaddr*)&g_server, sizeof(g_server));
}
bool suic_net_poll(void) {
bool receivedAny = false;
for (;;) {
uint8_t buf[1400];
struct sockaddr_in from = {0};
socklen_t fromLen = sizeof(from);
int n = (int)recvfrom(g_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;
}
receivedAny = true;
uint8_t type = buf[0];
if (type == MSG_WELCOME && n >= (int)sizeof(MsgWelcome)) {
MsgWelcome* w = (MsgWelcome*)buf;
g_myId = w->playerId;
} else if (type == MSG_SNAPSHOT && n >= (int)sizeof(MsgSnapshot)) {
MsgSnapshot* s = (MsgSnapshot*)buf;
for (int i = 0; i < SUIC_NET_MAX_PLAYERS; i++)
g_players[i].present = 0;
for (int i = 0; i < (int)s->count && i < SUIC_NET_MAX_PLAYERS; i++) {
PlayerStateNet* ps = &s->p[i];
if (ps->id >= SUIC_NET_MAX_PLAYERS) continue;
RemotePlayer* p = &g_players[ps->id];
p->prevX = p->x;
p->prevY = p->y;
p->prevZ = p->z;
p->present = 1;
p->alive = ps->alive;
p->hp = ps->hp;
p->x = ps->x;
p->y = ps->y;
p->z = ps->z;
p->yaw = ps->yaw;
p->pitch = ps->pitch;
p->weapon = ps->weapon;
p->pistolMag = ps->pistolMag;
p->rifleMag = ps->rifleMag;
p->pistolAmmo = ps->pistolAmmo;
p->rifleAmmo = ps->rifleAmmo;
p->medkits = ps->medkits;
p->reloadTimeLeft = ps->reloadTimeLeft;
memset(p->username, 0, SUIC_NET_USERNAME_MAX);
strncpy(p->username, ps->username, SUIC_NET_USERNAME_MAX - 1);
}
} else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) {
MsgItems* m = (MsgItems*)buf;
for (int i = 0; i < SUIC_NET_MAX_ITEMS; i++)
g_items[i].present = 0;
for (int i = 0; i < (int)m->count && i < SUIC_NET_MAX_ITEMS; i++) {
g_items[i].present = 1;
g_items[i].id = m->items[i].id;
g_items[i].type = m->items[i].type;
g_items[i].qty = m->items[i].qty;
g_items[i].x = m->items[i].x;
g_items[i].y = m->items[i].y;
g_items[i].z = m->items[i].z;
}
} else if (type == MSG_ROOM_STATE && n >= (int)sizeof(MsgRoomState)) {
const MsgRoomState* rs = (const MsgRoomState*)buf;
g_roomState = rs->state;
g_countdown = rs->countdownRemaining;
memset(g_winnerName, 0, SUIC_NET_USERNAME_MAX);
if (rs->winnerId < 255) {
strncpy(g_winnerName, rs->winnerName, SUIC_NET_USERNAME_MAX - 1);
}
}
}
return receivedAny;
}
uint8_t suic_net_get_my_id(void) { return g_myId; }
/* Player accessors */
bool suic_net_player_present(int i) { return i >= 0 && i < SUIC_NET_MAX_PLAYERS && g_players[i].present; }
bool suic_net_player_alive(int i) { return suic_net_player_present(i) && g_players[i].alive; }
int suic_net_player_hp(int i) { return suic_net_player_present(i) ? g_players[i].hp : 0; }
float suic_net_player_x(int i) { return suic_net_player_present(i) ? g_players[i].x : 0; }
float suic_net_player_y(int i) { return suic_net_player_present(i) ? g_players[i].y : 0; }
float suic_net_player_z(int i) { return suic_net_player_present(i) ? g_players[i].z : 0; }
float suic_net_player_prev_x(int i) { return suic_net_player_present(i) ? g_players[i].prevX : 0; }
float suic_net_player_prev_y(int i) { return suic_net_player_present(i) ? g_players[i].prevY : 0; }
float suic_net_player_prev_z(int i) { return suic_net_player_present(i) ? g_players[i].prevZ : 0; }
float suic_net_player_yaw(int i) { return suic_net_player_present(i) ? g_players[i].yaw : 0; }
float suic_net_player_pitch(int i) { return suic_net_player_present(i) ? g_players[i].pitch : 0; }
uint8_t suic_net_player_weapon(int i) { return suic_net_player_present(i) ? g_players[i].weapon : 0; }
int16_t suic_net_player_pistol_mag(int i) { return suic_net_player_present(i) ? g_players[i].pistolMag : 0; }
int16_t suic_net_player_rifle_mag(int i) { return suic_net_player_present(i) ? g_players[i].rifleMag : 0; }
int16_t suic_net_player_pistol_ammo(int i) { return suic_net_player_present(i) ? g_players[i].pistolAmmo : 0; }
int16_t suic_net_player_rifle_ammo(int i) { return suic_net_player_present(i) ? g_players[i].rifleAmmo : 0; }
int16_t suic_net_player_medkits(int i) { return suic_net_player_present(i) ? g_players[i].medkits : 0; }
int16_t suic_net_player_reload_time(int i) { return suic_net_player_present(i) ? g_players[i].reloadTimeLeft : 0; }
const char* suic_net_player_username(int i) { return suic_net_player_present(i) ? g_players[i].username : ""; }
/* Item accessors */
bool suic_net_item_present(int i) { return i >= 0 && i < SUIC_NET_MAX_ITEMS && g_items[i].present; }
uint16_t suic_net_item_id(int i) { return suic_net_item_present(i) ? g_items[i].id : 0; }
uint8_t suic_net_item_type(int i) { return suic_net_item_present(i) ? g_items[i].type : 0; }
int16_t suic_net_item_qty(int i) { return suic_net_item_present(i) ? g_items[i].qty : 0; }
float suic_net_item_x(int i) { return suic_net_item_present(i) ? g_items[i].x : 0; }
float suic_net_item_y(int i) { return suic_net_item_present(i) ? g_items[i].y : 0; }
float suic_net_item_z(int i) { return suic_net_item_present(i) ? g_items[i].z : 0; }
/* Room state */
int suic_net_room_state(void) { return g_roomState; }
float suic_net_countdown(void) { return g_countdown; }
const char* suic_net_winner_name(void) { return g_winnerName; }

81
game/suic_net.h Normal file
View file

@ -0,0 +1,81 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
/**
* Network protocol API for game client.
* Abstracts UDP socket communication with the game server.
*/
#define SUIC_NET_MAX_PLAYERS 16
#define SUIC_NET_USERNAME_MAX 16
#define SUIC_NET_MAX_ITEMS 64
/* Connection lifecycle */
int suic_net_init(const char* server_ip, int port);
void suic_net_shutdown(void);
bool suic_net_is_connected(void);
/* Sending */
void suic_net_send_hello(const char* username);
void suic_net_send_input(uint8_t playerId, uint32_t clientTick,
float moveX, float moveZ, float yaw, float pitch,
uint8_t buttons);
void suic_net_send_shoot(uint8_t playerId, uint32_t clientTick);
/* Receiving (poll-based) */
bool suic_net_poll(void);
uint8_t suic_net_get_my_id(void);
/* Player accessors */
bool suic_net_player_present(int index);
bool suic_net_player_alive(int index);
int suic_net_player_hp(int index);
float suic_net_player_x(int index);
float suic_net_player_y(int index);
float suic_net_player_z(int index);
float suic_net_player_prev_x(int index);
float suic_net_player_prev_y(int index);
float suic_net_player_prev_z(int index);
float suic_net_player_yaw(int index);
float suic_net_player_pitch(int index);
uint8_t suic_net_player_weapon(int index);
int16_t suic_net_player_pistol_mag(int index);
int16_t suic_net_player_rifle_mag(int index);
int16_t suic_net_player_pistol_ammo(int index);
int16_t suic_net_player_rifle_ammo(int index);
int16_t suic_net_player_medkits(int index);
int16_t suic_net_player_reload_time(int index);
const char* suic_net_player_username(int index);
/* Item accessors */
bool suic_net_item_present(int index);
uint16_t suic_net_item_id(int index);
uint8_t suic_net_item_type(int index);
int16_t suic_net_item_qty(int index);
float suic_net_item_x(int index);
float suic_net_item_y(int index);
float suic_net_item_z(int index);
/* Room state */
int suic_net_room_state(void);
float suic_net_countdown(void);
const char* suic_net_winner_name(void);
/* Button flags */
#define SUIC_BTN_RELOAD (1u << 0)
#define SUIC_BTN_SWITCH_PISTOL (1u << 1)
#define SUIC_BTN_SWITCH_RIFLE (1u << 2)
#define SUIC_BTN_PICK (1u << 3)
#define SUIC_BTN_USE_MEDKIT (1u << 4)
#define SUIC_BTN_JUMP (1u << 5)
/* Weapon types */
#define SUIC_WEAPON_PISTOL 0
#define SUIC_WEAPON_RIFLE 1
/* Item types */
#define SUIC_ITEM_NONE 0
#define SUIC_ITEM_MEDKIT 1
#define SUIC_ITEM_AMMO_PISTOL 2
#define SUIC_ITEM_AMMO_RIFLE 3

BIN
game/suic_net.o Normal file

Binary file not shown.

283
game/suic_terrain.c Normal file
View file

@ -0,0 +1,283 @@
#include "suic_terrain.h"
#include "raylib.h"
#include <math.h>
#include <stdint.h>
#include <stddef.h>
/* ===== SIMPLEX NOISE IMPLEMENTATION ===== */
static const uint8_t perm[512] = {
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7,
225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190,
6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117,
35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136,
171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158,
231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46,
245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209,
76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5,
202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16,
58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44,
154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253,
19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97,
228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51,
145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184,
84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156,
180, 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233,
7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203,
117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125,
136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146,
158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55,
46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73,
209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159,
86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47,
16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2,
44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39,
253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246,
97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81,
51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205,
93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61,
156, 180};
static float grad2(int hash, float x, float y) {
int h = hash & 7;
float u = h < 4 ? x : y;
float v = h < 4 ? y : x;
return ((h & 1) ? -u : u) + ((h & 2) ? -2.0f * v : 2.0f * v);
}
float suic_simplex_noise_2d(float x, float y) {
const float F2 = 0.366025403f;
const float G2 = 0.211324865f;
float s = (x + y) * F2;
int i = (int)floorf(x + s);
int j = (int)floorf(y + s);
float t = (i + j) * G2;
float X0 = i - t, Y0 = j - t;
float x0 = x - X0, y0 = y - Y0;
int i1 = (x0 > y0) ? 1 : 0;
int j1 = (x0 > y0) ? 0 : 1;
float x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
float x2 = x0 - 1.0f + 2.0f * G2, y2 = y0 - 1.0f + 2.0f * G2;
int ii = i & 255, jj = j & 255;
float n0 = 0.0f, n1 = 0.0f, n2 = 0.0f;
float t0 = 0.5f - x0 * x0 - y0 * y0;
if (t0 >= 0.0f) {
t0 *= t0;
n0 = t0 * t0 * grad2(perm[ii + perm[jj]], x0, y0);
}
float t1 = 0.5f - x1 * x1 - y1 * y1;
if (t1 >= 0.0f) {
t1 *= t1;
n1 = t1 * t1 * grad2(perm[ii + i1 + perm[jj + j1]], x1, y1);
}
float t2 = 0.5f - x2 * x2 - y2 * y2;
if (t2 >= 0.0f) {
t2 *= t2;
n2 = t2 * t2 * grad2(perm[ii + 1 + perm[jj + 1]], x2, y2);
}
return 45.0f * (n0 + n1 + n2);
}
float suic_fbm_noise(float x, float y, int octaves) {
float value = 0.0f, amplitude = 1.0f, frequency = 1.0f, max_value = 0.0f;
for (int i = 0; i < octaves; i++) {
value += suic_simplex_noise_2d(x * frequency, y * frequency) * amplitude;
max_value += amplitude;
amplitude *= 0.5f;
frequency *= 2.0f;
}
return value / max_value;
}
float suic_terrain_height(float x, float z) {
float height = suic_fbm_noise(x * 0.05f, z * 0.05f, 2);
height += suic_fbm_noise(x * 0.01f, z * 0.01f, 2) * 1.5f;
return height * 4.0f + 2.0f;
}
/* ===== TERRAIN MESH & MODEL ===== */
static Mesh g_terrainMesh = {0};
static Model g_terrainModel = {0};
static SuicTerrainShader g_terrainShader = {0};
static bool g_terrainInitialized = false;
static Mesh generate_terrain_mesh(void) {
int size = SUIC_TERRAIN_SIZE;
Mesh mesh = {0};
int vertexCount = size * size;
int triangleCount = (size - 1) * (size - 1) * 2;
mesh.vertexCount = vertexCount;
mesh.triangleCount = triangleCount;
mesh.vertices = (float *)MemAlloc(vertexCount * 3 * sizeof(float));
mesh.texcoords = (float *)MemAlloc(vertexCount * 2 * sizeof(float));
mesh.normals = (float *)MemAlloc(vertexCount * 3 * sizeof(float));
mesh.indices = (unsigned short *)MemAlloc(triangleCount * 3 * sizeof(unsigned short));
/* Generate vertices */
for (int z = 0; z < size; z++) {
for (int x = 0; x < size; x++) {
int idx = z * size + x;
float wx = ((float)x - size / 2.0f) * SUIC_TERRAIN_SCALE;
float wz = ((float)z - size / 2.0f) * SUIC_TERRAIN_SCALE;
float wy = suic_terrain_height(wx, wz);
mesh.vertices[idx * 3 + 0] = wx;
mesh.vertices[idx * 3 + 1] = wy;
mesh.vertices[idx * 3 + 2] = wz;
mesh.texcoords[idx * 2 + 0] = (float)x / (float)(size - 1);
mesh.texcoords[idx * 2 + 1] = (float)z / (float)(size - 1);
}
}
/* Generate indices */
int triIdx = 0;
for (int z = 0; z < size - 1; z++) {
for (int x = 0; x < size - 1; x++) {
int i0 = z * size + x;
int i1 = z * size + (x + 1);
int i2 = (z + 1) * size + x;
int i3 = (z + 1) * size + (x + 1);
mesh.indices[triIdx * 3 + 0] = i0;
mesh.indices[triIdx * 3 + 1] = i2;
mesh.indices[triIdx * 3 + 2] = i1;
triIdx++;
mesh.indices[triIdx * 3 + 0] = i1;
mesh.indices[triIdx * 3 + 1] = i2;
mesh.indices[triIdx * 3 + 2] = i3;
triIdx++;
}
}
/* Calculate normals using height field gradient */
for (int z = 0; z < size; z++) {
for (int x = 0; x < size; x++) {
int idx = z * size + x;
float h_right = (x + 1 < size)
? mesh.vertices[(z * size + (x + 1)) * 3 + 1]
: mesh.vertices[idx * 3 + 1];
float h_left = (x - 1 >= 0) ? mesh.vertices[(z * size + (x - 1)) * 3 + 1]
: mesh.vertices[idx * 3 + 1];
float h_down = (z + 1 < size)
? mesh.vertices[((z + 1) * size + x) * 3 + 1]
: mesh.vertices[idx * 3 + 1];
float h_up = (z - 1 >= 0) ? mesh.vertices[((z - 1) * size + x) * 3 + 1]
: mesh.vertices[idx * 3 + 1];
float dh_dx = (h_right - h_left) / (2.0f * SUIC_TERRAIN_SCALE);
float dh_dz = (h_down - h_up) / (2.0f * SUIC_TERRAIN_SCALE);
float nx = -dh_dx;
float ny = 1.0f;
float nz = -dh_dz;
float len = sqrtf(nx * nx + ny * ny + nz * nz);
if (len > 0.0001f) {
mesh.normals[idx * 3 + 0] = nx / len;
mesh.normals[idx * 3 + 1] = ny / len;
mesh.normals[idx * 3 + 2] = nz / len;
} else {
mesh.normals[idx * 3 + 0] = 0;
mesh.normals[idx * 3 + 1] = 1;
mesh.normals[idx * 3 + 2] = 0;
}
}
}
UploadMesh(&mesh, false);
return mesh;
}
void suic_terrain_init(void) {
if (g_terrainInitialized) return;
g_terrainMesh = generate_terrain_mesh();
g_terrainModel = LoadModelFromMesh(g_terrainMesh);
g_terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color = (Color){80, 140, 70, 255};
g_terrainShader = suic_terrain_shader_load();
if (g_terrainShader.shaderId != 0) {
g_terrainModel.materials[0].shader.id = g_terrainShader.shaderId;
}
g_terrainInitialized = true;
}
void suic_terrain_draw(SuicVec3 cameraPos) {
if (!g_terrainInitialized) return;
/* Set shader uniforms if shader is loaded */
if (g_terrainShader.shaderId != 0) {
SuicVec3 lightDir = suic_v3_norm(suic_v3(-0.8f, -1.0f, -0.6f));
SuicVec3 lightColor = suic_v3(1.0f, 1.0f, 1.0f);
suic_terrain_shader_set_uniforms(&g_terrainShader, cameraPos, lightDir, lightColor, 1.2f, 0.3f);
}
g_terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].color = (Color){80, 140, 70, 255};
DrawModel(g_terrainModel, (Vector3){0, 0, 0}, 1.0f, WHITE);
}
void suic_terrain_cleanup(void) {
if (!g_terrainInitialized) return;
UnloadModel(g_terrainModel);
suic_terrain_shader_unload(&g_terrainShader);
g_terrainInitialized = false;
}
/* ===== SHADER MANAGEMENT ===== */
SuicTerrainShader suic_terrain_shader_load(void) {
SuicTerrainShader ts = {0};
Shader shader = LoadShader("terrain.vs", "terrain.fs");
ts.shaderId = shader.id;
if (shader.id != 0) {
ts.locViewPos = GetShaderLocation(shader, "viewPos");
ts.locLightDir = GetShaderLocation(shader, "lightDir");
ts.locLightColor = GetShaderLocation(shader, "lightColor");
ts.locLightIntensity = GetShaderLocation(shader, "lightIntensity");
ts.locAmbientIntensity = GetShaderLocation(shader, "ambientIntensity");
ts.locTerrainColor = GetShaderLocation(shader, "terrainColor");
}
return ts;
}
void suic_terrain_shader_unload(SuicTerrainShader* ts) {
if (ts && ts->shaderId != 0) {
Shader s = {ts->shaderId, NULL};
UnloadShader(s);
ts->shaderId = 0;
}
}
void suic_terrain_shader_set_uniforms(SuicTerrainShader* ts, SuicVec3 viewPos,
SuicVec3 lightDir, SuicVec3 lightColor,
float intensity, float ambient) {
if (!ts || ts->shaderId == 0) return;
Shader shader = {ts->shaderId, NULL};
float viewPosArray[3] = {viewPos.x, viewPos.y, viewPos.z};
float lightDirArray[3] = {-lightDir.x, -lightDir.y, -lightDir.z};
float lightColorArray[3] = {lightColor.x, lightColor.y, lightColor.z};
float terrainColorArray[3] = {80.0f / 255.0f, 140.0f / 255.0f, 70.0f / 255.0f};
SetShaderValue(shader, ts->locViewPos, viewPosArray, SHADER_UNIFORM_VEC3);
SetShaderValue(shader, ts->locLightDir, lightDirArray, SHADER_UNIFORM_VEC3);
SetShaderValue(shader, ts->locLightColor, lightColorArray, SHADER_UNIFORM_VEC3);
SetShaderValue(shader, ts->locLightIntensity, &intensity, SHADER_UNIFORM_FLOAT);
SetShaderValue(shader, ts->locAmbientIntensity, &ambient, SHADER_UNIFORM_FLOAT);
SetShaderValue(shader, ts->locTerrainColor, terrainColorArray, SHADER_UNIFORM_VEC3);
}

43
game/suic_terrain.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "suic_math.h"
#include <stdbool.h>
/**
* Terrain generation and rendering API.
* Uses simplex noise for procedural heightmap generation.
*/
/* Terrain constants */
#define SUIC_TERRAIN_SIZE 256
#define SUIC_TERRAIN_SCALE 1.0f
#define SUIC_TERRAIN_MIN (-SUIC_TERRAIN_SIZE * SUIC_TERRAIN_SCALE / 2.0f)
#define SUIC_TERRAIN_MAX (SUIC_TERRAIN_SIZE * SUIC_TERRAIN_SCALE / 2.0f)
/* Noise generation */
float suic_simplex_noise_2d(float x, float y);
float suic_fbm_noise(float x, float y, int octaves);
/* Terrain height query */
float suic_terrain_height(float x, float z);
/* Terrain lifecycle */
void suic_terrain_init(void);
void suic_terrain_draw(SuicVec3 cameraPos);
void suic_terrain_cleanup(void);
/* Shader management */
typedef struct SuicTerrainShader {
unsigned int shaderId;
int locViewPos;
int locLightDir;
int locLightColor;
int locLightIntensity;
int locAmbientIntensity;
int locTerrainColor;
} SuicTerrainShader;
SuicTerrainShader suic_terrain_shader_load(void);
void suic_terrain_shader_unload(SuicTerrainShader* ts);
void suic_terrain_shader_set_uniforms(SuicTerrainShader* ts, SuicVec3 viewPos,
SuicVec3 lightDir, SuicVec3 lightColor,
float intensity, float ambient);

BIN
game/suic_terrain.o Normal file

Binary file not shown.

163
game/suic_ui.c Normal file
View file

@ -0,0 +1,163 @@
#include "suic_ui.h"
#include "suic_net.h"
#include "raylib.h"
#include <string.h>
#include <stdio.h>
#include <math.h>
/* ===== USERNAME PROMPT ===== */
void suic_ui_username_prompt(char* outName, int maxLen) {
memset(outName, 0, maxLen);
while (!WindowShouldClose()) {
int ch = GetCharPressed();
while (ch > 0) {
int len = (int)strlen(outName);
if (ch >= 32 && ch <= 126) {
if (len < maxLen - 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();
}
}
/* ===== HUD ===== */
void suic_ui_draw_hud(int hp, int weapon, int mag, int reserve,
int medkits, int reloadTimeLeft) {
int sw = GetScreenWidth();
int sh = GetScreenHeight();
/* Health panel */
DrawRectangle(10, 10, 280, 110, (Color){0, 0, 0, 120});
DrawText("HP", 20, 20, 20, RAYWHITE);
int healthBarWidth = (hp * 220) / 100;
Color healthColor = (hp > 60) ? (Color){80, 255, 80, 120}
: (hp > 30) ? (Color){255, 200, 80, 120}
: (Color){255, 80, 80, 120};
DrawRectangle(20, 45, 220, 20, (Color){40, 40, 40, 255});
DrawRectangle(20, 45, healthBarWidth, 20, healthColor);
DrawText(TextFormat("%d", hp), 250, 47, 18, RAYWHITE);
DrawText(TextFormat("Medkits: %d (H to use)", medkits), 20, 75, 18,
(medkits > 0) ? (Color){120, 255, 120, 120} : (Color){120, 120, 120, 120});
DrawText("1=Pistol 2=Rifle R=Reload F=Pick SPACE=Jump", 20, 95, 12,
(Color){150, 150, 150, 150});
/* Weapon panel */
const char* weaponName = (weapon == SUIC_WEAPON_PISTOL) ? "PISTOL" : "RIFLE";
Color weaponColor = (weapon == SUIC_WEAPON_PISTOL)
? (Color){100, 200, 255, 255}
: (Color){255, 150, 100, 255};
DrawRectangle(sw - 260, sh - 120, 250, 110, (Color){0, 0, 0, 180});
DrawText(weaponName, sw - 250, sh - 110, 28, weaponColor);
DrawText(TextFormat("%d", mag), sw - 250, sh - 75, 40, RAYWHITE);
DrawText(TextFormat("/ %d", reserve), sw - 140, sh - 65, 24, (Color){180, 180, 180, 255});
if (reloadTimeLeft > 0)
DrawText("RELOADING...", sw - 250, sh - 30, 20, (Color){255, 200, 80, 255});
else if (mag == 0)
DrawText("RELOAD!", sw - 250, sh - 30, 20, (Color){255, 80, 80, 255});
}
/* ===== CROSSHAIR ===== */
void suic_ui_draw_crosshair(int screenW, int screenH, float spread, bool scoped) {
int cx = screenW / 2, cy = screenH / 2;
int gap = scoped ? 2 : 6 + (int)spread;
int len = scoped ? 5 : 10;
int 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, scoped ? 1.5f : 3.0f, (Color){240, 240, 245, 160});
}
/* ===== ROOM STATE ===== */
void suic_ui_draw_room_state(int screenW, int screenH, int state,
float countdown, const char* winnerName) {
if (state == 0) { /* Waiting */
DrawRectangle(screenW / 2 - 150, screenH / 2 - 50, 350, 100, (Color){0, 0, 0, 200});
DrawText("WAITING FOR PLAYERS", screenW / 2 - 120, screenH / 2 - 30, 24, RAYWHITE);
DrawText("Need at least 2 players", screenW / 2 - 100, screenH / 2 - 5, 18,
(Color){200, 200, 200, 255});
} else if (state == 1) { /* Counting down */
DrawRectangle(screenW / 2 - 150, screenH / 2 - 50, 350, 100, (Color){0, 0, 0, 200});
DrawText("GAME STARTING SOON", screenW / 2 - 120, screenH / 2 - 30, 24,
(Color){255, 255, 80, 255});
DrawText(TextFormat("%.1f seconds", countdown), screenW / 2 - 60,
screenH / 2 - 5, 20, RAYWHITE);
} else if (state == 3) { /* Finished */
DrawRectangle(screenW / 2 - 200, screenH / 2 - 50, 450, 100, (Color){0, 0, 0, 200});
DrawText("GAME FINISHED", screenW / 2 - 80, screenH / 2 - 30, 28,
(Color){255, 80, 80, 255});
if (winnerName && winnerName[0]) {
DrawText(TextFormat("Winner: %s", winnerName), screenW / 2 - 100, screenH / 2 - 5,
24, (Color){255, 255, 80, 255});
} else {
DrawText("No winner", screenW / 2 - 50, screenH / 2 - 5, 24, RAYWHITE);
}
}
}
/* ===== NAMEPLATES ===== */
void suic_ui_draw_nameplate(int screenW, int screenH, float worldX, float worldY, float worldZ,
const char* username, bool isLocalPlayer,
float camPosX, float camPosY, float camPosZ,
float camTargetX, float camTargetY, float camTargetZ) {
if (!username || !username[0]) return;
Vector3 head = {worldX, worldY + 1.2f, worldZ};
Vector3 camPos = {camPosX, camPosY, camPosZ};
Vector3 camTarget = {camTargetX, camTargetY, camTargetZ};
/* Check if in front of camera */
Vector3 camForward = {camTarget.x - camPos.x, camTarget.y - camPos.y, camTarget.z - camPos.z};
float len = sqrtf(camForward.x * camForward.x + camForward.y * camForward.y + camForward.z * camForward.z);
if (len > 0) {
camForward.x /= len;
camForward.y /= len;
camForward.z /= len;
}
Vector3 toHead = {head.x - camPos.x, head.y - camPos.y, head.z - camPos.z};
float dot = camForward.x * toHead.x + camForward.y * toHead.y + camForward.z * toHead.z;
if (dot <= 0.0f) return;
Camera3D cam = {0};
cam.position = camPos;
cam.target = camTarget;
cam.up = (Vector3){0, 1, 0};
cam.fovy = 75.0f;
cam.projection = CAMERA_PERSPECTIVE;
Vector2 s = GetWorldToScreen(head, cam);
if (s.x < -200 || s.x > screenW + 200 || s.y < -200 || s.y > screenH + 200)
return;
int fontSize = 18;
int w = MeasureText(username, fontSize);
Color tc = isLocalPlayer ? (Color){180, 230, 255, 255} : RAYWHITE;
DrawText(username, (int)(s.x - w / 2), (int)(s.y - fontSize), fontSize, tc);
}

28
game/suic_ui.h Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
/**
* UI rendering API for game client.
* HUD, crosshair, room state overlays, username prompt.
*/
/* Username prompt (blocking) */
void suic_ui_username_prompt(char* outName, int maxLen);
/* HUD */
void suic_ui_draw_hud(int hp, int weapon, int mag, int reserve,
int medkits, int reloadTimeLeft);
/* Crosshair */
void suic_ui_draw_crosshair(int screenW, int screenH, float spread, bool scoped);
/* Room state overlay */
void suic_ui_draw_room_state(int screenW, int screenH, int state,
float countdown, const char* winnerName);
/* Nameplates */
void suic_ui_draw_nameplate(int screenW, int screenH, float worldX, float worldY, float worldZ,
const char* username, bool isLocalPlayer,
float camPosX, float camPosY, float camPosZ,
float camTargetX, float camTargetY, float camTargetZ);

BIN
game/suic_ui.o Normal file

Binary file not shown.