From b4d0ad9e95226d225d5361b1182866884aaa6366 Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Thu, 30 Apr 2026 19:19:27 +0200 Subject: Structural refactor --- Makefile | 2 +- all.h | 161 ++++++++-------- assets.c | 81 +++++++++ config.h | 5 + game.c | 359 +++++------------------------------- interface.c | 6 +- main.c | 143 +++++++++------ map.c | 597 +++++++++++++++++++++++++++++++++++++++--------------------- menu.c | 39 ++++ player.c | 184 +++++++------------ 10 files changed, 809 insertions(+), 768 deletions(-) create mode 100644 assets.c create mode 100644 menu.c diff --git a/Makefile b/Makefile index 57b58db..dc3c99d 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ LDFLAGS := ./vendor/$(RAYLIB_VER)/lib/libraylib.a -lm GAME := bin/stalag HEXDUMP := bin/hexdump PACKER := bin/packer -SOURCES := main.c map.c game.c player.c interface.c +SOURCES := main.c map.c game.c player.c interface.c assets.c menu.c ifeq ($(SYSTEM), linux_amd64) LDFLAGS += -lX11 diff --git a/all.h b/all.h index 59bdd32..26ccffb 100644 --- a/all.h +++ b/all.h @@ -15,134 +15,143 @@ #include #include -// --- Map Structures --- +#ifndef PLAYER_FOV +#define PLAYER_FOV 90.0f +#endif typedef struct { - Vector3 p[3]; - char texture[64]; - float shift[2]; - float rotate; - float scale[2]; + Vector3 p[3]; + char texture[64]; + float shift[2]; + float rotate; + float scale[2]; } MapPlane; typedef struct { - MapPlane *planes; - int plane_count; + MapPlane *planes; + int plane_count; } MapBrush; typedef struct { - char key[64]; - char value[256]; + char key[64]; + char value[256]; } MapProperty; typedef struct { - MapProperty *properties; - int property_count; - MapBrush *brushes; - int brush_count; + MapProperty *properties; + int property_count; + MapBrush *brushes; + int brush_count; } MapEntity; typedef struct { - MapEntity *entities; - int entity_count; + MapEntity *entities; + int entity_count; } Map; typedef struct { - const char *data; - size_t length; - size_t pos; + const char *data; + size_t length; + size_t pos; } MapParser; typedef struct { - Vector3 normal; - float dist; + Vector3 normal; + float dist; } Plane; typedef struct { - Vector3 *verts; - int count; + Vector3 *verts; + int count; } Polygon; typedef struct { - char texture[64]; - array(float) vertices; - array(float) texcoords; - array(float) normals; + char texture[64]; + array(float) vertices; + array(float) texcoords; + array(float) normals; } TextureGroup; typedef struct { - char name[64]; - Texture2D tex; + char name[64]; + Texture2D tex; } CachedTexture; -// --- Game State --- +// Asset Module +Texture2D GetTexture(const char *name); +void UnloadAssets(void); +Font LoadFontVFS(const char *path, int fontSize); + +// Map Module +typedef struct { + Model *models; + int count; +} MapState; + +bool LoadMap(const char *filename); +void UnloadMap(void); +bool CheckMapCollision(Vector3 start, Vector3 end, RayCollision *outCollision); +Map ParseMap(const char *filename); +void FreeMap(Map map); + +// Menu Module +void UpdateMenu(void); +void DrawMenu(void); +// Player Module typedef enum { - MOVE_NORMAL, - MOVE_FLY + MOVE_NORMAL, + MOVE_FLY } MovementMode; +typedef struct { + MovementMode move_mode; + Vector3 pos; + Vector3 velocity; + bool is_grounded; + float yaw; + float pitch; + float lean_amount; + float crouch_amount; + float horizontal_speed; +} PlayerState; + +void UpdatePlayer(void); + +// Game State Module typedef enum { - STATE_TITLE, - STATE_PLAYING + STATE_MENU, + STATE_PLAYING } GameStateMode; typedef struct { - GameStateMode mode; - char map_path[256]; - Camera camera; - Model *world_models; - int world_model_count; - bool cursor_captured; - bool vsync; - Font font_ui; - - MovementMode move_mode; - Vector3 pos; - Vector3 velocity; - bool is_grounded; - float yaw; - float pitch; - float lean_amount; - float crouch_amount; - float horizontal_speed; + GameStateMode mode; + char map_path[256]; + Camera camera; + bool cursor_captured; + bool vsync; + Font font_ui; + + PlayerState player; + MapState map; } GameState; extern GameState game; -// --- Prototypes --- - -// Map -char map_peek(MapParser *p); -char map_get(MapParser *p); -void map_skip_whitespace(MapParser *p); -bool map_expect(MapParser *p, char expected); -void map_parse_token(MapParser *p, char *buffer, int size); -Vector3 map_parse_vector(MapParser *p); -Map ParseMap(const char *filename); -void FreeMap(Map map); +void InitGame(void); +void SetMap(const char *path); +void UpdateGame(void); +void DrawGame(void); -// Geometry +// Geometry Helpers Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3); void PolyFree(Polygon p); Polygon PolyClip(Polygon poly, Plane plane); Polygon CreateLargeQuad(Plane plane); Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane); -// Game -Texture2D GetTexture(const char *name); -void InitGame(void); -void SetMap(const char *path); -void UpdateGame(void); -void DrawGame(void); -bool LoadMap(const char *filename); -void UnloadMap(void); - // Interface void DrawCrosshair(void); void DrawDebugInfo(void); -// Player -void UpdatePlayer(void); - #endif diff --git a/assets.c b/assets.c new file mode 100644 index 0000000..0b1f66c --- /dev/null +++ b/assets.c @@ -0,0 +1,81 @@ +#include "all.h" + +#include +#include +#include + +static array(CachedTexture) texture_cache; + +Texture2D GetTexture(const char *name) { + if (texture_cache.data == NULL) { + array_init(texture_cache); + } + + for (int i = 0; i < texture_cache.length; i++) { + if (strcmp(texture_cache.data[i].name, name) == 0) return texture_cache.data[i].tex; + } + + char lname[256]; + strncpy(lname, name, sizeof(lname)); + lname[sizeof(lname)-1] = '\0'; + for (int i = 0; lname[i]; i++) lname[i] = tolower(lname[i]); + + char path[256]; + void *data = NULL; + size_t size = 0; + const char *ext = ".png"; + + snprintf(path, sizeof(path), "textures/%s.png", lname); + data = vfs_read(path, &size); + if (!data) { + snprintf(path, sizeof(path), "textures/%s.jpg", lname); + data = vfs_read(path, &size); + ext = ".jpg"; + } + + Texture2D tex = { 0 }; + if (data) { + Image img = LoadImageFromMemory(ext, data, (int)size); + if (img.data) { + tex = LoadTextureFromImage(img); + UnloadImage(img); + } + vfs_free(data); + } + + if (tex.id == 0) { + TraceLog(LOG_WARNING, "Failed to load texture: '%s'", name); + Image img = GenImageChecked(64, 64, 8, 8, (Color){128, 128, 128, 255}, (Color){200, 200, 200, 255}); + tex = LoadTextureFromImage(img); + UnloadImage(img); + } else { + GenTextureMipmaps(&tex); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + SetTextureWrap(tex, TEXTURE_WRAP_REPEAT); + } + + CachedTexture cached; + strncpy(cached.name, name, sizeof(cached.name)); + cached.tex = tex; + array_push(texture_cache, cached); + + return tex; +} + +void UnloadAssets(void) { + for (int i = 0; i < texture_cache.length; i++) { + UnloadTexture(texture_cache.data[i].tex); + } + array_clear(texture_cache); +} + +Font LoadFontVFS(const char *path, int fontSize) { + size_t size = 0; + void *data = vfs_read(path, &size); + if (data) { + Font font = LoadFontFromMemory(".ttf", data, (int)size, fontSize, NULL, 0); + vfs_free(data); + return font; + } + return GetFontDefault(); +} diff --git a/config.h b/config.h index 7013074..9536ddd 100644 --- a/config.h +++ b/config.h @@ -5,6 +5,11 @@ #define WINDOW_HEIGHT 720 #define WINDOW_TITLE "Stalag" +#define VFS_DATA_PAK "data.pak" + +#define UI_FONT_PATH "fonts/LiberationSans-Bold.ttf" +#define UI_FONT_SIZE 20 + #define PLAYER_MOVE_SPEED 200.0f #define PLAYER_FLY_SPEED 400.0f #define PLAYER_ROTATION_SPEED 0.05f diff --git a/game.c b/game.c index 67767d3..40d1876 100644 --- a/game.c +++ b/game.c @@ -1,332 +1,69 @@ #include "all.h" -#include + #include -#include GameState game; -static array(CachedTexture) texture_cache; - -Texture2D GetTexture(const char *name) { - for (int i = 0; i < texture_cache.length; i++) { - if (strcmp(texture_cache.data[i].name, name) == 0) return texture_cache.data[i].tex; - } - - char lname[256]; - strncpy(lname, name, sizeof(lname)); - for (int i = 0; lname[i]; i++) lname[i] = tolower(lname[i]); - - char path[256]; - void *data = NULL; - size_t size = 0; - const char *ext = ".jpg"; - - snprintf(path, sizeof(path), "textures/%s.png", lname); - data = vfs_read(path, &size); - if (data) { - ext = ".png"; - } else { - snprintf(path, sizeof(path), "textures/%s.jpg", lname); - data = vfs_read(path, &size); - ext = ".jpg"; - } - - Texture2D tex = { 0 }; - if (data) { - Image img = LoadImageFromMemory(ext, data, (int)size); - if (img.data) { - tex = LoadTextureFromImage(img); - UnloadImage(img); - } - vfs_free(data); - } - - if (tex.id == 0) { - TraceLog(LOG_WARNING, "Failed to load texture: '%s' (tried path: '%s')", name, path); - Image img = GenImageChecked(64, 64, 8, 8, (Color){128, 128, 128, 255}, (Color){200, 200, 200, 255}); - tex = LoadTextureFromImage(img); - UnloadImage(img); - } - - if (tex.id != 0) { - GenTextureMipmaps(&tex); - SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); - SetTextureWrap(tex, TEXTURE_WRAP_REPEAT); - - CachedTexture cached; - strncpy(cached.name, name, sizeof(cached.name)); - cached.tex = tex; - array_push(texture_cache, cached); - } - - return tex; -} - -void UnloadMap(void) { - if (game.world_models) { - for (int i = 0; i < game.world_model_count; i++) { - UnloadModel(game.world_models[i]); - } - free(game.world_models); - game.world_models = NULL; - game.world_model_count = 0; - } - - for (int i = 0; i < texture_cache.length; i++) { - UnloadTexture(texture_cache.data[i].tex); - } - array_clear(texture_cache); -} - -bool LoadMap(const char *filename) { - TraceLog(LOG_INFO, "Loading map: %s", filename); - - Map map = ParseMap(filename); - if (map.entity_count == 0) { - TraceLog(LOG_ERROR, "Failed to load map or map is empty: %s", filename); - return false; - } - - UnloadMap(); - - array(TextureGroup) groups; - array_init(groups); - - // Default camera if no start found - game.camera.position = (Vector3){ 0, 10, 0 }; - game.camera.target = (Vector3){ 1, 10, 0 }; - game.camera.up = (Vector3){ 0, 1, 0 }; - game.camera.fovy = PLAYER_FOV; - game.camera.projection = CAMERA_PERSPECTIVE; - - int total_brushes = 0; - for (int i = 0; i < map.entity_count; i++) { - MapEntity *e = &map.entities[i]; - bool is_world = false; - const char *classname = ""; - for (int j = 0; j < e->property_count; j++) { - if (strcmp(e->properties[j].key, "classname") == 0) { - classname = e->properties[j].value; - if (strcmp(classname, "worldspawn") == 0) is_world = true; - } - if (strcmp(e->properties[j].key, "origin") == 0) { - float x, y, z; - sscanf(e->properties[j].value, "%f %f %f", &x, &y, &z); - if (strcmp(classname, "info_player_start") == 0) { - game.pos = (Vector3){ x, z, -y }; - game.camera.position = game.pos; - float angle = 0; - for (int k = 0; k < e->property_count; k++) { - if (strcmp(e->properties[k].key, "angle") == 0) { - angle = (float)atof(e->properties[k].value); - } - } - game.yaw = (angle + 90.0f) * DEG2RAD; - game.pitch = 0; - game.camera.target = Vector3Add(game.pos, (Vector3){sinf(game.yaw), 0, cosf(game.yaw)}); - TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f (yaw: %f)", game.pos.x, game.pos.y, game.pos.z, game.yaw); - } - } - } - - if (is_world) { - total_brushes += e->brush_count; - for (int j = 0; j < e->brush_count; j++) { - MapBrush *brush = &e->brushes[j]; - for (int p_idx = 0; p_idx < brush->plane_count; p_idx++) { - MapPlane *mp = &brush->planes[p_idx]; - Plane plane = PlaneFromPoints(mp->p[0], mp->p[1], mp->p[2]); - - if (Vector3Length(plane.normal) < 0.5f) continue; - - Polygon poly = CreateLargeQuad(plane); - for (int k = 0; k < brush->plane_count; k++) { - if (p_idx == k) continue; - Plane clipPlane = PlaneFromPoints(brush->planes[k].p[0], brush->planes[k].p[1], brush->planes[k].p[2]); - if (Vector3Length(clipPlane.normal) < 0.5f) continue; - - Polygon next = PolyClip(poly, clipPlane); - PolyFree(poly); - poly = next; - } - if (poly.count >= 3) { - TextureGroup *group = NULL; - for (int g = 0; g < groups.length; g++) { - if (strcmp(groups.data[g].texture, mp->texture) == 0) { - group = &groups.data[g]; - break; - } - } - if (!group) { - TextureGroup new_group = { 0 }; - strncpy(new_group.texture, mp->texture, sizeof(new_group.texture)); - array_init(new_group.vertices); - array_init(new_group.texcoords); - array_init(new_group.normals); - array_push(groups, new_group); - group = &groups.data[groups.length - 1]; - } - for (int v = 1; v < poly.count - 1; v++) { - Vector3 v0 = poly.verts[0]; - Vector3 v1 = poly.verts[v+1]; - Vector3 v2 = poly.verts[v]; - array_push(group->vertices, v0.x); array_push(group->vertices, v0.y); array_push(group->vertices, v0.z); - array_push(group->vertices, v1.x); array_push(group->vertices, v1.y); array_push(group->vertices, v1.z); - array_push(group->vertices, v2.x); array_push(group->vertices, v2.y); array_push(group->vertices, v2.z); - Vector2 uv0 = GetUV(v0, mp, plane); - Vector2 uv1 = GetUV(v1, mp, plane); - Vector2 uv2 = GetUV(v2, mp, plane); - array_push(group->texcoords, uv0.x); array_push(group->texcoords, uv0.y); - array_push(group->texcoords, uv1.x); array_push(group->texcoords, uv1.y); - array_push(group->texcoords, uv2.x); array_push(group->texcoords, uv2.y); - Vector3 out_normal = Vector3Scale(plane.normal, -1.0f); - array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z); - array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z); - array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z); - } - } - PolyFree(poly); - } - } - } - } - - array(Model) final_models; - array_init(final_models); - - for (int i = 0; i < groups.length; i++) { - TextureGroup *g = &groups.data[i]; - if (g->vertices.length == 0) continue; - Mesh mesh = { 0 }; - mesh.vertexCount = (int)g->vertices.length / 3; - mesh.triangleCount = mesh.vertexCount / 3; - mesh.vertices = (float *)malloc(g->vertices.length * sizeof(float)); - memcpy(mesh.vertices, g->vertices.data, g->vertices.length * sizeof(float)); - mesh.texcoords = (float *)malloc(g->texcoords.length * sizeof(float)); - memcpy(mesh.texcoords, g->texcoords.data, g->texcoords.length * sizeof(float)); - mesh.normals = (float *)malloc(g->normals.length * sizeof(float)); - memcpy(mesh.normals, g->normals.data, g->normals.length * sizeof(float)); - UploadMesh(&mesh, false); - Model model = LoadModelFromMesh(mesh); - model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = GetTexture(g->texture); - array_push(final_models, model); - array_free(g->vertices); - array_free(g->texcoords); - array_free(g->normals); - } - - game.world_models = final_models.data; - game.world_model_count = (int)final_models.length; - array_free(groups); - - FreeMap(map); - TraceLog(LOG_INFO, "Processed %d brushes into %d models", total_brushes, game.world_model_count); - return true; -} void InitGame(void) { - array_init(texture_cache); - game.world_models = NULL; - game.world_model_count = 0; - - // Load UI Font - size_t font_size = 0; - void *font_data = vfs_read("fonts/LiberationSans-Bold.ttf", &font_size); - if (font_data) { - game.font_ui = LoadFontFromMemory(".ttf", font_data, (int)font_size, 20, NULL, 0); - vfs_free(font_data); - } else { - game.font_ui = GetFontDefault(); - } - - game.mode = STATE_TITLE; - - game.cursor_captured = false; - EnableCursor(); - - game.move_mode = MOVE_NORMAL; - game.velocity = (Vector3){ 0, 0, 0 }; - game.is_grounded = false; + memset(&game, 0, sizeof(game)); + + game.font_ui = LoadFontVFS(UI_FONT_PATH, UI_FONT_SIZE); + game.mode = STATE_MENU; + game.cursor_captured = false; + EnableCursor(); + + game.player.move_mode = MOVE_NORMAL; + game.player.pos = (Vector3){ 0, 0, 0 }; + game.player.velocity = (Vector3){ 0, 0, 0 }; + game.player.is_grounded = false; + + // Camera setup + game.camera.up = (Vector3){ 0, 1, 0 }; + game.camera.fovy = PLAYER_FOV; + game.camera.projection = CAMERA_PERSPECTIVE; } void SetMap(const char *path) { - strncpy(game.map_path, path, sizeof(game.map_path) - 1); - game.map_path[sizeof(game.map_path) - 1] = '\0'; + strncpy(game.map_path, path, sizeof(game.map_path) - 1); + game.map_path[sizeof(game.map_path) - 1] = '\0'; } void UpdateGame(void) { - if (game.mode == STATE_TITLE) { - if (IsKeyPressed(KEY_ENTER)) { - if (LoadMap(game.map_path)) { - game.mode = STATE_PLAYING; - } - } - return; - } - - if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { - game.cursor_captured = !game.cursor_captured; - if (game.cursor_captured) DisableCursor(); - else EnableCursor(); - } - - if (IsKeyPressed(KEY_ONE)) LoadMap("maps/demo1.map"); - if (IsKeyPressed(KEY_TWO)) LoadMap("maps/demo2.map"); - if (IsKeyPressed(KEY_THREE)) LoadMap("maps/demo3.map"); - - if (IsKeyPressed(KEY_V)) { - game.vsync = !game.vsync; - if (game.vsync) { - SetWindowState(FLAG_VSYNC_HINT); - SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor())); - } else { - ClearWindowState(FLAG_VSYNC_HINT); - SetTargetFPS(0); - } - } - - UpdatePlayer(); + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { + game.cursor_captured = !game.cursor_captured; + if (game.cursor_captured) DisableCursor(); + else EnableCursor(); + } + + if (IsKeyPressed(KEY_V)) { + game.vsync = !game.vsync; + if (game.vsync) { + SetWindowState(FLAG_VSYNC_HINT); + SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor())); + } else { + ClearWindowState(FLAG_VSYNC_HINT); + SetTargetFPS(0); + } + } + + UpdatePlayer(); } void DrawGame(void) { - BeginDrawing(); - ClearBackground(BLACK); + BeginDrawing(); + ClearBackground(DARKGRAY); - if (game.mode == STATE_TITLE) { - int screenWidth = GetScreenWidth(); - int screenHeight = GetScreenHeight(); + BeginMode3D(game.camera); - const char *title = "STALAG"; - const char *sub = "Press ENTER to Start"; - - int titleSize = 60; - int subSize = 20; + rlEnableBackfaceCulling(); + for (int i = 0; i < game.map.count; i++) { + DrawModel(game.map.models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE); + } - Vector2 titlePos = { - (float)(screenWidth - MeasureTextEx(game.font_ui, title, (float)titleSize, 4).x) / 2, - (float)screenHeight / 2 - 40 - }; - Vector2 subPos = { - (float)(screenWidth - MeasureTextEx(game.font_ui, sub, (float)subSize, 2).x) / 2, - (float)screenHeight / 2 + 40 - }; + EndMode3D(); - DrawTextEx(game.font_ui, title, titlePos, (float)titleSize, 4, WHITE); - DrawTextEx(game.font_ui, sub, subPos, (float)subSize, 2, GRAY); - } else { - ClearBackground(DARKGRAY); - BeginMode3D(game.camera); - - // Enable backface culling to hide interior faces of brushes - rlEnableBackfaceCulling(); - for (int i = 0; i < game.world_model_count; i++) { - DrawModel(game.world_models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE); - } - - EndMode3D(); - - DrawCrosshair(); - DrawDebugInfo(); - } + DrawCrosshair(); + DrawDebugInfo(); - EndDrawing(); + EndDrawing(); } diff --git a/interface.c b/interface.c index d2cb4e0..58d8721 100644 --- a/interface.c +++ b/interface.c @@ -8,7 +8,7 @@ void DrawCrosshair(void) { } void DrawDebugInfo(void) { - DrawTextEx(game.font_ui, TextFormat("%i FPS", GetFPS()), (Vector2){ 10, 10 }, 20, 2, GREEN); - DrawTextEx(game.font_ui, TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), (Vector2){ 10, 35 }, 20, 2, GREEN); - DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN); + DrawTextEx(game.font_ui, TextFormat("%i FPS", GetFPS()), (Vector2){ 10, 10 }, 20, 2, GREEN); + DrawTextEx(game.font_ui, TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), (Vector2){ 10, 35 }, 20, 2, GREEN); + DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.player.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN); } diff --git a/main.c b/main.c index e497e73..9f32ecc 100644 --- a/main.c +++ b/main.c @@ -1,60 +1,97 @@ -#define _POSIX_C_SOURCE 200809L #define NONSTD_IMPLEMENTATION #define VFS_IMPLEMENTATION #include "all.h" + #include -#include int main(int argc, char *argv[]) { - char map_path[256] = "maps/demo3.map"; - bool skip_title = false; - - static struct option long_options[] = { - {"map", required_argument, 0, 'm'}, - {0, 0, 0, 0} - }; - - int opt; - int option_index = 0; - while ((opt = getopt_long_only(argc, argv, "m:", long_options, &option_index)) != -1) { - switch (opt) { - case 'm': - strncpy(map_path, optarg, sizeof(map_path) - 1); - skip_title = true; - break; - } - } - - SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI); - InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); - - int monitor = GetCurrentMonitor(); - SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, - (GetMonitorHeight(monitor) - GetScreenHeight()) / 2); - - SetTargetFPS(GetMonitorRefreshRate(monitor)); - - vfs_init("data.pak"); - InitGame(); - SetMap(map_path); - - if (skip_title) { - if (LoadMap(game.map_path)) { - game.mode = STATE_PLAYING; - game.cursor_captured = true; - DisableCursor(); - } - } - - game.vsync = true; - - while (!WindowShouldClose()) { - UpdateGame(); - DrawGame(); - } - - vfs_shutdown(); - CloseWindow(); - - return 0; + stringb map_path; + sb_init(&map_path, 256); + sb_append_cstr(&map_path, "maps/demo3.map"); + + bool skip_title = false; + + static struct option long_options[] = { + {"map", required_argument, 0, 'm'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + while ((opt = getopt_long_only(argc, argv, "m:", long_options, &option_index)) != -1) { + switch (opt) { + case 'm': + sb_free(&map_path); + sb_init(&map_path, 256); + sb_append_cstr(&map_path, optarg); + skip_title = true; + break; + } + } + + SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI); + InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); + + int monitor = GetCurrentMonitor(); + SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, (GetMonitorHeight(monitor) - GetScreenHeight()) / 2); + SetTargetFPS(GetMonitorRefreshRate(monitor)); + + vfs_init(VFS_DATA_PAK); + InitGame(); + SetMap(map_path.data); + + if (skip_title) { + if (LoadMap(game.map_path)) { + game.mode = STATE_PLAYING; + game.cursor_captured = true; + DisableCursor(); + } + } + + game.vsync = true; + + while (!WindowShouldClose()) { + // Global Map Switching (Debug/Demo) + if (IsKeyPressed(KEY_ONE)) { + if (LoadMap("maps/demo1.map")) { + game.mode = STATE_PLAYING; + game.cursor_captured = true; + DisableCursor(); + } + } + if (IsKeyPressed(KEY_TWO)) { + if (LoadMap("maps/demo2.map")) { + game.mode = STATE_PLAYING; + game.cursor_captured = true; + DisableCursor(); + } + } + if (IsKeyPressed(KEY_THREE)) { + if (LoadMap("maps/demo3.map")) { + game.mode = STATE_PLAYING; + game.cursor_captured = true; + DisableCursor(); + } + } + + // Game State Switcher + switch (game.mode) { + case STATE_MENU: + UpdateMenu(); + DrawMenu(); + break; + case STATE_PLAYING: + UpdateGame(); + DrawGame(); + break; + } + } + + UnloadMap(); + UnloadAssets(); + vfs_shutdown(); + CloseWindow(); + sb_free(&map_path); + + return 0; } diff --git a/map.c b/map.c index 12d40f9..4e2b83c 100644 --- a/map.c +++ b/map.c @@ -1,246 +1,437 @@ #include "all.h" + #include #include #include +#include +#include char map_peek(MapParser *p) { - if (p->pos >= p->length) return 0; - return p->data[p->pos]; + if (p->pos >= p->length) return 0; + return p->data[p->pos]; } char map_get(MapParser *p) { - if (p->pos >= p->length) return 0; - return p->data[p->pos++]; + if (p->pos >= p->length) return 0; + return p->data[p->pos++]; } void map_skip_whitespace(MapParser *p) { - while (true) { - char c = map_peek(p); - if (isspace(c)) { - map_get(p); - } else if (c == '/' && p->data[p->pos + 1] == '/') { - while (map_peek(p) != '\n' && map_peek(p) != 0) map_get(p); - } else { - break; - } - } + while (true) { + char c = map_peek(p); + if (isspace(c)) { + map_get(p); + } else if (c == '/' && p->data[p->pos + 1] == '/') { + while (map_peek(p) != '\n' && map_peek(p) != 0) map_get(p); + } else { + break; + } + } } bool map_expect(MapParser *p, char expected) { - map_skip_whitespace(p); - if (map_get(p) == expected) return true; - return false; + map_skip_whitespace(p); + if (map_get(p) == expected) return true; + return false; } void map_parse_token(MapParser *p, char *buffer, int size) { - map_skip_whitespace(p); - int i = 0; - bool quoted = false; - if (map_peek(p) == '"') { - quoted = true; - map_get(p); - } - - while (true) { - char c = map_peek(p); - if (c == 0) break; - if (quoted) { - if (c == '"') { - map_get(p); - break; - } - } else { - if (isspace(c) || c == '(' || c == ')' || c == '{' || c == '}') break; - } - if (i < size - 1) buffer[i++] = map_get(p); - else map_get(p); - } - buffer[i] = 0; + map_skip_whitespace(p); + int i = 0; + bool quoted = false; + if (map_peek(p) == '"') { + quoted = true; + map_get(p); + } + + while (true) { + char c = map_peek(p); + if (c == 0) break; + if (quoted) { + if (c == '"') { + map_get(p); + break; + } + } else { + if (isspace(c) || c == '(' || c == ')' || c == '{' || c == '}') break; + } + if (i < size - 1) buffer[i++] = map_get(p); + else map_get(p); + } + buffer[i] = 0; } Vector3 map_parse_vector(MapParser *p) { - map_expect(p, '('); - char buf[64]; - map_parse_token(p, buf, sizeof(buf)); - float x = (float)atof(buf); - map_parse_token(p, buf, sizeof(buf)); - float y = (float)atof(buf); - map_parse_token(p, buf, sizeof(buf)); - float z = (float)atof(buf); - map_expect(p, ')'); - return (Vector3){ x, z, -y }; + map_expect(p, '('); + char buf[64]; + map_parse_token(p, buf, sizeof(buf)); + float x = (float)atof(buf); + map_parse_token(p, buf, sizeof(buf)); + float y = (float)atof(buf); + map_parse_token(p, buf, sizeof(buf)); + float z = (float)atof(buf); + map_expect(p, ')'); + return (Vector3){ x, z, -y }; } Map ParseMap(const char *filename) { - size_t size; - char *data = (char *)vfs_read(filename, &size); - Map map = { 0 }; - if (!data) return map; - - MapParser p = { data, size, 0 }; - array(MapEntity) entities; - array_init(entities); - - while (true) { - map_skip_whitespace(&p); - if (map_peek(&p) == 0) break; - - if (map_expect(&p, '{')) { - MapEntity entity = { 0 }; - array(MapProperty) props; - array(MapBrush) brushes; - array_init(props); - array_init(brushes); - - while (true) { - map_skip_whitespace(&p); - char c = map_peek(&p); - if (c == '}') { - map_get(&p); - break; - } else if (c == '"') { - MapProperty prop; - map_parse_token(&p, prop.key, sizeof(prop.key)); - map_parse_token(&p, prop.value, sizeof(prop.value)); - array_push(props, prop); - } else if (c == '{') { - map_get(&p); - MapBrush brush = { 0 }; - array(MapPlane) planes; - array_init(planes); - while (true) { - map_skip_whitespace(&p); - if (map_peek(&p) == '}') { - map_get(&p); - break; - } - MapPlane plane; - plane.p[0] = map_parse_vector(&p); - plane.p[1] = map_parse_vector(&p); - plane.p[2] = map_parse_vector(&p); - map_parse_token(&p, plane.texture, sizeof(plane.texture)); - - char buf[64]; - map_parse_token(&p, buf, sizeof(buf)); plane.shift[0] = (float)atof(buf); - map_parse_token(&p, buf, sizeof(buf)); plane.shift[1] = (float)atof(buf); - map_parse_token(&p, buf, sizeof(buf)); plane.rotate = (float)atof(buf); - map_parse_token(&p, buf, sizeof(buf)); plane.scale[0] = (float)atof(buf); - map_parse_token(&p, buf, sizeof(buf)); plane.scale[1] = (float)atof(buf); - - array_push(planes, plane); - } - brush.planes = planes.data; - brush.plane_count = (int)planes.length; - array_push(brushes, brush); - } else { - map_get(&p); - } - } - entity.properties = props.data; - entity.property_count = (int)props.length; - entity.brushes = brushes.data; - entity.brush_count = (int)brushes.length; - array_push(entities, entity); - } - } - - map.entities = entities.data; - map.entity_count = (int)entities.length; - vfs_free(data); - return map; + size_t size; + char *data = (char *)vfs_read(filename, &size); + Map map = { 0 }; + if (!data) return map; + + MapParser p = { data, size, 0 }; + array(MapEntity) entities; + array_init(entities); + + while (true) { + map_skip_whitespace(&p); + if (map_peek(&p) == 0) break; + + if (map_expect(&p, '{')) { + MapEntity entity = { 0 }; + array(MapProperty) props; + array(MapBrush) brushes; + array_init(props); + array_init(brushes); + + while (true) { + map_skip_whitespace(&p); + char c = map_peek(&p); + if (c == '}') { + map_get(&p); + break; + } else if (c == '"') { + MapProperty prop; + map_parse_token(&p, prop.key, sizeof(prop.key)); + map_parse_token(&p, prop.value, sizeof(prop.value)); + array_push(props, prop); + } else if (c == '{') { + map_get(&p); + MapBrush brush = { 0 }; + array(MapPlane) planes; + array_init(planes); + while (true) { + map_skip_whitespace(&p); + if (map_peek(&p) == '}') { + map_get(&p); + break; + } + MapPlane plane; + plane.p[0] = map_parse_vector(&p); + plane.p[1] = map_parse_vector(&p); + plane.p[2] = map_parse_vector(&p); + map_parse_token(&p, plane.texture, sizeof(plane.texture)); + + char buf[64]; + map_parse_token(&p, buf, sizeof(buf)); plane.shift[0] = (float)atof(buf); + map_parse_token(&p, buf, sizeof(buf)); plane.shift[1] = (float)atof(buf); + map_parse_token(&p, buf, sizeof(buf)); plane.rotate = (float)atof(buf); + map_parse_token(&p, buf, sizeof(buf)); plane.scale[0] = (float)atof(buf); + map_parse_token(&p, buf, sizeof(buf)); plane.scale[1] = (float)atof(buf); + + array_push(planes, plane); + } + brush.planes = planes.data; + brush.plane_count = (int)planes.length; + array_push(brushes, brush); + } else { + map_get(&p); + } + } + entity.properties = props.data; + entity.property_count = (int)props.length; + entity.brushes = brushes.data; + entity.brush_count = (int)brushes.length; + array_push(entities, entity); + } + } + + map.entities = entities.data; + map.entity_count = (int)entities.length; + vfs_free(data); + return map; } void FreeMap(Map map) { - for (int i = 0; i < map.entity_count; i++) { - MapEntity *e = &map.entities[i]; - if (e->properties) free(e->properties); - for (int j = 0; j < e->brush_count; j++) { - if (e->brushes[j].planes) free(e->brushes[j].planes); - } - if (e->brushes) free(e->brushes); - } - if (map.entities) free(map.entities); + for (int i = 0; i < map.entity_count; i++) { + MapEntity *e = &map.entities[i]; + if (e->properties) free(e->properties); + for (int j = 0; j < e->brush_count; j++) { + if (e->brushes[j].planes) free(e->brushes[j].planes); + } + if (e->brushes) free(e->brushes); + } + if (map.entities) free(map.entities); } Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3) { - Vector3 v1 = Vector3Subtract(p2, p1); - Vector3 v2 = Vector3Subtract(p3, p1); - Vector3 normal = Vector3Normalize(Vector3CrossProduct(v1, v2)); - return (Plane){ normal, Vector3DotProduct(normal, p1) }; + Vector3 v1 = Vector3Subtract(p2, p1); + Vector3 v2 = Vector3Subtract(p3, p1); + Vector3 normal = Vector3Normalize(Vector3CrossProduct(v1, v2)); + return (Plane){ normal, Vector3DotProduct(normal, p1) }; } void PolyFree(Polygon p) { - if (p.verts) free(p.verts); + if (p.verts) free(p.verts); } Polygon PolyClip(Polygon poly, Plane plane) { - if (poly.count == 0) return poly; - - array(Vector3) out_verts; - array_init(out_verts); - - for (int i = 0; i < poly.count; i++) { - Vector3 p1 = poly.verts[i]; - Vector3 p2 = poly.verts[(i + 1) % poly.count]; - - float d1 = Vector3DotProduct(plane.normal, p1) - plane.dist; - float d2 = Vector3DotProduct(plane.normal, p2) - plane.dist; - - // Using a smaller epsilon and more robust logic - const float eps = 0.001f; - - if (d1 >= -eps) { - if (d2 >= -eps) { - array_push(out_verts, p2); - } else { - float t = d1 / (d1 - d2); - Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t)); - array_push(out_verts, intersect); - } - } else { - if (d2 >= -eps) { - float t = d1 / (d1 - d2); - Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t)); - array_push(out_verts, intersect); - array_push(out_verts, p2); - } - } - } - - Polygon res; - res.verts = out_verts.data; - res.count = (int)out_verts.length; - return res; + if (poly.count == 0) return poly; + + array(Vector3) out_verts; + array_init(out_verts); + + for (int i = 0; i < poly.count; i++) { + Vector3 p1 = poly.verts[i]; + Vector3 p2 = poly.verts[(i + 1) % poly.count]; + + float d1 = Vector3DotProduct(plane.normal, p1) - plane.dist; + float d2 = Vector3DotProduct(plane.normal, p2) - plane.dist; + + const float eps = 0.001f; + + if (d1 >= -eps) { + if (d2 >= -eps) { + array_push(out_verts, p2); + } else { + float t = d1 / (d1 - d2); + Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t)); + array_push(out_verts, intersect); + } + } else { + if (d2 >= -eps) { + float t = d1 / (d1 - d2); + Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t)); + array_push(out_verts, intersect); + array_push(out_verts, p2); + } + } + } + + Polygon res; + res.verts = out_verts.data; + res.count = (int)out_verts.length; + return res; } Polygon CreateLargeQuad(Plane plane) { - Vector3 n = plane.normal; - Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 }; - Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n)); - up = Vector3CrossProduct(n, right); - - Vector3 center = Vector3Scale(n, plane.dist); - float size = 10000.0f; - - Polygon poly; - poly.verts = ALLOC(Vector3, 4); - poly.count = 4; - poly.verts[0] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, size))); - poly.verts[1] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, size))); - poly.verts[2] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, -size))); - poly.verts[3] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, -size))); - - return poly; + Vector3 n = plane.normal; + Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 }; + Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n)); + up = Vector3CrossProduct(n, right); + + Vector3 center = Vector3Scale(n, plane.dist); + float size = 10000.0f; + + Polygon poly; + poly.verts = ALLOC(Vector3, 4); + poly.count = 4; + poly.verts[0] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, size))); + poly.verts[1] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, size))); + poly.verts[2] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, -size))); + poly.verts[3] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, -size))); + + return poly; } Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane) { - Vector3 n = plane.normal; - Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 }; - Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n)); - up = Vector3CrossProduct(n, right); - - float u = Vector3DotProduct(p, right) * (1.0f / (mp->scale[0] ? mp->scale[0] : 1.0f)) + mp->shift[0]; - float v = Vector3DotProduct(p, up) * (1.0f / (mp->scale[1] ? mp->scale[1] : 1.0f)) + mp->shift[1]; - - return (Vector2){ u / 64.0f, v / 64.0f }; + Vector3 n = plane.normal; + Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 }; + Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n)); + up = Vector3CrossProduct(n, right); + + float u = Vector3DotProduct(p, right) * (1.0f / (mp->scale[0] ? mp->scale[0] : 1.0f)) + mp->shift[0]; + float v = Vector3DotProduct(p, up) * (1.0f / (mp->scale[1] ? mp->scale[1] : 1.0f)) + mp->shift[1]; + + return (Vector2){ u / 64.0f, v / 64.0f }; +} + +void UnloadMap(void) { + if (game.map.models) { + for (int i = 0; i < game.map.count; i++) { + UnloadModel(game.map.models[i]); + } + free(game.map.models); + game.map.models = NULL; + game.map.count = 0; + } +} + +bool LoadMap(const char *filename) { + TraceLog(LOG_INFO, "Loading map: %s", filename); + + Map map = ParseMap(filename); + if (map.entity_count == 0) { + TraceLog(LOG_ERROR, "Failed to load map or map is empty: %s", filename); + return false; + } + + UnloadMap(); + + array(TextureGroup) groups; + array_init(groups); + + // Default player state if no start found + game.player.pos = (Vector3){ 0, 10, 0 }; + game.player.yaw = 0; + game.player.pitch = 0; + + int total_brushes = 0; + for (int i = 0; i < map.entity_count; i++) { + MapEntity *e = &map.entities[i]; + bool is_world = false; + const char *classname = ""; + + for (int j = 0; j < e->property_count; j++) { + if (strcmp(e->properties[j].key, "classname") == 0) { + classname = e->properties[j].value; + if (strcmp(classname, "worldspawn") == 0) is_world = true; + } + } + + // Handle entities (like player start) + for (int j = 0; j < e->property_count; j++) { + if (strcmp(e->properties[j].key, "origin") == 0) { + float x, y, z; + sscanf(e->properties[j].value, "%f %f %f", &x, &y, &z); + if (strcmp(classname, "info_player_start") == 0) { + game.player.pos = (Vector3){ x, z, -y }; + float angle = 0; + for (int k = 0; k < e->property_count; k++) { + if (strcmp(e->properties[k].key, "angle") == 0) { + angle = (float)atof(e->properties[k].value); + } + } + game.player.yaw = (angle + 90.0f) * DEG2RAD; + game.player.pitch = 0; + TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f", game.player.pos.x, game.player.pos.y, game.player.pos.z); + } + } + } + + if (is_world) { + total_brushes += e->brush_count; + for (int j = 0; j < e->brush_count; j++) { + MapBrush *brush = &e->brushes[j]; + for (int p_idx = 0; p_idx < brush->plane_count; p_idx++) { + MapPlane *mp = &brush->planes[p_idx]; + Plane plane = PlaneFromPoints(mp->p[0], mp->p[1], mp->p[2]); + + if (Vector3Length(plane.normal) < 0.5f) continue; + + Polygon poly = CreateLargeQuad(plane); + for (int k = 0; k < brush->plane_count; k++) { + if (p_idx == k) continue; + Plane clipPlane = PlaneFromPoints(brush->planes[k].p[0], brush->planes[k].p[1], brush->planes[k].p[2]); + if (Vector3Length(clipPlane.normal) < 0.5f) continue; + + Polygon next = PolyClip(poly, clipPlane); + PolyFree(poly); + poly = next; + } + + if (poly.count >= 3) { + TextureGroup *group = NULL; + for (int g = 0; g < groups.length; g++) { + if (strcmp(groups.data[g].texture, mp->texture) == 0) { + group = &groups.data[g]; + break; + } + } + if (!group) { + TextureGroup new_group = { 0 }; + strncpy(new_group.texture, mp->texture, sizeof(new_group.texture)); + array_init(new_group.vertices); + array_init(new_group.texcoords); + array_init(new_group.normals); + array_push(groups, new_group); + group = &groups.data[groups.length - 1]; + } + for (int v = 1; v < poly.count - 1; v++) { + Vector3 v0 = poly.verts[0]; + Vector3 v1 = poly.verts[v+1]; + Vector3 v2 = poly.verts[v]; + array_push(group->vertices, v0.x); array_push(group->vertices, v0.y); array_push(group->vertices, v0.z); + array_push(group->vertices, v1.x); array_push(group->vertices, v1.y); array_push(group->vertices, v1.z); + array_push(group->vertices, v2.x); array_push(group->vertices, v2.y); array_push(group->vertices, v2.z); + Vector2 uv0 = GetUV(v0, mp, plane); + Vector2 uv1 = GetUV(v1, mp, plane); + Vector2 uv2 = GetUV(v2, mp, plane); + array_push(group->texcoords, uv0.x); array_push(group->texcoords, uv0.y); + array_push(group->texcoords, uv1.x); array_push(group->texcoords, uv1.y); + array_push(group->texcoords, uv2.x); array_push(group->texcoords, uv2.y); + Vector3 out_normal = Vector3Scale(plane.normal, -1.0f); + array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z); + array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z); + array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z); + } + } + PolyFree(poly); + } + } + } + } + + array(Model) final_models; + array_init(final_models); + + for (int i = 0; i < groups.length; i++) { + TextureGroup *g = &groups.data[i]; + if (g->vertices.length == 0) continue; + Mesh mesh = { 0 }; + mesh.vertexCount = (int)g->vertices.length / 3; + mesh.triangleCount = mesh.vertexCount / 3; + mesh.vertices = (float *)malloc(g->vertices.length * sizeof(float)); + memcpy(mesh.vertices, g->vertices.data, g->vertices.length * sizeof(float)); + mesh.texcoords = (float *)malloc(g->texcoords.length * sizeof(float)); + memcpy(mesh.texcoords, g->texcoords.data, g->texcoords.length * sizeof(float)); + mesh.normals = (float *)malloc(g->normals.length * sizeof(float)); + memcpy(mesh.normals, g->normals.data, g->normals.length * sizeof(float)); + UploadMesh(&mesh, false); + Model model = LoadModelFromMesh(mesh); + model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = GetTexture(g->texture); + array_push(final_models, model); + array_free(g->vertices); + array_free(g->texcoords); + array_free(g->normals); + } + + game.map.models = final_models.data; + game.map.count = (int)final_models.length; + array_free(groups); + + FreeMap(map); + TraceLog(LOG_INFO, "Processed %d brushes into %d models", total_brushes, game.map.count); + return true; +} + +bool CheckMapCollision(Vector3 start, Vector3 end, RayCollision *outCollision) { + Vector3 diff = Vector3Subtract(end, start); + float maxDist = Vector3Length(diff); + if (maxDist < 0.001f) return false; + + Ray ray = { 0 }; + ray.position = start; + ray.direction = Vector3Scale(diff, 1.0f / maxDist); + + RayCollision closest = { 0 }; + closest.distance = FLT_MAX; + closest.hit = false; + + for (int i = 0; i < game.map.count; i++) { + Model model = game.map.models[i]; + for (int m = 0; m < model.meshCount; m++) { + RayCollision col = GetRayCollisionMesh(ray, model.meshes[m], model.transform); + if (col.hit && col.distance < closest.distance && col.distance <= maxDist) { + closest = col; + } + } + } + + if (closest.hit) { + if (outCollision) *outCollision = closest; + return true; + } + return false; } diff --git a/menu.c b/menu.c new file mode 100644 index 0000000..c1db1f7 --- /dev/null +++ b/menu.c @@ -0,0 +1,39 @@ +#include "all.h" + +void UpdateMenu(void) { + if (IsKeyPressed(KEY_ENTER)) { + if (LoadMap(game.map_path)) { + game.mode = STATE_PLAYING; + game.cursor_captured = true; + DisableCursor(); + } + } +} + +void DrawMenu(void) { + BeginDrawing(); + ClearBackground(BLACK); + + int screenWidth = GetScreenWidth(); + int screenHeight = GetScreenHeight(); + + const char *title = "STALAG"; + const char *sub = "Press ENTER to Start"; + + int titleSize = 60; + int subSize = 20; + + Vector2 titlePos = { + (float)(screenWidth - MeasureTextEx(game.font_ui, title, (float)titleSize, 4).x) / 2, + (float)screenHeight / 2 - 40 + }; + Vector2 subPos = { + (float)(screenWidth - MeasureTextEx(game.font_ui, sub, (float)subSize, 2).x) / 2, + (float)screenHeight / 2 + 40 + }; + + DrawTextEx(game.font_ui, title, titlePos, (float)titleSize, 4, WHITE); + DrawTextEx(game.font_ui, sub, subPos, (float)subSize, 2, GRAY); + + EndDrawing(); +} diff --git a/player.c b/player.c index 01c56ca..52788f1 100644 --- a/player.c +++ b/player.c @@ -1,55 +1,24 @@ #include "all.h" #include "raymath.h" + #include static void PlayerRotate(void) { if (!game.cursor_captured) return; Vector2 mouseDelta = GetMouseDelta(); - game.yaw += -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY; - game.pitch += mouseDelta.y * PLAYER_MOUSE_SENSITIVITY; + game.player.yaw += -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY; + game.player.pitch += mouseDelta.y * PLAYER_MOUSE_SENSITIVITY; - // Clamp pitch to avoid gimbal lock/flipping (approx 89 degrees) - if (game.pitch > 89.0f * DEG2RAD) game.pitch = 89.0f * DEG2RAD; - if (game.pitch < -89.0f * DEG2RAD) game.pitch = -89.0f * DEG2RAD; -} - -static bool CheckWorldCollision(Vector3 start, Vector3 end, RayCollision *outCollision) { - Vector3 diff = Vector3Subtract(end, start); - float maxDist = Vector3Length(diff); - if (maxDist < 0.001f) return false; - - Ray ray = { 0 }; - ray.position = start; - ray.direction = Vector3Scale(diff, 1.0f / maxDist); - - RayCollision closest = { 0 }; - closest.distance = FLT_MAX; - closest.hit = false; - - for (int i = 0; i < game.world_model_count; i++) { - Model model = game.world_models[i]; - for (int m = 0; m < model.meshCount; m++) { - RayCollision col = GetRayCollisionMesh(ray, model.meshes[m], model.transform); - if (col.hit && col.distance < closest.distance && col.distance <= maxDist) { - closest = col; - } - } - } - - if (closest.hit) { - if (outCollision) *outCollision = closest; - return true; - } - return false; + if (game.player.pitch > 89.0f * DEG2RAD) game.player.pitch = 89.0f * DEG2RAD; + if (game.player.pitch < -89.0f * DEG2RAD) game.player.pitch = -89.0f * DEG2RAD; } static void MoveNormal(float dt) { - float eyeHeight = PLAYER_EYE_HEIGHT - (game.crouch_amount * PLAYER_CROUCH_OFFSET); + float eyeHeight = PLAYER_EYE_HEIGHT - (game.player.crouch_amount * PLAYER_CROUCH_OFFSET); - // Movement vectors based on yaw - Vector3 forward = { sinf(game.yaw), 0, cosf(game.yaw) }; - Vector3 right = { sinf(game.yaw - PI/2.0f), 0, cosf(game.yaw - PI/2.0f) }; + Vector3 forward = { sinf(game.player.yaw), 0, cosf(game.player.yaw) }; + Vector3 right = { sinf(game.player.yaw - PI/2.0f), 0, cosf(game.player.yaw - PI/2.0f) }; Vector3 moveDir = { 0 }; if (IsKeyDown(KEY_W)) moveDir = Vector3Add(moveDir, forward); @@ -59,24 +28,19 @@ static void MoveNormal(float dt) { float speed = PLAYER_MOVE_SPEED; if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) speed *= PLAYER_SPRINT_MULTIPLIER; - - // Apply crouch speed penalty - speed *= Lerp(1.0f, PLAYER_CROUCH_MOVE_MULTIPLIER, game.crouch_amount); + speed *= Lerp(1.0f, PLAYER_CROUCH_MOVE_MULTIPLIER, game.player.crouch_amount); if (Vector3Length(moveDir) > 0) { moveDir = Vector3Scale(Vector3Normalize(moveDir), speed * dt); } - // Apply gravity - game.velocity.y -= PLAYER_GRAVITY * dt; + game.player.velocity.y -= PLAYER_GRAVITY * dt; - // Jump - if (game.is_grounded && IsKeyPressed(KEY_SPACE)) { - game.velocity.y = PLAYER_JUMP_FORCE; - game.is_grounded = false; + if (game.player.is_grounded && IsKeyPressed(KEY_SPACE)) { + game.player.velocity.y = PLAYER_JUMP_FORCE; + game.player.is_grounded = false; } - // Horizontal movement with sliding collision Vector3 remainingMove = moveDir; for (int iter = 0; iter < 4 && Vector3Length(remainingMove) > 0.001f; iter++) { RayCollision closestHit = { 0 }; @@ -84,7 +48,7 @@ static void MoveNormal(float dt) { bool hitFound = false; float heights[] = { -eyeHeight + 10.0f, -eyeHeight / 2.0f, 0.0f }; - Vector3 currentPos = game.pos; + Vector3 currentPos = game.player.pos; for (int i = 0; i < 3; i++) { Vector3 start = Vector3Add(currentPos, (Vector3){0, heights[i], 0}); @@ -93,8 +57,7 @@ static void MoveNormal(float dt) { Vector3 end = Vector3Add(start, Vector3Scale(dir, dist)); RayCollision col; - if (CheckWorldCollision(start, end, &col)) { - // Adjust distance to be relative to the player boundary + if (CheckMapCollision(start, end, &col)) { float adjustedDist = col.distance - PLAYER_RADIUS; if (adjustedDist < closestHit.distance) { closestHit = col; @@ -105,13 +68,11 @@ static void MoveNormal(float dt) { } if (hitFound) { - // Move as far as possible float moveDist = fmaxf(0, closestHit.distance - 0.1f); Vector3 moveStep = Vector3Scale(Vector3Normalize(remainingMove), moveDist); - game.pos.x += moveStep.x; - game.pos.z += moveStep.z; + game.player.pos.x += moveStep.x; + game.player.pos.z += moveStep.z; - // Project remaining movement onto the plane of the wall Vector3 slideNormal = { closestHit.normal.x, 0, closestHit.normal.z }; if (Vector3Length(slideNormal) > 0.001f) { slideNormal = Vector3Normalize(slideNormal); @@ -122,68 +83,61 @@ static void MoveNormal(float dt) { remainingMove = (Vector3){0, 0, 0}; } } else { - // No collision found, move the rest of the way - game.pos.x += remainingMove.x; - game.pos.z += remainingMove.z; + game.player.pos.x += remainingMove.x; + game.player.pos.z += remainingMove.z; break; } } - // Vertical movement with collision - float verticalMove = game.velocity.y * dt; - Vector3 vStart = game.pos; + float verticalMove = game.player.velocity.y * dt; + Vector3 vStart = game.player.pos; - if (verticalMove < 0) { // Falling/Down + if (verticalMove < 0) { Vector3 start = vStart; - // Check slightly below feet Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove - eyeHeight, 0}); RayCollision vCol; - if (CheckWorldCollision(start, end, &vCol) && vCol.normal.y > 0.5f) { - game.pos.y = vCol.point.y + eyeHeight; - game.velocity.y = 0; - game.is_grounded = true; + if (CheckMapCollision(start, end, &vCol) && vCol.normal.y > 0.5f) { + game.player.pos.y = vCol.point.y + eyeHeight; + game.player.velocity.y = 0; + game.player.is_grounded = true; } else { - game.pos.y += verticalMove; - game.is_grounded = false; + game.player.pos.y += verticalMove; + game.player.is_grounded = false; } - } else if (verticalMove > 0) { // Jumping/Up + } else if (verticalMove > 0) { Vector3 start = vStart; - // Check above head float headHeight = PLAYER_HEIGHT - PLAYER_EYE_HEIGHT; Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove + headHeight, 0}); RayCollision vCol; - if (CheckWorldCollision(start, end, &vCol)) { - game.pos.y = vCol.point.y - headHeight - 1.0f; - game.velocity.y = 0; + if (CheckMapCollision(start, end, &vCol)) { + game.player.pos.y = vCol.point.y - headHeight - 1.0f; + game.player.velocity.y = 0; } else { - game.pos.y += verticalMove; + game.player.pos.y += verticalMove; } - game.is_grounded = false; + game.player.is_grounded = false; } else { - // Not moving vertically, but check if we are still on ground Vector3 start = vStart; Vector3 end = Vector3Add(vStart, (Vector3){0, -eyeHeight - 2.0f, 0}); RayCollision vCol; - if (CheckWorldCollision(start, end, &vCol) && vCol.normal.y > 0.5f) { - game.is_grounded = true; - // Snap to floor if very close + if (CheckMapCollision(start, end, &vCol) && vCol.normal.y > 0.5f) { + game.player.is_grounded = true; if (vCol.distance < eyeHeight + 1.0f) { - game.pos.y = vCol.point.y + eyeHeight; + game.player.pos.y = vCol.point.y + eyeHeight; } } else { - game.is_grounded = false; + game.player.is_grounded = false; } } } static void MoveFly(float dt) { - // Full 3D movement based on yaw/pitch Vector3 forward = { - cosf(game.pitch) * sinf(game.yaw), - -sinf(game.pitch), - cosf(game.pitch) * cosf(game.yaw) + cosf(game.player.pitch) * sinf(game.player.yaw), + -sinf(game.player.pitch), + cosf(game.player.pitch) * cosf(game.player.yaw) }; - Vector3 right = { sinf(game.yaw - PI/2.0f), 0, cosf(game.yaw - PI/2.0f) }; + Vector3 right = { sinf(game.player.yaw - PI/2.0f), 0, cosf(game.player.yaw - PI/2.0f) }; Vector3 move = { 0 }; if (IsKeyDown(KEY_W)) move = Vector3Add(move, forward); @@ -191,7 +145,6 @@ static void MoveFly(float dt) { if (IsKeyDown(KEY_D)) move = Vector3Add(move, right); if (IsKeyDown(KEY_A)) move = Vector3Subtract(move, right); - // Fly up/down if (IsKeyDown(KEY_SPACE)) move.y += 1.0f; if (IsKeyDown(KEY_LEFT_CONTROL)) move.y -= 1.0f; @@ -200,86 +153,75 @@ static void MoveFly(float dt) { if (Vector3Length(move) > 0) { move = Vector3Scale(Vector3Normalize(move), speed * dt); - game.pos = Vector3Add(game.pos, move); + game.player.pos = Vector3Add(game.player.pos, move); } - game.velocity = (Vector3){0, 0, 0}; - game.is_grounded = false; + game.player.velocity = (Vector3){0, 0, 0}; + game.player.is_grounded = false; } void UpdatePlayer(void) { float dt = GetFrameTime(); - Vector3 oldPos = game.pos; + Vector3 oldPos = game.player.pos; if (IsKeyPressed(KEY_F)) { - game.move_mode = (game.move_mode == MOVE_NORMAL) ? MOVE_FLY : MOVE_NORMAL; - TraceLog(LOG_INFO, "Movement mode: %s", (game.move_mode == MOVE_NORMAL) ? "NORMAL" : "FLY"); + game.player.move_mode = (game.player.move_mode == MOVE_NORMAL) ? MOVE_FLY : MOVE_NORMAL; } PlayerRotate(); - // Crouching logic bool canStand = true; - if (game.crouch_amount > 0.1f) { - Vector3 headPos = game.pos; + if (game.player.crouch_amount > 0.1f) { + Vector3 headPos = game.player.pos; Vector3 headEnd = Vector3Add(headPos, (Vector3){0, PLAYER_HEIGHT - PLAYER_EYE_HEIGHT + PLAYER_CROUCH_OFFSET, 0}); RayCollision col; - if (CheckWorldCollision(headPos, headEnd, &col)) { + if (CheckMapCollision(headPos, headEnd, &col)) { canStand = false; } } float targetCrouch = 0.0f; if (IsKeyDown(KEY_LEFT_CONTROL) || !canStand) targetCrouch = 1.0f; - game.crouch_amount = Lerp(game.crouch_amount, targetCrouch, PLAYER_CROUCH_SPEED * dt); + game.player.crouch_amount = Lerp(game.player.crouch_amount, targetCrouch, PLAYER_CROUCH_SPEED * dt); - // Leaning logic float targetLean = 0.0f; if (IsKeyDown(KEY_Q)) targetLean -= 1.0f; if (IsKeyDown(KEY_E)) targetLean += 1.0f; + game.player.lean_amount = Lerp(game.player.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt); - game.lean_amount = Lerp(game.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt); - - if (game.move_mode == MOVE_FLY) { + if (game.player.move_mode == MOVE_FLY) { MoveFly(dt); } else { MoveNormal(dt); } - // Apply lean as a pivot from the neck/waist - float leanAngle = game.lean_amount * PLAYER_LEAN_ANGLE * DEG2RAD; - Vector3 bodyForward = { sinf(game.yaw), 0, cosf(game.yaw) }; + float leanAngle = game.player.lean_amount * PLAYER_LEAN_ANGLE * DEG2RAD; + Vector3 bodyForward = { sinf(game.player.yaw), 0, cosf(game.player.yaw) }; - float currentEyeHeight = PLAYER_EYE_HEIGHT - (game.crouch_amount * PLAYER_CROUCH_OFFSET); + float currentEyeHeight = PLAYER_EYE_HEIGHT - (game.player.crouch_amount * PLAYER_CROUCH_OFFSET); float pivotDist = fminf(PLAYER_LEAN_PIVOT_DISTANCE, currentEyeHeight * 0.8f); Vector3 neckToEye = { 0, pivotDist, 0 }; Vector3 rotatedOffset = Vector3RotateByAxisAngle(neckToEye, bodyForward, leanAngle); - Vector3 pivot = Vector3Subtract(game.pos, (Vector3){ 0, pivotDist, 0 }); + Vector3 pivot = Vector3Subtract(game.player.pos, (Vector3){ 0, pivotDist, 0 }); - // Update camera based on physical pos + visual lean pivot game.camera.position = Vector3Add(pivot, rotatedOffset); - // Apply roll to up vector Vector3 forward = { - cosf(game.pitch) * sinf(game.yaw), - -sinf(game.pitch), - cosf(game.pitch) * cosf(game.yaw) + cosf(game.player.pitch) * sinf(game.player.yaw), + -sinf(game.player.pitch), + cosf(game.player.pitch) * cosf(game.player.yaw) }; - // Use a stable world-up vector and apply lean roll Vector3 up = { 0, 1, 0 }; up = Vector3RotateByAxisAngle(up, forward, leanAngle); game.camera.up = up; - - // Use a longer target distance for better precision in the projection matrix game.camera.target = Vector3Add(game.camera.position, Vector3Scale(forward, 20.0f)); - // Calculate horizontal speed for UI if (dt > 0) { - Vector2 velH = { game.pos.x - oldPos.x, game.pos.z - oldPos.z }; - game.horizontal_speed = Vector2Length(velH) / dt; + Vector2 velH = { game.player.pos.x - oldPos.x, game.player.pos.z - oldPos.z }; + game.player.horizontal_speed = Vector2Length(velH) / dt; } else { - game.horizontal_speed = 0; + game.player.horizontal_speed = 0; } } -- cgit v1.2.3