Structural refactor

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-04-30 19:19:27 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-04-30 19:44:07 +0200
Commit b4d0ad9e95226d225d5361b1182866884aaa6366 (patch)
-rw-r--r-- Makefile 2
-rw-r--r-- all.h 159
-rw-r--r-- assets.c 81
-rw-r--r-- config.h 5
-rw-r--r-- game.c 349
-rw-r--r-- interface.c 6
-rw-r--r-- main.c 123
-rw-r--r-- map.c 573
-rw-r--r-- menu.c 39
-rw-r--r-- player.c 184
10 files changed, 781 insertions, 740 deletions
diff --git a/Makefile b/Makefile
...
16
GAME         := bin/stalag
16
GAME         := bin/stalag
17
HEXDUMP      := bin/hexdump
17
HEXDUMP      := bin/hexdump
18
PACKER       := bin/packer
18
PACKER       := bin/packer
19
SOURCES      := main.c map.c game.c player.c interface.c
19
SOURCES      := main.c map.c game.c player.c interface.c assets.c menu.c
20
  
20
  
21
ifeq ($(SYSTEM), linux_amd64)
21
ifeq ($(SYSTEM), linux_amd64)
22
	LDFLAGS += -lX11
22
	LDFLAGS += -lX11
...
diff --git a/all.h b/all.h
...
15
#include <stdbool.h>
15
#include <stdbool.h>
16
#include <stddef.h>
16
#include <stddef.h>
17
  
17
  
18
// --- Map Structures ---
18
#ifndef PLAYER_FOV
  
19
#define PLAYER_FOV 90.0f
  
20
#endif
19
  
21
  
20
typedef struct {
22
typedef struct {
21
    Vector3 p[3];
23
	Vector3 p[3];
22
    char texture[64];
24
	char texture[64];
23
    float shift[2];
25
	float shift[2];
24
    float rotate;
26
	float rotate;
25
    float scale[2];
27
	float scale[2];
26
} MapPlane;
28
} MapPlane;
27
  
29
  
28
typedef struct {
30
typedef struct {
29
    MapPlane *planes;
31
	MapPlane *planes;
30
    int plane_count;
32
	int plane_count;
31
} MapBrush;
33
} MapBrush;
32
  
34
  
33
typedef struct {
35
typedef struct {
34
    char key[64];
36
	char key[64];
35
    char value[256];
37
	char value[256];
36
} MapProperty;
38
} MapProperty;
37
  
39
  
38
typedef struct {
40
typedef struct {
39
    MapProperty *properties;
41
	MapProperty *properties;
40
    int property_count;
42
	int property_count;
41
    MapBrush *brushes;
43
	MapBrush *brushes;
42
    int brush_count;
44
	int brush_count;
43
} MapEntity;
45
} MapEntity;
44
  
46
  
45
typedef struct {
47
typedef struct {
46
    MapEntity *entities;
48
	MapEntity *entities;
47
    int entity_count;
49
	int entity_count;
48
} Map;
50
} Map;
49
  
51
  
50
typedef struct {
52
typedef struct {
51
    const char *data;
53
	const char *data;
52
    size_t length;
54
	size_t length;
53
    size_t pos;
55
	size_t pos;
54
} MapParser;
56
} MapParser;
55
  
57
  
56
typedef struct {
58
typedef struct {
57
    Vector3 normal;
59
	Vector3 normal;
58
    float dist;
60
	float dist;
59
} Plane;
61
} Plane;
60
  
62
  
61
typedef struct {
63
typedef struct {
62
    Vector3 *verts;
64
	Vector3 *verts;
63
    int count;
65
	int count;
64
} Polygon;
66
} Polygon;
65
  
67
  
66
typedef struct {
68
typedef struct {
67
    char texture[64];
69
	char texture[64];
68
    array(float) vertices;
70
	array(float) vertices;
69
    array(float) texcoords;
71
	array(float) texcoords;
70
    array(float) normals;
72
	array(float) normals;
71
} TextureGroup;
73
} TextureGroup;
72
  
74
  
73
typedef struct {
75
typedef struct {
74
    char name[64];
76
	char name[64];
75
    Texture2D tex;
77
	Texture2D tex;
76
} CachedTexture;
78
} CachedTexture;
77
  
79
  
78
// --- Game State ---
80
// Asset Module
  
81
Texture2D GetTexture(const char *name);
  
82
void UnloadAssets(void);
  
83
Font LoadFontVFS(const char *path, int fontSize);
  
84
  
  
85
// Map Module
  
86
typedef struct {
  
87
	Model *models;
  
88
	int count;
  
89
} MapState;
  
90
  
  
91
bool LoadMap(const char *filename);
  
92
void UnloadMap(void);
  
93
bool CheckMapCollision(Vector3 start, Vector3 end, RayCollision *outCollision);
  
94
Map ParseMap(const char *filename);
  
95
void FreeMap(Map map);
  
96
  
  
97
// Menu Module
  
98
void UpdateMenu(void);
  
99
void DrawMenu(void);
79
  
100
  
  
101
// Player Module
80
typedef enum {
102
typedef enum {
81
    MOVE_NORMAL,
103
	MOVE_NORMAL,
82
    MOVE_FLY
104
	MOVE_FLY
83
} MovementMode;
105
} MovementMode;
84
  
106
  
  
107
typedef struct {
  
108
	MovementMode move_mode;
  
109
	Vector3 pos;
  
110
	Vector3 velocity;
  
111
	bool is_grounded;
  
112
	float yaw;
  
113
	float pitch;
  
114
	float lean_amount;
  
115
	float crouch_amount;
  
116
	float horizontal_speed;
  
117
} PlayerState;
  
118
  
  
119
void UpdatePlayer(void);
  
120
  
  
121
// Game State Module
85
typedef enum {
122
typedef enum {
86
    STATE_TITLE,
123
	STATE_MENU,
87
    STATE_PLAYING
124
	STATE_PLAYING
88
} GameStateMode;
125
} GameStateMode;
89
  
126
  
90
typedef struct {
127
typedef struct {
91
    GameStateMode mode;
128
	GameStateMode mode;
92
    char map_path[256];
129
	char map_path[256];
93
    Camera camera;
130
	Camera camera;
94
    Model *world_models;
131
	bool cursor_captured;
95
    int world_model_count;
132
	bool vsync;
96
    bool cursor_captured;
133
	Font font_ui;
97
    bool vsync;
  
98
    Font font_ui;
  
99
  
134
  
100
    MovementMode move_mode;
135
	PlayerState player;
101
    Vector3 pos;
136
	MapState map;
102
    Vector3 velocity;
  
103
    bool is_grounded;
  
104
    float yaw;
  
105
    float pitch;
  
106
    float lean_amount;
  
107
    float crouch_amount;
  
108
    float horizontal_speed;
  
109
} GameState;
137
} GameState;
110
  
138
  
111
extern GameState game;
139
extern GameState game;
112
  
140
  
113
// --- Prototypes ---
141
void InitGame(void);
  
142
void SetMap(const char *path);
  
143
void UpdateGame(void);
  
144
void DrawGame(void);
114
  
145
  
115
// Map
146
// Geometry Helpers
116
char map_peek(MapParser *p);
  
117
char map_get(MapParser *p);
  
118
void map_skip_whitespace(MapParser *p);
  
119
bool map_expect(MapParser *p, char expected);
  
120
void map_parse_token(MapParser *p, char *buffer, int size);
  
121
Vector3 map_parse_vector(MapParser *p);
  
122
Map ParseMap(const char *filename);
  
123
void FreeMap(Map map);
  
124
  
  
125
// Geometry
  
126
Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3);
147
Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3);
127
void PolyFree(Polygon p);
148
void PolyFree(Polygon p);
128
Polygon PolyClip(Polygon poly, Plane plane);
149
Polygon PolyClip(Polygon poly, Plane plane);
129
Polygon CreateLargeQuad(Plane plane);
150
Polygon CreateLargeQuad(Plane plane);
130
Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane);
151
Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane);
131
  
152
  
132
// Game
  
133
Texture2D GetTexture(const char *name);
  
134
void InitGame(void);
  
135
void SetMap(const char *path);
  
136
void UpdateGame(void);
  
137
void DrawGame(void);
  
138
bool LoadMap(const char *filename);
  
139
void UnloadMap(void);
  
140
  
  
141
// Interface
153
// Interface
142
void DrawCrosshair(void);
154
void DrawCrosshair(void);
143
void DrawDebugInfo(void);
155
void DrawDebugInfo(void);
144
  
  
145
// Player
  
146
void UpdatePlayer(void);
  
147
  
156
  
148
#endif
157
#endif
diff --git a/assets.c b/assets.c
  
1
#include "all.h"
  
2
  
  
3
#include <stdio.h>
  
4
#include <string.h>
  
5
#include <ctype.h>
  
6
  
  
7
static array(CachedTexture) texture_cache;
  
8
  
  
9
Texture2D GetTexture(const char *name) {
  
10
	if (texture_cache.data == NULL) {
  
11
		array_init(texture_cache);
  
12
	}
  
13
  
  
14
	for (int i = 0; i < texture_cache.length; i++) {
  
15
		if (strcmp(texture_cache.data[i].name, name) == 0) return texture_cache.data[i].tex;
  
16
	}
  
17
  
  
18
	char lname[256];
  
19
	strncpy(lname, name, sizeof(lname));
  
20
	lname[sizeof(lname)-1] = '\0';
  
21
	for (int i = 0; lname[i]; i++) lname[i] = tolower(lname[i]);
  
22
  
  
23
	char path[256];
  
24
	void *data = NULL;
  
25
	size_t size = 0;
  
26
	const char *ext = ".png";
  
27
  
  
28
	snprintf(path, sizeof(path), "textures/%s.png", lname);
  
29
	data = vfs_read(path, &size);
  
30
	if (!data) {
  
31
		snprintf(path, sizeof(path), "textures/%s.jpg", lname);
  
32
		data = vfs_read(path, &size);
  
33
		ext = ".jpg";
  
34
	}
  
35
  
  
36
	Texture2D tex = { 0 };
  
37
	if (data) {
  
38
		Image img = LoadImageFromMemory(ext, data, (int)size);
  
39
		if (img.data) {
  
40
			tex = LoadTextureFromImage(img);
  
41
			UnloadImage(img);
  
42
		}
  
43
		vfs_free(data);
  
44
	}
  
45
  
  
46
	if (tex.id == 0) {
  
47
		TraceLog(LOG_WARNING, "Failed to load texture: '%s'", name);
  
48
		Image img = GenImageChecked(64, 64, 8, 8, (Color){128, 128, 128, 255}, (Color){200, 200, 200, 255});
  
49
		tex = LoadTextureFromImage(img);
  
50
		UnloadImage(img);
  
51
	} else {
  
52
		GenTextureMipmaps(&tex);
  
53
		SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR);
  
54
		SetTextureWrap(tex, TEXTURE_WRAP_REPEAT);
  
55
	}
  
56
  
  
57
	CachedTexture cached;
  
58
	strncpy(cached.name, name, sizeof(cached.name));
  
59
	cached.tex = tex;
  
60
	array_push(texture_cache, cached);
  
61
  
  
62
	return tex;
  
63
}
  
64
  
  
65
void UnloadAssets(void) {
  
66
	for (int i = 0; i < texture_cache.length; i++) {
  
67
		UnloadTexture(texture_cache.data[i].tex);
  
68
	}
  
69
	array_clear(texture_cache);
  
70
}
  
71
  
  
72
Font LoadFontVFS(const char *path, int fontSize) {
  
73
	size_t size = 0;
  
74
	void *data = vfs_read(path, &size);
  
75
	if (data) {
  
76
		Font font = LoadFontFromMemory(".ttf", data, (int)size, fontSize, NULL, 0);
  
77
		vfs_free(data);
  
78
		return font;
  
79
	}
  
80
	return GetFontDefault();
  
81
}
diff --git a/config.h b/config.h
...
5
#define WINDOW_HEIGHT 720
5
#define WINDOW_HEIGHT 720
6
#define WINDOW_TITLE "Stalag"
6
#define WINDOW_TITLE "Stalag"
7
  
7
  
  
8
#define VFS_DATA_PAK "data.pak"
  
9
  
  
10
#define UI_FONT_PATH "fonts/LiberationSans-Bold.ttf"
  
11
#define UI_FONT_SIZE 20
  
12
  
8
#define PLAYER_MOVE_SPEED 200.0f
13
#define PLAYER_MOVE_SPEED 200.0f
9
#define PLAYER_FLY_SPEED 400.0f
14
#define PLAYER_FLY_SPEED 400.0f
10
#define PLAYER_ROTATION_SPEED 0.05f
15
#define PLAYER_ROTATION_SPEED 0.05f
...
diff --git a/game.c b/game.c
1
#include "all.h"
1
#include "all.h"
2
#include <stdio.h>
2
  
3
#include <string.h>
3
#include <string.h>
4
#include <ctype.h>
  
5
  
4
  
6
GameState game;
5
GameState game;
7
static array(CachedTexture) texture_cache;
  
8
  
  
9
Texture2D GetTexture(const char *name) {
  
10
    for (int i = 0; i < texture_cache.length; i++) {
  
11
        if (strcmp(texture_cache.data[i].name, name) == 0) return texture_cache.data[i].tex;
  
12
    }
  
13
  
  
14
    char lname[256];
  
15
    strncpy(lname, name, sizeof(lname));
  
16
    for (int i = 0; lname[i]; i++) lname[i] = tolower(lname[i]);
  
17
  
  
18
    char path[256];
  
19
    void *data = NULL;
  
20
    size_t size = 0;
  
21
    const char *ext = ".jpg";
  
22
  
  
23
    snprintf(path, sizeof(path), "textures/%s.png", lname);
  
24
    data = vfs_read(path, &size);
  
25
    if (data) {
  
26
        ext = ".png";
  
27
    } else {
  
28
        snprintf(path, sizeof(path), "textures/%s.jpg", lname);
  
29
        data = vfs_read(path, &size);
  
30
        ext = ".jpg";
  
31
    }
  
32
  
  
33
    Texture2D tex = { 0 };
  
34
    if (data) {
  
35
        Image img = LoadImageFromMemory(ext, data, (int)size);
  
36
        if (img.data) {
  
37
            tex = LoadTextureFromImage(img);
  
38
            UnloadImage(img);
  
39
        }
  
40
        vfs_free(data);
  
41
    }
  
42
  
  
43
    if (tex.id == 0) {
  
44
        TraceLog(LOG_WARNING, "Failed to load texture: '%s' (tried path: '%s')", name, path);
  
45
        Image img = GenImageChecked(64, 64, 8, 8, (Color){128, 128, 128, 255}, (Color){200, 200, 200, 255});
  
46
        tex = LoadTextureFromImage(img);
  
47
        UnloadImage(img);
  
48
    }
  
49
    
  
50
    if (tex.id != 0) {
  
51
        GenTextureMipmaps(&tex);
  
52
        SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR);
  
53
        SetTextureWrap(tex, TEXTURE_WRAP_REPEAT);
  
54
        
  
55
        CachedTexture cached;
  
56
        strncpy(cached.name, name, sizeof(cached.name));
  
57
        cached.tex = tex;
  
58
        array_push(texture_cache, cached);
  
59
    }
  
60
    
  
61
    return tex;
  
62
}
  
63
  
  
64
void UnloadMap(void) {
  
65
    if (game.world_models) {
  
66
        for (int i = 0; i < game.world_model_count; i++) {
  
67
            UnloadModel(game.world_models[i]);
  
68
        }
  
69
        free(game.world_models);
  
70
        game.world_models = NULL;
  
71
        game.world_model_count = 0;
  
72
    }
  
73
  
  
74
    for (int i = 0; i < texture_cache.length; i++) {
  
75
        UnloadTexture(texture_cache.data[i].tex);
  
76
    }
  
77
    array_clear(texture_cache);
  
78
}
  
79
  
  
80
bool LoadMap(const char *filename) {
  
81
    TraceLog(LOG_INFO, "Loading map: %s", filename);
  
82
    
  
83
    Map map = ParseMap(filename);
  
84
    if (map.entity_count == 0) {
  
85
        TraceLog(LOG_ERROR, "Failed to load map or map is empty: %s", filename);
  
86
        return false;
  
87
    }
  
88
  
  
89
    UnloadMap();
  
90
    
  
91
    array(TextureGroup) groups;
  
92
    array_init(groups);
  
93
    
  
94
    // Default camera if no start found
  
95
    game.camera.position = (Vector3){ 0, 10, 0 };
  
96
    game.camera.target = (Vector3){ 1, 10, 0 };
  
97
    game.camera.up = (Vector3){ 0, 1, 0 };
  
98
    game.camera.fovy = PLAYER_FOV;
  
99
    game.camera.projection = CAMERA_PERSPECTIVE;
  
100
  
  
101
    int total_brushes = 0;
  
102
    for (int i = 0; i < map.entity_count; i++) {
  
103
        MapEntity *e = &map.entities[i];
  
104
        bool is_world = false;
  
105
        const char *classname = "";
  
106
        for (int j = 0; j < e->property_count; j++) {
  
107
            if (strcmp(e->properties[j].key, "classname") == 0) {
  
108
                classname = e->properties[j].value;
  
109
                if (strcmp(classname, "worldspawn") == 0) is_world = true;
  
110
            }
  
111
            if (strcmp(e->properties[j].key, "origin") == 0) {
  
112
                float x, y, z;
  
113
                sscanf(e->properties[j].value, "%f %f %f", &x, &y, &z);
  
114
                if (strcmp(classname, "info_player_start") == 0) {
  
115
                    game.pos = (Vector3){ x, z, -y };
  
116
                    game.camera.position = game.pos;
  
117
                    float angle = 0;
  
118
                    for (int k = 0; k < e->property_count; k++) {
  
119
                        if (strcmp(e->properties[k].key, "angle") == 0) {
  
120
                            angle = (float)atof(e->properties[k].value);
  
121
                        }
  
122
                    }
  
123
                    game.yaw = (angle + 90.0f) * DEG2RAD;
  
124
                    game.pitch = 0;
  
125
                    game.camera.target = Vector3Add(game.pos, (Vector3){sinf(game.yaw), 0, cosf(game.yaw)});
  
126
                    TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f (yaw: %f)", game.pos.x, game.pos.y, game.pos.z, game.yaw);
  
127
                }
  
128
            }
  
129
        }
  
130
        
  
131
        if (is_world) {
  
132
            total_brushes += e->brush_count;
  
133
            for (int j = 0; j < e->brush_count; j++) {
  
134
                MapBrush *brush = &e->brushes[j];
  
135
                for (int p_idx = 0; p_idx < brush->plane_count; p_idx++) {
  
136
                    MapPlane *mp = &brush->planes[p_idx];
  
137
                    Plane plane = PlaneFromPoints(mp->p[0], mp->p[1], mp->p[2]);
  
138
                    
  
139
                    if (Vector3Length(plane.normal) < 0.5f) continue;
  
140
  
  
141
                    Polygon poly = CreateLargeQuad(plane);
  
142
                    for (int k = 0; k < brush->plane_count; k++) {
  
143
                        if (p_idx == k) continue;
  
144
                        Plane clipPlane = PlaneFromPoints(brush->planes[k].p[0], brush->planes[k].p[1], brush->planes[k].p[2]);
  
145
                        if (Vector3Length(clipPlane.normal) < 0.5f) continue;
  
146
  
  
147
                        Polygon next = PolyClip(poly, clipPlane);
  
148
                        PolyFree(poly);
  
149
                        poly = next;
  
150
                    }
  
151
                    if (poly.count >= 3) {
  
152
                        TextureGroup *group = NULL;
  
153
                        for (int g = 0; g < groups.length; g++) {
  
154
                            if (strcmp(groups.data[g].texture, mp->texture) == 0) {
  
155
                                group = &groups.data[g];
  
156
                                break;
  
157
                            }
  
158
                        }
  
159
                        if (!group) {
  
160
                            TextureGroup new_group = { 0 };
  
161
                            strncpy(new_group.texture, mp->texture, sizeof(new_group.texture));
  
162
                            array_init(new_group.vertices);
  
163
                            array_init(new_group.texcoords);
  
164
                            array_init(new_group.normals);
  
165
                            array_push(groups, new_group);
  
166
                            group = &groups.data[groups.length - 1];
  
167
                        }
  
168
                        for (int v = 1; v < poly.count - 1; v++) {
  
169
                            Vector3 v0 = poly.verts[0];
  
170
                            Vector3 v1 = poly.verts[v+1];
  
171
                            Vector3 v2 = poly.verts[v];
  
172
                            array_push(group->vertices, v0.x); array_push(group->vertices, v0.y); array_push(group->vertices, v0.z);
  
173
                            array_push(group->vertices, v1.x); array_push(group->vertices, v1.y); array_push(group->vertices, v1.z);
  
174
                            array_push(group->vertices, v2.x); array_push(group->vertices, v2.y); array_push(group->vertices, v2.z);
  
175
                            Vector2 uv0 = GetUV(v0, mp, plane);
  
176
                            Vector2 uv1 = GetUV(v1, mp, plane);
  
177
                            Vector2 uv2 = GetUV(v2, mp, plane);
  
178
                            array_push(group->texcoords, uv0.x); array_push(group->texcoords, uv0.y);
  
179
                            array_push(group->texcoords, uv1.x); array_push(group->texcoords, uv1.y);
  
180
                            array_push(group->texcoords, uv2.x); array_push(group->texcoords, uv2.y);
  
181
                            Vector3 out_normal = Vector3Scale(plane.normal, -1.0f);
  
182
                            array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
  
183
                            array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
  
184
                            array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
  
185
                        }
  
186
                    }
  
187
                    PolyFree(poly);
  
188
                }
  
189
            }
  
190
        }
  
191
    }
  
192
    
  
193
    array(Model) final_models;
  
194
    array_init(final_models);
  
195
    
  
196
    for (int i = 0; i < groups.length; i++) {
  
197
        TextureGroup *g = &groups.data[i];
  
198
        if (g->vertices.length == 0) continue;
  
199
        Mesh mesh = { 0 };
  
200
        mesh.vertexCount = (int)g->vertices.length / 3;
  
201
        mesh.triangleCount = mesh.vertexCount / 3;
  
202
        mesh.vertices = (float *)malloc(g->vertices.length * sizeof(float));
  
203
        memcpy(mesh.vertices, g->vertices.data, g->vertices.length * sizeof(float));
  
204
        mesh.texcoords = (float *)malloc(g->texcoords.length * sizeof(float));
  
205
        memcpy(mesh.texcoords, g->texcoords.data, g->texcoords.length * sizeof(float));
  
206
        mesh.normals = (float *)malloc(g->normals.length * sizeof(float));
  
207
        memcpy(mesh.normals, g->normals.data, g->normals.length * sizeof(float));
  
208
        UploadMesh(&mesh, false);
  
209
        Model model = LoadModelFromMesh(mesh);
  
210
        model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = GetTexture(g->texture);
  
211
        array_push(final_models, model);
  
212
        array_free(g->vertices);
  
213
        array_free(g->texcoords);
  
214
        array_free(g->normals);
  
215
    }
  
216
    
  
217
    game.world_models = final_models.data;
  
218
    game.world_model_count = (int)final_models.length;
  
219
    array_free(groups);
  
220
    
  
221
    FreeMap(map);
  
222
    TraceLog(LOG_INFO, "Processed %d brushes into %d models", total_brushes, game.world_model_count);
  
223
    return true;
  
224
}
  
225
  
6
  
226
void InitGame(void) {
7
void InitGame(void) {
227
    array_init(texture_cache);
8
	memset(&game, 0, sizeof(game));
228
    game.world_models = NULL;
  
229
    game.world_model_count = 0;
  
230
    
  
231
    // Load UI Font
  
232
    size_t font_size = 0;
  
233
    void *font_data = vfs_read("fonts/LiberationSans-Bold.ttf", &font_size);
  
234
    if (font_data) {
  
235
        game.font_ui = LoadFontFromMemory(".ttf", font_data, (int)font_size, 20, NULL, 0);
  
236
        vfs_free(font_data);
  
237
    } else {
  
238
        game.font_ui = GetFontDefault();
  
239
    }
  
240
  
9
  
241
    game.mode = STATE_TITLE;
10
	game.font_ui = LoadFontVFS(UI_FONT_PATH, UI_FONT_SIZE);
  
11
	game.mode = STATE_MENU;
  
12
	game.cursor_captured = false;
  
13
	EnableCursor();
242
  
14
  
243
    game.cursor_captured = false;
15
	game.player.move_mode = MOVE_NORMAL;
244
    EnableCursor();
16
	game.player.pos = (Vector3){ 0, 0, 0 };
  
17
	game.player.velocity = (Vector3){ 0, 0, 0 };
  
18
	game.player.is_grounded = false;
245
  
19
  
246
    game.move_mode = MOVE_NORMAL;
20
	// Camera setup
247
    game.velocity = (Vector3){ 0, 0, 0 };
21
	game.camera.up = (Vector3){ 0, 1, 0 };
248
    game.is_grounded = false;
22
	game.camera.fovy = PLAYER_FOV;
  
23
	game.camera.projection = CAMERA_PERSPECTIVE;
249
}
24
}
250
  
25
  
251
void SetMap(const char *path) {
26
void SetMap(const char *path) {
252
    strncpy(game.map_path, path, sizeof(game.map_path) - 1);
27
	strncpy(game.map_path, path, sizeof(game.map_path) - 1);
253
    game.map_path[sizeof(game.map_path) - 1] = '\0';
28
	game.map_path[sizeof(game.map_path) - 1] = '\0';
254
}
29
}
255
  
30
  
256
void UpdateGame(void) {
31
void UpdateGame(void) {
257
    if (game.mode == STATE_TITLE) {
32
	if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
258
        if (IsKeyPressed(KEY_ENTER)) {
33
		game.cursor_captured = !game.cursor_captured;
259
            if (LoadMap(game.map_path)) {
34
		if (game.cursor_captured) DisableCursor();
260
                game.mode = STATE_PLAYING;
35
		else EnableCursor();
261
            }
36
	}
262
        }
  
263
        return;
  
264
    }
  
265
  
  
266
    if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
  
267
        game.cursor_captured = !game.cursor_captured;
  
268
        if (game.cursor_captured) DisableCursor();
  
269
        else EnableCursor();
  
270
    }
  
271
  
  
272
    if (IsKeyPressed(KEY_ONE)) LoadMap("maps/demo1.map");
  
273
    if (IsKeyPressed(KEY_TWO)) LoadMap("maps/demo2.map");
  
274
    if (IsKeyPressed(KEY_THREE)) LoadMap("maps/demo3.map");
  
275
  
37
  
276
    if (IsKeyPressed(KEY_V)) {
38
	if (IsKeyPressed(KEY_V)) {
277
        game.vsync = !game.vsync;
39
		game.vsync = !game.vsync;
278
        if (game.vsync) {
40
		if (game.vsync) {
279
            SetWindowState(FLAG_VSYNC_HINT);
41
			SetWindowState(FLAG_VSYNC_HINT);
280
            SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
42
			SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
281
        } else {
43
		} else {
282
            ClearWindowState(FLAG_VSYNC_HINT);
44
			ClearWindowState(FLAG_VSYNC_HINT);
283
            SetTargetFPS(0);
45
			SetTargetFPS(0);
284
        }
46
		}
285
    }
47
	}
286
  
48
  
287
    UpdatePlayer();
49
	UpdatePlayer();
288
}
50
}
289
  
51
  
290
void DrawGame(void) {
52
void DrawGame(void) {
291
    BeginDrawing();
53
	BeginDrawing();
292
    ClearBackground(BLACK);
54
	ClearBackground(DARKGRAY);
293
  
55
  
294
    if (game.mode == STATE_TITLE) {
56
	BeginMode3D(game.camera);
295
        int screenWidth = GetScreenWidth();
  
296
        int screenHeight = GetScreenHeight();
  
297
  
57
  
298
        const char *title = "STALAG";
58
	rlEnableBackfaceCulling();
299
        const char *sub = "Press ENTER to Start";
59
	for (int i = 0; i < game.map.count; i++) {
300
        
60
		DrawModel(game.map.models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE);
301
        int titleSize = 60;
61
	}
302
        int subSize = 20;
  
303
  
62
  
304
        Vector2 titlePos = { 
63
	EndMode3D();
305
            (float)(screenWidth - MeasureTextEx(game.font_ui, title, (float)titleSize, 4).x) / 2, 
  
306
            (float)screenHeight / 2 - 40 
  
307
        };
  
308
        Vector2 subPos = { 
  
309
            (float)(screenWidth - MeasureTextEx(game.font_ui, sub, (float)subSize, 2).x) / 2, 
  
310
            (float)screenHeight / 2 + 40 
  
311
        };
  
312
  
64
  
313
        DrawTextEx(game.font_ui, title, titlePos, (float)titleSize, 4, WHITE);
65
	DrawCrosshair();
314
        DrawTextEx(game.font_ui, sub, subPos, (float)subSize, 2, GRAY);
66
	DrawDebugInfo();
315
    } else {
  
316
        ClearBackground(DARKGRAY);
  
317
        BeginMode3D(game.camera);
  
318
        
  
319
        // Enable backface culling to hide interior faces of brushes
  
320
        rlEnableBackfaceCulling();
  
321
        for (int i = 0; i < game.world_model_count; i++) {
  
322
            DrawModel(game.world_models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE);
  
323
        }
  
324
        
  
325
        EndMode3D();
  
326
        
  
327
        DrawCrosshair();
  
328
        DrawDebugInfo();
  
329
    }
  
330
  
67
  
331
    EndDrawing();
68
	EndDrawing();
332
}
69
}
diff --git a/interface.c b/interface.c
...
8
}
8
}
9
  
9
  
10
void DrawDebugInfo(void) {
10
void DrawDebugInfo(void) {
11
    DrawTextEx(game.font_ui, TextFormat("%i FPS", GetFPS()), (Vector2){ 10, 10 }, 20, 2, GREEN);
11
	DrawTextEx(game.font_ui, TextFormat("%i FPS", GetFPS()), (Vector2){ 10, 10 }, 20, 2, GREEN);
12
    DrawTextEx(game.font_ui, TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), (Vector2){ 10, 35 }, 20, 2, GREEN);
12
	DrawTextEx(game.font_ui, TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), (Vector2){ 10, 35 }, 20, 2, GREEN);
13
    DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN);
13
	DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.player.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN);
14
}
14
}
diff --git a/main.c b/main.c
1
#define _POSIX_C_SOURCE 200809L
  
2
#define NONSTD_IMPLEMENTATION
1
#define NONSTD_IMPLEMENTATION
3
#define VFS_IMPLEMENTATION
2
#define VFS_IMPLEMENTATION
4
#include "all.h"
3
#include "all.h"
  
4
  
5
#include <getopt.h>
5
#include <getopt.h>
6
#include <string.h>
  
7
  
6
  
8
int main(int argc, char *argv[]) {
7
int main(int argc, char *argv[]) {
9
    char map_path[256] = "maps/demo3.map";
8
	stringb map_path;
10
    bool skip_title = false;
9
	sb_init(&map_path, 256);
  
10
	sb_append_cstr(&map_path, "maps/demo3.map");
11
  
11
  
12
    static struct option long_options[] = {
12
	bool skip_title = false;
13
        {"map", required_argument, 0, 'm'},
  
14
        {0, 0, 0, 0}
  
15
    };
  
16
  
13
  
17
    int opt;
14
	static struct option long_options[] = {
18
    int option_index = 0;
15
		{"map", required_argument, 0, 'm'},
19
    while ((opt = getopt_long_only(argc, argv, "m:", long_options, &option_index)) != -1) {
16
		{0, 0, 0, 0}
20
        switch (opt) {
17
	};
21
            case 'm':
  
22
                strncpy(map_path, optarg, sizeof(map_path) - 1);
  
23
                skip_title = true;
  
24
                break;
  
25
        }
  
26
    }
  
27
  
18
  
28
    SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI);
19
	int opt;
29
    InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE);
20
	int option_index = 0;
  
21
	while ((opt = getopt_long_only(argc, argv, "m:", long_options, &option_index)) != -1) {
  
22
		switch (opt) {
  
23
			case 'm':
  
24
				sb_free(&map_path);
  
25
				sb_init(&map_path, 256);
  
26
				sb_append_cstr(&map_path, optarg);
  
27
				skip_title = true;
  
28
				break;
  
29
		}
  
30
	}
30
  
31
  
31
    int monitor = GetCurrentMonitor();
32
	SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI);
32
    SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, 
33
	InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE);
33
                      (GetMonitorHeight(monitor) - GetScreenHeight()) / 2);
34
  
34
    
35
	int monitor = GetCurrentMonitor();
35
    SetTargetFPS(GetMonitorRefreshRate(monitor));
36
	SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, (GetMonitorHeight(monitor) - GetScreenHeight()) / 2);
  
37
	SetTargetFPS(GetMonitorRefreshRate(monitor));
  
38
  
  
39
	vfs_init(VFS_DATA_PAK);
  
40
	InitGame();
  
41
	SetMap(map_path.data);
36
  
42
  
37
    vfs_init("data.pak");
43
	if (skip_title) {
38
    InitGame();
44
		if (LoadMap(game.map_path)) {
39
    SetMap(map_path);
45
			game.mode = STATE_PLAYING;
  
46
			game.cursor_captured = true;
  
47
			DisableCursor();
  
48
		}
  
49
	}
40
  
50
  
41
    if (skip_title) {
51
	game.vsync = true;
42
        if (LoadMap(game.map_path)) {
  
43
            game.mode = STATE_PLAYING;
  
44
            game.cursor_captured = true;
  
45
            DisableCursor();
  
46
        }
  
47
    }
  
48
  
52
  
49
    game.vsync = true;
53
	while (!WindowShouldClose()) {
  
54
		// Global Map Switching (Debug/Demo)
  
55
		if (IsKeyPressed(KEY_ONE)) {
  
56
			if (LoadMap("maps/demo1.map")) {
  
57
				game.mode = STATE_PLAYING;
  
58
				game.cursor_captured = true;
  
59
				DisableCursor();
  
60
			}
  
61
		}
  
62
		if (IsKeyPressed(KEY_TWO)) {
  
63
			if (LoadMap("maps/demo2.map")) {
  
64
				game.mode = STATE_PLAYING;
  
65
				game.cursor_captured = true;
  
66
				DisableCursor();
  
67
			}
  
68
		}
  
69
		if (IsKeyPressed(KEY_THREE)) {
  
70
			if (LoadMap("maps/demo3.map")) {
  
71
				game.mode = STATE_PLAYING;
  
72
				game.cursor_captured = true;
  
73
				DisableCursor();
  
74
			}
  
75
		}
50
  
76
  
51
    while (!WindowShouldClose()) {
77
		// Game State Switcher
52
        UpdateGame();
78
		switch (game.mode) {
53
        DrawGame();
79
			case STATE_MENU:
54
    }
80
				UpdateMenu();
  
81
				DrawMenu();
  
82
				break;
  
83
			case STATE_PLAYING:
  
84
				UpdateGame();
  
85
				DrawGame();
  
86
				break;
  
87
		}
  
88
	}
55
  
89
  
56
    vfs_shutdown();
90
	UnloadMap();
57
    CloseWindow();
91
	UnloadAssets();
  
92
	vfs_shutdown();
  
93
	CloseWindow();
  
94
	sb_free(&map_path);
58
  
95
  
59
    return 0;
96
	return 0;
60
}
97
}
diff --git a/map.c b/map.c
1
#include "all.h"
1
#include "all.h"
  
2
  
2
#include <ctype.h>
3
#include <ctype.h>
3
#include <stdlib.h>
4
#include <stdlib.h>
4
#include <stdio.h>
5
#include <stdio.h>
  
6
#include <string.h>
  
7
#include <float.h>
5
  
8
  
6
char map_peek(MapParser *p) {
9
char map_peek(MapParser *p) {
7
    if (p->pos >= p->length) return 0;
10
	if (p->pos >= p->length) return 0;
8
    return p->data[p->pos];
11
	return p->data[p->pos];
9
}
12
}
10
  
13
  
11
char map_get(MapParser *p) {
14
char map_get(MapParser *p) {
12
    if (p->pos >= p->length) return 0;
15
	if (p->pos >= p->length) return 0;
13
    return p->data[p->pos++];
16
	return p->data[p->pos++];
14
}
17
}
15
  
18
  
16
void map_skip_whitespace(MapParser *p) {
19
void map_skip_whitespace(MapParser *p) {
17
    while (true) {
20
	while (true) {
18
        char c = map_peek(p);
21
		char c = map_peek(p);
19
        if (isspace(c)) {
22
		if (isspace(c)) {
20
            map_get(p);
23
			map_get(p);
21
        } else if (c == '/' && p->data[p->pos + 1] == '/') {
24
		} else if (c == '/' && p->data[p->pos + 1] == '/') {
22
            while (map_peek(p) != '\n' && map_peek(p) != 0) map_get(p);
25
			while (map_peek(p) != '\n' && map_peek(p) != 0) map_get(p);
23
        } else {
26
		} else {
24
            break;
27
			break;
25
        }
28
		}
26
    }
29
	}
27
}
30
}
28
  
31
  
29
bool map_expect(MapParser *p, char expected) {
32
bool map_expect(MapParser *p, char expected) {
30
    map_skip_whitespace(p);
33
	map_skip_whitespace(p);
31
    if (map_get(p) == expected) return true;
34
	if (map_get(p) == expected) return true;
32
    return false;
35
	return false;
33
}
36
}
34
  
37
  
35
void map_parse_token(MapParser *p, char *buffer, int size) {
38
void map_parse_token(MapParser *p, char *buffer, int size) {
36
    map_skip_whitespace(p);
39
	map_skip_whitespace(p);
37
    int i = 0;
40
	int i = 0;
38
    bool quoted = false;
41
	bool quoted = false;
39
    if (map_peek(p) == '"') {
42
	if (map_peek(p) == '"') {
40
        quoted = true;
43
		quoted = true;
41
        map_get(p);
44
		map_get(p);
42
    }
45
	}
43
  
46
  
44
    while (true) {
47
	while (true) {
45
        char c = map_peek(p);
48
		char c = map_peek(p);
46
        if (c == 0) break;
49
		if (c == 0) break;
47
        if (quoted) {
50
		if (quoted) {
48
            if (c == '"') {
51
			if (c == '"') {
49
                map_get(p);
52
				map_get(p);
50
                break;
53
				break;
51
            }
54
			}
52
        } else {
55
		} else {
53
            if (isspace(c) || c == '(' || c == ')' || c == '{' || c == '}') break;
56
			if (isspace(c) || c == '(' || c == ')' || c == '{' || c == '}') break;
54
        }
57
		}
55
        if (i < size - 1) buffer[i++] = map_get(p);
58
		if (i < size - 1) buffer[i++] = map_get(p);
56
        else map_get(p);
59
		else map_get(p);
57
    }
60
	}
58
    buffer[i] = 0;
61
	buffer[i] = 0;
59
}
62
}
60
  
63
  
61
Vector3 map_parse_vector(MapParser *p) {
64
Vector3 map_parse_vector(MapParser *p) {
62
    map_expect(p, '(');
65
	map_expect(p, '(');
63
    char buf[64];
66
	char buf[64];
64
    map_parse_token(p, buf, sizeof(buf));
67
	map_parse_token(p, buf, sizeof(buf));
65
    float x = (float)atof(buf);
68
	float x = (float)atof(buf);
66
    map_parse_token(p, buf, sizeof(buf));
69
	map_parse_token(p, buf, sizeof(buf));
67
    float y = (float)atof(buf);
70
	float y = (float)atof(buf);
68
    map_parse_token(p, buf, sizeof(buf));
71
	map_parse_token(p, buf, sizeof(buf));
69
    float z = (float)atof(buf);
72
	float z = (float)atof(buf);
70
    map_expect(p, ')');
73
	map_expect(p, ')');
71
    return (Vector3){ x, z, -y };
74
	return (Vector3){ x, z, -y };
72
}
75
}
73
  
76
  
74
Map ParseMap(const char *filename) {
77
Map ParseMap(const char *filename) {
75
    size_t size;
78
	size_t size;
76
    char *data = (char *)vfs_read(filename, &size);
79
	char *data = (char *)vfs_read(filename, &size);
77
    Map map = { 0 };
80
	Map map = { 0 };
78
    if (!data) return map;
81
	if (!data) return map;
  
82
  
  
83
	MapParser p = { data, size, 0 };
  
84
	array(MapEntity) entities;
  
85
	array_init(entities);
79
  
86
  
80
    MapParser p = { data, size, 0 };
87
	while (true) {
81
    array(MapEntity) entities;
88
		map_skip_whitespace(&p);
82
    array_init(entities);
89
		if (map_peek(&p) == 0) break;
  
90
  
  
91
		if (map_expect(&p, '{')) {
  
92
			MapEntity entity = { 0 };
  
93
			array(MapProperty) props;
  
94
			array(MapBrush) brushes;
  
95
			array_init(props);
  
96
			array_init(brushes);
83
  
97
  
84
    while (true) {
98
			while (true) {
85
        map_skip_whitespace(&p);
99
				map_skip_whitespace(&p);
86
        if (map_peek(&p) == 0) break;
100
				char c = map_peek(&p);
  
101
				if (c == '}') {
  
102
					map_get(&p);
  
103
					break;
  
104
				} else if (c == '"') {
  
105
					MapProperty prop;
  
106
					map_parse_token(&p, prop.key, sizeof(prop.key));
  
107
					map_parse_token(&p, prop.value, sizeof(prop.value));
  
108
					array_push(props, prop);
  
109
				} else if (c == '{') {
  
110
					map_get(&p);
  
111
					MapBrush brush = { 0 };
  
112
					array(MapPlane) planes;
  
113
					array_init(planes);
  
114
					while (true) {
  
115
						map_skip_whitespace(&p);
  
116
						if (map_peek(&p) == '}') {
  
117
							map_get(&p);
  
118
							break;
  
119
						}
  
120
						MapPlane plane;
  
121
						plane.p[0] = map_parse_vector(&p);
  
122
						plane.p[1] = map_parse_vector(&p);
  
123
						plane.p[2] = map_parse_vector(&p);
  
124
						map_parse_token(&p, plane.texture, sizeof(plane.texture));
87
  
125
  
88
        if (map_expect(&p, '{')) {
126
						char buf[64];
89
            MapEntity entity = { 0 };
127
						map_parse_token(&p, buf, sizeof(buf)); plane.shift[0] = (float)atof(buf);
90
            array(MapProperty) props;
128
						map_parse_token(&p, buf, sizeof(buf)); plane.shift[1] = (float)atof(buf);
91
            array(MapBrush) brushes;
129
						map_parse_token(&p, buf, sizeof(buf)); plane.rotate = (float)atof(buf);
92
            array_init(props);
130
						map_parse_token(&p, buf, sizeof(buf)); plane.scale[0] = (float)atof(buf);
93
            array_init(brushes);
131
						map_parse_token(&p, buf, sizeof(buf)); plane.scale[1] = (float)atof(buf);
94
  
132
  
95
            while (true) {
133
						array_push(planes, plane);
96
                map_skip_whitespace(&p);
134
					}
97
                char c = map_peek(&p);
135
					brush.planes = planes.data;
98
                if (c == '}') {
136
					brush.plane_count = (int)planes.length;
99
                    map_get(&p);
137
					array_push(brushes, brush);
100
                    break;
138
				} else {
101
                } else if (c == '"') {
139
					map_get(&p);
102
                    MapProperty prop;
140
				}
103
                    map_parse_token(&p, prop.key, sizeof(prop.key));
141
			}
104
                    map_parse_token(&p, prop.value, sizeof(prop.value));
142
			entity.properties = props.data;
105
                    array_push(props, prop);
143
			entity.property_count = (int)props.length;
106
                } else if (c == '{') {
144
			entity.brushes = brushes.data;
107
                    map_get(&p);
145
			entity.brush_count = (int)brushes.length;
108
                    MapBrush brush = { 0 };
146
			array_push(entities, entity);
109
                    array(MapPlane) planes;
147
		}
110
                    array_init(planes);
148
	}
111
                    while (true) {
  
112
                        map_skip_whitespace(&p);
  
113
                        if (map_peek(&p) == '}') {
  
114
                            map_get(&p);
  
115
                            break;
  
116
                        }
  
117
                        MapPlane plane;
  
118
                        plane.p[0] = map_parse_vector(&p);
  
119
                        plane.p[1] = map_parse_vector(&p);
  
120
                        plane.p[2] = map_parse_vector(&p);
  
121
                        map_parse_token(&p, plane.texture, sizeof(plane.texture));
  
122
                        
  
123
                        char buf[64];
  
124
                        map_parse_token(&p, buf, sizeof(buf)); plane.shift[0] = (float)atof(buf);
  
125
                        map_parse_token(&p, buf, sizeof(buf)); plane.shift[1] = (float)atof(buf);
  
126
                        map_parse_token(&p, buf, sizeof(buf)); plane.rotate = (float)atof(buf);
  
127
                        map_parse_token(&p, buf, sizeof(buf)); plane.scale[0] = (float)atof(buf);
  
128
                        map_parse_token(&p, buf, sizeof(buf)); plane.scale[1] = (float)atof(buf);
  
129
                        
  
130
                        array_push(planes, plane);
  
131
                    }
  
132
                    brush.planes = planes.data;
  
133
                    brush.plane_count = (int)planes.length;
  
134
                    array_push(brushes, brush);
  
135
                } else {
  
136
                    map_get(&p);
  
137
                }
  
138
            }
  
139
            entity.properties = props.data;
  
140
            entity.property_count = (int)props.length;
  
141
            entity.brushes = brushes.data;
  
142
            entity.brush_count = (int)brushes.length;
  
143
            array_push(entities, entity);
  
144
        }
  
145
    }
  
146
  
149
  
147
    map.entities = entities.data;
150
	map.entities = entities.data;
148
    map.entity_count = (int)entities.length;
151
	map.entity_count = (int)entities.length;
149
    vfs_free(data);
152
	vfs_free(data);
150
    return map;
153
	return map;
151
}
154
}
152
  
155
  
153
void FreeMap(Map map) {
156
void FreeMap(Map map) {
154
    for (int i = 0; i < map.entity_count; i++) {
157
	for (int i = 0; i < map.entity_count; i++) {
155
        MapEntity *e = &map.entities[i];
158
		MapEntity *e = &map.entities[i];
156
        if (e->properties) free(e->properties);
159
		if (e->properties) free(e->properties);
157
        for (int j = 0; j < e->brush_count; j++) {
160
		for (int j = 0; j < e->brush_count; j++) {
158
            if (e->brushes[j].planes) free(e->brushes[j].planes);
161
			if (e->brushes[j].planes) free(e->brushes[j].planes);
159
        }
162
		}
160
        if (e->brushes) free(e->brushes);
163
		if (e->brushes) free(e->brushes);
161
    }
164
	}
162
    if (map.entities) free(map.entities);
165
	if (map.entities) free(map.entities);
163
}
166
}
164
  
167
  
165
Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3) {
168
Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3) {
166
    Vector3 v1 = Vector3Subtract(p2, p1);
169
	Vector3 v1 = Vector3Subtract(p2, p1);
167
    Vector3 v2 = Vector3Subtract(p3, p1);
170
	Vector3 v2 = Vector3Subtract(p3, p1);
168
    Vector3 normal = Vector3Normalize(Vector3CrossProduct(v1, v2));
171
	Vector3 normal = Vector3Normalize(Vector3CrossProduct(v1, v2));
169
    return (Plane){ normal, Vector3DotProduct(normal, p1) };
172
	return (Plane){ normal, Vector3DotProduct(normal, p1) };
170
}
173
}
171
  
174
  
172
void PolyFree(Polygon p) {
175
void PolyFree(Polygon p) {
173
    if (p.verts) free(p.verts);
176
	if (p.verts) free(p.verts);
174
}
177
}
175
  
178
  
176
Polygon PolyClip(Polygon poly, Plane plane) {
179
Polygon PolyClip(Polygon poly, Plane plane) {
177
    if (poly.count == 0) return poly;
180
	if (poly.count == 0) return poly;
178
    
181
  
179
    array(Vector3) out_verts;
182
	array(Vector3) out_verts;
180
    array_init(out_verts);
183
	array_init(out_verts);
181
  
184
  
182
    for (int i = 0; i < poly.count; i++) {
185
	for (int i = 0; i < poly.count; i++) {
183
        Vector3 p1 = poly.verts[i];
186
		Vector3 p1 = poly.verts[i];
184
        Vector3 p2 = poly.verts[(i + 1) % poly.count];
187
		Vector3 p2 = poly.verts[(i + 1) % poly.count];
185
  
188
  
186
        float d1 = Vector3DotProduct(plane.normal, p1) - plane.dist;
189
		float d1 = Vector3DotProduct(plane.normal, p1) - plane.dist;
187
        float d2 = Vector3DotProduct(plane.normal, p2) - plane.dist;
190
		float d2 = Vector3DotProduct(plane.normal, p2) - plane.dist;
188
  
191
  
189
        // Using a smaller epsilon and more robust logic
192
		const float eps = 0.001f;
190
        const float eps = 0.001f;
  
191
  
193
  
192
        if (d1 >= -eps) {
194
		if (d1 >= -eps) {
193
            if (d2 >= -eps) {
195
			if (d2 >= -eps) {
194
                array_push(out_verts, p2);
196
				array_push(out_verts, p2);
195
            } else {
197
			} else {
196
                float t = d1 / (d1 - d2);
198
				float t = d1 / (d1 - d2);
197
                Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t));
199
				Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t));
198
                array_push(out_verts, intersect);
200
				array_push(out_verts, intersect);
199
            }
201
			}
200
        } else {
202
		} else {
201
            if (d2 >= -eps) {
203
			if (d2 >= -eps) {
202
                float t = d1 / (d1 - d2);
204
				float t = d1 / (d1 - d2);
203
                Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t));
205
				Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t));
204
                array_push(out_verts, intersect);
206
				array_push(out_verts, intersect);
205
                array_push(out_verts, p2);
207
				array_push(out_verts, p2);
206
            }
208
			}
207
        }
209
		}
208
    }
210
	}
209
  
211
  
210
    Polygon res;
212
	Polygon res;
211
    res.verts = out_verts.data;
213
	res.verts = out_verts.data;
212
    res.count = (int)out_verts.length;
214
	res.count = (int)out_verts.length;
213
    return res;
215
	return res;
214
}
216
}
215
  
217
  
216
Polygon CreateLargeQuad(Plane plane) {
218
Polygon CreateLargeQuad(Plane plane) {
217
    Vector3 n = plane.normal;
219
	Vector3 n = plane.normal;
218
    Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 };
220
	Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 };
219
    Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n));
221
	Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n));
220
    up = Vector3CrossProduct(n, right);
222
	up = Vector3CrossProduct(n, right);
221
  
223
  
222
    Vector3 center = Vector3Scale(n, plane.dist);
224
	Vector3 center = Vector3Scale(n, plane.dist);
223
    float size = 10000.0f;
225
	float size = 10000.0f;
224
    
226
  
225
    Polygon poly;
227
	Polygon poly;
226
    poly.verts = ALLOC(Vector3, 4);
228
	poly.verts = ALLOC(Vector3, 4);
227
    poly.count = 4;
229
	poly.count = 4;
228
    poly.verts[0] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, size)));
230
	poly.verts[0] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, size)));
229
    poly.verts[1] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, size)));
231
	poly.verts[1] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, size)));
230
    poly.verts[2] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, -size)));
232
	poly.verts[2] = Vector3Add(center, Vector3Add(Vector3Scale(right, -size), Vector3Scale(up, -size)));
231
    poly.verts[3] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, -size)));
233
	poly.verts[3] = Vector3Add(center, Vector3Add(Vector3Scale(right, size), Vector3Scale(up, -size)));
232
    
234
  
233
    return poly;
235
	return poly;
234
}
236
}
235
  
237
  
236
Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane) {
238
Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane) {
237
    Vector3 n = plane.normal;
239
	Vector3 n = plane.normal;
238
    Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 };
240
	Vector3 up = (fabsf(n.y) < 0.999f) ? (Vector3){ 0, 1, 0 } : (Vector3){ 1, 0, 0 };
239
    Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n));
241
	Vector3 right = Vector3Normalize(Vector3CrossProduct(up, n));
240
    up = Vector3CrossProduct(n, right);
242
	up = Vector3CrossProduct(n, right);
241
    
243
  
242
    float u = Vector3DotProduct(p, right) * (1.0f / (mp->scale[0] ? mp->scale[0] : 1.0f)) + mp->shift[0];
244
	float u = Vector3DotProduct(p, right) * (1.0f / (mp->scale[0] ? mp->scale[0] : 1.0f)) + mp->shift[0];
243
    float v = Vector3DotProduct(p, up) * (1.0f / (mp->scale[1] ? mp->scale[1] : 1.0f)) + mp->shift[1];
245
	float v = Vector3DotProduct(p, up) * (1.0f / (mp->scale[1] ? mp->scale[1] : 1.0f)) + mp->shift[1];
244
    
246
  
245
    return (Vector2){ u / 64.0f, v / 64.0f };
247
	return (Vector2){ u / 64.0f, v / 64.0f };
  
248
}
  
249
  
  
250
void UnloadMap(void) {
  
251
	if (game.map.models) {
  
252
		for (int i = 0; i < game.map.count; i++) {
  
253
			UnloadModel(game.map.models[i]);
  
254
		}
  
255
		free(game.map.models);
  
256
		game.map.models = NULL;
  
257
		game.map.count = 0;
  
258
	}
  
259
}
  
260
  
  
261
bool LoadMap(const char *filename) {
  
262
	TraceLog(LOG_INFO, "Loading map: %s", filename);
  
263
  
  
264
	Map map = ParseMap(filename);
  
265
	if (map.entity_count == 0) {
  
266
		TraceLog(LOG_ERROR, "Failed to load map or map is empty: %s", filename);
  
267
		return false;
  
268
	}
  
269
  
  
270
	UnloadMap();
  
271
  
  
272
	array(TextureGroup) groups;
  
273
	array_init(groups);
  
274
  
  
275
	// Default player state if no start found
  
276
	game.player.pos = (Vector3){ 0, 10, 0 };
  
277
	game.player.yaw = 0;
  
278
	game.player.pitch = 0;
  
279
  
  
280
	int total_brushes = 0;
  
281
	for (int i = 0; i < map.entity_count; i++) {
  
282
		MapEntity *e = &map.entities[i];
  
283
		bool is_world = false;
  
284
		const char *classname = "";
  
285
  
  
286
		for (int j = 0; j < e->property_count; j++) {
  
287
			if (strcmp(e->properties[j].key, "classname") == 0) {
  
288
				classname = e->properties[j].value;
  
289
				if (strcmp(classname, "worldspawn") == 0) is_world = true;
  
290
			}
  
291
		}
  
292
  
  
293
		// Handle entities (like player start)
  
294
		for (int j = 0; j < e->property_count; j++) {
  
295
			if (strcmp(e->properties[j].key, "origin") == 0) {
  
296
				float x, y, z;
  
297
				sscanf(e->properties[j].value, "%f %f %f", &x, &y, &z);
  
298
				if (strcmp(classname, "info_player_start") == 0) {
  
299
					game.player.pos = (Vector3){ x, z, -y };
  
300
					float angle = 0;
  
301
					for (int k = 0; k < e->property_count; k++) {
  
302
						if (strcmp(e->properties[k].key, "angle") == 0) {
  
303
							angle = (float)atof(e->properties[k].value);
  
304
						}
  
305
					}
  
306
					game.player.yaw = (angle + 90.0f) * DEG2RAD;
  
307
					game.player.pitch = 0;
  
308
					TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f", game.player.pos.x, game.player.pos.y, game.player.pos.z);
  
309
				}
  
310
			}
  
311
		}
  
312
  
  
313
		if (is_world) {
  
314
			total_brushes += e->brush_count;
  
315
			for (int j = 0; j < e->brush_count; j++) {
  
316
				MapBrush *brush = &e->brushes[j];
  
317
				for (int p_idx = 0; p_idx < brush->plane_count; p_idx++) {
  
318
					MapPlane *mp = &brush->planes[p_idx];
  
319
					Plane plane = PlaneFromPoints(mp->p[0], mp->p[1], mp->p[2]);
  
320
  
  
321
					if (Vector3Length(plane.normal) < 0.5f) continue;
  
322
  
  
323
					Polygon poly = CreateLargeQuad(plane);
  
324
					for (int k = 0; k < brush->plane_count; k++) {
  
325
						if (p_idx == k) continue;
  
326
						Plane clipPlane = PlaneFromPoints(brush->planes[k].p[0], brush->planes[k].p[1], brush->planes[k].p[2]);
  
327
						if (Vector3Length(clipPlane.normal) < 0.5f) continue;
  
328
  
  
329
						Polygon next = PolyClip(poly, clipPlane);
  
330
						PolyFree(poly);
  
331
						poly = next;
  
332
					}
  
333
  
  
334
					if (poly.count >= 3) {
  
335
						TextureGroup *group = NULL;
  
336
						for (int g = 0; g < groups.length; g++) {
  
337
							if (strcmp(groups.data[g].texture, mp->texture) == 0) {
  
338
								group = &groups.data[g];
  
339
								break;
  
340
							}
  
341
						}
  
342
						if (!group) {
  
343
							TextureGroup new_group = { 0 };
  
344
							strncpy(new_group.texture, mp->texture, sizeof(new_group.texture));
  
345
							array_init(new_group.vertices);
  
346
							array_init(new_group.texcoords);
  
347
							array_init(new_group.normals);
  
348
							array_push(groups, new_group);
  
349
							group = &groups.data[groups.length - 1];
  
350
						}
  
351
						for (int v = 1; v < poly.count - 1; v++) {
  
352
							Vector3 v0 = poly.verts[0];
  
353
							Vector3 v1 = poly.verts[v+1];
  
354
							Vector3 v2 = poly.verts[v];
  
355
							array_push(group->vertices, v0.x); array_push(group->vertices, v0.y); array_push(group->vertices, v0.z);
  
356
							array_push(group->vertices, v1.x); array_push(group->vertices, v1.y); array_push(group->vertices, v1.z);
  
357
							array_push(group->vertices, v2.x); array_push(group->vertices, v2.y); array_push(group->vertices, v2.z);
  
358
							Vector2 uv0 = GetUV(v0, mp, plane);
  
359
							Vector2 uv1 = GetUV(v1, mp, plane);
  
360
							Vector2 uv2 = GetUV(v2, mp, plane);
  
361
							array_push(group->texcoords, uv0.x); array_push(group->texcoords, uv0.y);
  
362
							array_push(group->texcoords, uv1.x); array_push(group->texcoords, uv1.y);
  
363
							array_push(group->texcoords, uv2.x); array_push(group->texcoords, uv2.y);
  
364
							Vector3 out_normal = Vector3Scale(plane.normal, -1.0f);
  
365
							array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
  
366
							array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
  
367
							array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
  
368
						}
  
369
					}
  
370
					PolyFree(poly);
  
371
				}
  
372
			}
  
373
		}
  
374
	}
  
375
  
  
376
	array(Model) final_models;
  
377
	array_init(final_models);
  
378
  
  
379
	for (int i = 0; i < groups.length; i++) {
  
380
		TextureGroup *g = &groups.data[i];
  
381
		if (g->vertices.length == 0) continue;
  
382
		Mesh mesh = { 0 };
  
383
		mesh.vertexCount = (int)g->vertices.length / 3;
  
384
		mesh.triangleCount = mesh.vertexCount / 3;
  
385
		mesh.vertices = (float *)malloc(g->vertices.length * sizeof(float));
  
386
		memcpy(mesh.vertices, g->vertices.data, g->vertices.length * sizeof(float));
  
387
		mesh.texcoords = (float *)malloc(g->texcoords.length * sizeof(float));
  
388
		memcpy(mesh.texcoords, g->texcoords.data, g->texcoords.length * sizeof(float));
  
389
		mesh.normals = (float *)malloc(g->normals.length * sizeof(float));
  
390
		memcpy(mesh.normals, g->normals.data, g->normals.length * sizeof(float));
  
391
		UploadMesh(&mesh, false);
  
392
		Model model = LoadModelFromMesh(mesh);
  
393
		model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = GetTexture(g->texture);
  
394
		array_push(final_models, model);
  
395
		array_free(g->vertices);
  
396
		array_free(g->texcoords);
  
397
		array_free(g->normals);
  
398
	}
  
399
  
  
400
	game.map.models = final_models.data;
  
401
	game.map.count = (int)final_models.length;
  
402
	array_free(groups);
  
403
  
  
404
	FreeMap(map);
  
405
	TraceLog(LOG_INFO, "Processed %d brushes into %d models", total_brushes, game.map.count);
  
406
	return true;
  
407
}
  
408
  
  
409
bool CheckMapCollision(Vector3 start, Vector3 end, RayCollision *outCollision) {
  
410
	Vector3 diff = Vector3Subtract(end, start);
  
411
	float maxDist = Vector3Length(diff);
  
412
	if (maxDist < 0.001f) return false;
  
413
  
  
414
	Ray ray = { 0 };
  
415
	ray.position = start;
  
416
	ray.direction = Vector3Scale(diff, 1.0f / maxDist);
  
417
  
  
418
	RayCollision closest = { 0 };
  
419
	closest.distance = FLT_MAX;
  
420
	closest.hit = false;
  
421
  
  
422
	for (int i = 0; i < game.map.count; i++) {
  
423
		Model model = game.map.models[i];
  
424
		for (int m = 0; m < model.meshCount; m++) {
  
425
			RayCollision col = GetRayCollisionMesh(ray, model.meshes[m], model.transform);
  
426
			if (col.hit && col.distance < closest.distance && col.distance <= maxDist) {
  
427
				closest = col;
  
428
			}
  
429
		}
  
430
	}
  
431
  
  
432
	if (closest.hit) {
  
433
		if (outCollision) *outCollision = closest;
  
434
		return true;
  
435
	}
  
436
	return false;
246
}
437
}
diff --git a/menu.c b/menu.c
  
1
#include "all.h"
  
2
  
  
3
void UpdateMenu(void) {
  
4
	if (IsKeyPressed(KEY_ENTER)) {
  
5
		if (LoadMap(game.map_path)) {
  
6
			game.mode = STATE_PLAYING;
  
7
			game.cursor_captured = true;
  
8
			DisableCursor();
  
9
		}
  
10
	}
  
11
}
  
12
  
  
13
void DrawMenu(void) {
  
14
	BeginDrawing();
  
15
	ClearBackground(BLACK);
  
16
  
  
17
	int screenWidth = GetScreenWidth();
  
18
	int screenHeight = GetScreenHeight();
  
19
  
  
20
	const char *title = "STALAG";
  
21
	const char *sub = "Press ENTER to Start";
  
22
  
  
23
	int titleSize = 60;
  
24
	int subSize = 20;
  
25
  
  
26
	Vector2 titlePos = { 
  
27
		(float)(screenWidth - MeasureTextEx(game.font_ui, title, (float)titleSize, 4).x) / 2, 
  
28
		(float)screenHeight / 2 - 40 
  
29
	};
  
30
	Vector2 subPos = { 
  
31
		(float)(screenWidth - MeasureTextEx(game.font_ui, sub, (float)subSize, 2).x) / 2, 
  
32
		(float)screenHeight / 2 + 40 
  
33
	};
  
34
  
  
35
	DrawTextEx(game.font_ui, title, titlePos, (float)titleSize, 4, WHITE);
  
36
	DrawTextEx(game.font_ui, sub, subPos, (float)subSize, 2, GRAY);
  
37
  
  
38
	EndDrawing();
  
39
}
diff --git a/player.c b/player.c
1
#include "all.h"
1
#include "all.h"
2
#include "raymath.h"
2
#include "raymath.h"
  
3
  
3
#include <float.h>
4
#include <float.h>
4
  
5
  
5
static void PlayerRotate(void) {
6
static void PlayerRotate(void) {
6
	if (!game.cursor_captured) return;
7
	if (!game.cursor_captured) return;
7
  
8
  
8
	Vector2 mouseDelta = GetMouseDelta();
9
	Vector2 mouseDelta = GetMouseDelta();
9
	game.yaw += -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY;
10
	game.player.yaw += -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY;
10
	game.pitch += mouseDelta.y * PLAYER_MOUSE_SENSITIVITY;
11
	game.player.pitch += mouseDelta.y * PLAYER_MOUSE_SENSITIVITY;
11
  
  
12
	// Clamp pitch to avoid gimbal lock/flipping (approx 89 degrees)
  
13
	if (game.pitch > 89.0f * DEG2RAD) game.pitch = 89.0f * DEG2RAD;
  
14
	if (game.pitch < -89.0f * DEG2RAD) game.pitch = -89.0f * DEG2RAD;
  
15
}
  
16
  
  
17
static bool CheckWorldCollision(Vector3 start, Vector3 end, RayCollision *outCollision) {
  
18
	Vector3 diff = Vector3Subtract(end, start);
  
19
	float maxDist = Vector3Length(diff);
  
20
	if (maxDist < 0.001f) return false;
  
21
  
  
22
	Ray ray = { 0 };
  
23
	ray.position = start;
  
24
	ray.direction = Vector3Scale(diff, 1.0f / maxDist);
  
25
  
  
26
	RayCollision closest = { 0 };
  
27
	closest.distance = FLT_MAX;
  
28
	closest.hit = false;
  
29
  
  
30
	for (int i = 0; i < game.world_model_count; i++) {
  
31
		Model model = game.world_models[i];
  
32
		for (int m = 0; m < model.meshCount; m++) {
  
33
			RayCollision col = GetRayCollisionMesh(ray, model.meshes[m], model.transform);
  
34
			if (col.hit && col.distance < closest.distance && col.distance <= maxDist) {
  
35
				closest = col;
  
36
			}
  
37
		}
  
38
	}
  
39
  
12
  
40
	if (closest.hit) {
13
	if (game.player.pitch > 89.0f * DEG2RAD) game.player.pitch = 89.0f * DEG2RAD;
41
		if (outCollision) *outCollision = closest;
14
	if (game.player.pitch < -89.0f * DEG2RAD) game.player.pitch = -89.0f * DEG2RAD;
42
		return true;
  
43
	}
  
44
	return false;
  
45
}
15
}
46
  
16
  
47
static void MoveNormal(float dt) {
17
static void MoveNormal(float dt) {
48
	float eyeHeight = PLAYER_EYE_HEIGHT - (game.crouch_amount * PLAYER_CROUCH_OFFSET);
18
	float eyeHeight = PLAYER_EYE_HEIGHT - (game.player.crouch_amount * PLAYER_CROUCH_OFFSET);
49
  
19
  
50
	// Movement vectors based on yaw
20
	Vector3 forward = { sinf(game.player.yaw), 0, cosf(game.player.yaw) };
51
	Vector3 forward = { sinf(game.yaw), 0, cosf(game.yaw) };
21
	Vector3 right = { sinf(game.player.yaw - PI/2.0f), 0, cosf(game.player.yaw - PI/2.0f) };
52
	Vector3 right = { sinf(game.yaw - PI/2.0f), 0, cosf(game.yaw - PI/2.0f) };
  
53
  
22
  
54
	Vector3 moveDir = { 0 };
23
	Vector3 moveDir = { 0 };
55
	if (IsKeyDown(KEY_W)) moveDir = Vector3Add(moveDir, forward);
24
	if (IsKeyDown(KEY_W)) moveDir = Vector3Add(moveDir, forward);
...
59
  
28
  
60
	float speed = PLAYER_MOVE_SPEED;
29
	float speed = PLAYER_MOVE_SPEED;
61
	if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) speed *= PLAYER_SPRINT_MULTIPLIER;
30
	if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) speed *= PLAYER_SPRINT_MULTIPLIER;
62
  
31
	speed *= Lerp(1.0f, PLAYER_CROUCH_MOVE_MULTIPLIER, game.player.crouch_amount);
63
	// Apply crouch speed penalty
  
64
	speed *= Lerp(1.0f, PLAYER_CROUCH_MOVE_MULTIPLIER, game.crouch_amount);
  
65
  
32
  
66
	if (Vector3Length(moveDir) > 0) {
33
	if (Vector3Length(moveDir) > 0) {
67
		moveDir = Vector3Scale(Vector3Normalize(moveDir), speed * dt);
34
		moveDir = Vector3Scale(Vector3Normalize(moveDir), speed * dt);
68
	}
35
	}
69
  
36
  
70
	// Apply gravity
37
	game.player.velocity.y -= PLAYER_GRAVITY * dt;
71
	game.velocity.y -= PLAYER_GRAVITY * dt;
  
72
  
38
  
73
	// Jump
39
	if (game.player.is_grounded && IsKeyPressed(KEY_SPACE)) {
74
	if (game.is_grounded && IsKeyPressed(KEY_SPACE)) {
40
		game.player.velocity.y = PLAYER_JUMP_FORCE;
75
		game.velocity.y = PLAYER_JUMP_FORCE;
41
		game.player.is_grounded = false;
76
		game.is_grounded = false;
  
77
	}
42
	}
78
  
43
  
79
	// Horizontal movement with sliding collision
  
80
	Vector3 remainingMove = moveDir;
44
	Vector3 remainingMove = moveDir;
81
	for (int iter = 0; iter < 4 && Vector3Length(remainingMove) > 0.001f; iter++) {
45
	for (int iter = 0; iter < 4 && Vector3Length(remainingMove) > 0.001f; iter++) {
82
		RayCollision closestHit = { 0 };
46
		RayCollision closestHit = { 0 };
...
84
		bool hitFound = false;
48
		bool hitFound = false;
85
  
49
  
86
		float heights[] = { -eyeHeight + 10.0f, -eyeHeight / 2.0f, 0.0f };
50
		float heights[] = { -eyeHeight + 10.0f, -eyeHeight / 2.0f, 0.0f };
87
		Vector3 currentPos = game.pos;
51
		Vector3 currentPos = game.player.pos;
88
  
52
  
89
		for (int i = 0; i < 3; i++) {
53
		for (int i = 0; i < 3; i++) {
90
			Vector3 start = Vector3Add(currentPos, (Vector3){0, heights[i], 0});
54
			Vector3 start = Vector3Add(currentPos, (Vector3){0, heights[i], 0});
...
93
			Vector3 end = Vector3Add(start, Vector3Scale(dir, dist));
57
			Vector3 end = Vector3Add(start, Vector3Scale(dir, dist));
94
  
58
  
95
			RayCollision col;
59
			RayCollision col;
96
			if (CheckWorldCollision(start, end, &col)) {
60
			if (CheckMapCollision(start, end, &col)) {
97
				// Adjust distance to be relative to the player boundary
  
98
				float adjustedDist = col.distance - PLAYER_RADIUS;
61
				float adjustedDist = col.distance - PLAYER_RADIUS;
99
				if (adjustedDist < closestHit.distance) {
62
				if (adjustedDist < closestHit.distance) {
100
					closestHit = col;
63
					closestHit = col;
...
105
		}
68
		}
106
  
69
  
107
		if (hitFound) {
70
		if (hitFound) {
108
			// Move as far as possible
  
109
			float moveDist = fmaxf(0, closestHit.distance - 0.1f);
71
			float moveDist = fmaxf(0, closestHit.distance - 0.1f);
110
			Vector3 moveStep = Vector3Scale(Vector3Normalize(remainingMove), moveDist);
72
			Vector3 moveStep = Vector3Scale(Vector3Normalize(remainingMove), moveDist);
111
			game.pos.x += moveStep.x;
73
			game.player.pos.x += moveStep.x;
112
			game.pos.z += moveStep.z;
74
			game.player.pos.z += moveStep.z;
113
  
75
  
114
			// Project remaining movement onto the plane of the wall
  
115
			Vector3 slideNormal = { closestHit.normal.x, 0, closestHit.normal.z };
76
			Vector3 slideNormal = { closestHit.normal.x, 0, closestHit.normal.z };
116
			if (Vector3Length(slideNormal) > 0.001f) {
77
			if (Vector3Length(slideNormal) > 0.001f) {
117
				slideNormal = Vector3Normalize(slideNormal);
78
				slideNormal = Vector3Normalize(slideNormal);
...
122
				remainingMove = (Vector3){0, 0, 0};
83
				remainingMove = (Vector3){0, 0, 0};
123
			}
84
			}
124
		} else {
85
		} else {
125
			// No collision found, move the rest of the way
86
			game.player.pos.x += remainingMove.x;
126
			game.pos.x += remainingMove.x;
87
			game.player.pos.z += remainingMove.z;
127
			game.pos.z += remainingMove.z;
  
128
			break;
88
			break;
129
		}
89
		}
130
	}
90
	}
131
  
91
  
132
	// Vertical movement with collision
92
	float verticalMove = game.player.velocity.y * dt;
133
	float verticalMove = game.velocity.y * dt;
93
	Vector3 vStart = game.player.pos;
134
	Vector3 vStart = game.pos;
  
135
  
94
  
136
	if (verticalMove < 0) { // Falling/Down
95
	if (verticalMove < 0) {
137
		Vector3 start = vStart;
96
		Vector3 start = vStart;
138
		// Check slightly below feet
  
139
		Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove - eyeHeight, 0});
97
		Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove - eyeHeight, 0});
140
		RayCollision vCol;
98
		RayCollision vCol;
141
		if (CheckWorldCollision(start, end, &vCol) && vCol.normal.y > 0.5f) {
99
		if (CheckMapCollision(start, end, &vCol) && vCol.normal.y > 0.5f) {
142
			game.pos.y = vCol.point.y + eyeHeight;
100
			game.player.pos.y = vCol.point.y + eyeHeight;
143
			game.velocity.y = 0;
101
			game.player.velocity.y = 0;
144
			game.is_grounded = true;
102
			game.player.is_grounded = true;
145
		} else {
103
		} else {
146
			game.pos.y += verticalMove;
104
			game.player.pos.y += verticalMove;
147
			game.is_grounded = false;
105
			game.player.is_grounded = false;
148
		}
106
		}
149
	} else if (verticalMove > 0) { // Jumping/Up
107
	} else if (verticalMove > 0) {
150
		Vector3 start = vStart;
108
		Vector3 start = vStart;
151
		// Check above head
  
152
		float headHeight = PLAYER_HEIGHT - PLAYER_EYE_HEIGHT;
109
		float headHeight = PLAYER_HEIGHT - PLAYER_EYE_HEIGHT;
153
		Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove + headHeight, 0});
110
		Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove + headHeight, 0});
154
		RayCollision vCol;
111
		RayCollision vCol;
155
		if (CheckWorldCollision(start, end, &vCol)) {
112
		if (CheckMapCollision(start, end, &vCol)) {
156
			game.pos.y = vCol.point.y - headHeight - 1.0f;
113
			game.player.pos.y = vCol.point.y - headHeight - 1.0f;
157
			game.velocity.y = 0;
114
			game.player.velocity.y = 0;
158
		} else {
115
		} else {
159
			game.pos.y += verticalMove;
116
			game.player.pos.y += verticalMove;
160
		}
117
		}
161
		game.is_grounded = false;
118
		game.player.is_grounded = false;
162
	} else {
119
	} else {
163
		// Not moving vertically, but check if we are still on ground
  
164
		Vector3 start = vStart;
120
		Vector3 start = vStart;
165
		Vector3 end = Vector3Add(vStart, (Vector3){0, -eyeHeight - 2.0f, 0});
121
		Vector3 end = Vector3Add(vStart, (Vector3){0, -eyeHeight - 2.0f, 0});
166
		RayCollision vCol;
122
		RayCollision vCol;
167
		if (CheckWorldCollision(start, end, &vCol) && vCol.normal.y > 0.5f) {
123
		if (CheckMapCollision(start, end, &vCol) && vCol.normal.y > 0.5f) {
168
			game.is_grounded = true;
124
			game.player.is_grounded = true;
169
			// Snap to floor if very close
  
170
			if (vCol.distance < eyeHeight + 1.0f) {
125
			if (vCol.distance < eyeHeight + 1.0f) {
171
				game.pos.y = vCol.point.y + eyeHeight;
126
				game.player.pos.y = vCol.point.y + eyeHeight;
172
			}
127
			}
173
		} else {
128
		} else {
174
			game.is_grounded = false;
129
			game.player.is_grounded = false;
175
		}
130
		}
176
	}
131
	}
177
}
132
}
178
  
133
  
179
static void MoveFly(float dt) {
134
static void MoveFly(float dt) {
180
	// Full 3D movement based on yaw/pitch
  
181
	Vector3 forward = { 
135
	Vector3 forward = { 
182
		cosf(game.pitch) * sinf(game.yaw), 
136
		cosf(game.player.pitch) * sinf(game.player.yaw), 
183
		-sinf(game.pitch), 
137
		-sinf(game.player.pitch), 
184
		cosf(game.pitch) * cosf(game.yaw) 
138
		cosf(game.player.pitch) * cosf(game.player.yaw) 
185
	};
139
	};
186
	Vector3 right = { sinf(game.yaw - PI/2.0f), 0, cosf(game.yaw - PI/2.0f) };
140
	Vector3 right = { sinf(game.player.yaw - PI/2.0f), 0, cosf(game.player.yaw - PI/2.0f) };
187
  
141
  
188
	Vector3 move = { 0 };
142
	Vector3 move = { 0 };
189
	if (IsKeyDown(KEY_W)) move = Vector3Add(move, forward);
143
	if (IsKeyDown(KEY_W)) move = Vector3Add(move, forward);
...
191
	if (IsKeyDown(KEY_D)) move = Vector3Add(move, right);
145
	if (IsKeyDown(KEY_D)) move = Vector3Add(move, right);
192
	if (IsKeyDown(KEY_A)) move = Vector3Subtract(move, right);
146
	if (IsKeyDown(KEY_A)) move = Vector3Subtract(move, right);
193
  
147
  
194
	// Fly up/down
  
195
	if (IsKeyDown(KEY_SPACE)) move.y += 1.0f;
148
	if (IsKeyDown(KEY_SPACE)) move.y += 1.0f;
196
	if (IsKeyDown(KEY_LEFT_CONTROL)) move.y -= 1.0f;
149
	if (IsKeyDown(KEY_LEFT_CONTROL)) move.y -= 1.0f;
197
  
150
  
...
200
  
153
  
201
	if (Vector3Length(move) > 0) {
154
	if (Vector3Length(move) > 0) {
202
		move = Vector3Scale(Vector3Normalize(move), speed * dt);
155
		move = Vector3Scale(Vector3Normalize(move), speed * dt);
203
		game.pos = Vector3Add(game.pos, move);
156
		game.player.pos = Vector3Add(game.player.pos, move);
204
	}
157
	}
205
  
158
  
206
	game.velocity = (Vector3){0, 0, 0};
159
	game.player.velocity = (Vector3){0, 0, 0};
207
	game.is_grounded = false;
160
	game.player.is_grounded = false;
208
}
161
}
209
  
162
  
210
void UpdatePlayer(void) {
163
void UpdatePlayer(void) {
211
	float dt = GetFrameTime();
164
	float dt = GetFrameTime();
212
	Vector3 oldPos = game.pos;
165
	Vector3 oldPos = game.player.pos;
213
  
166
  
214
	if (IsKeyPressed(KEY_F)) {
167
	if (IsKeyPressed(KEY_F)) {
215
		game.move_mode = (game.move_mode == MOVE_NORMAL) ? MOVE_FLY : MOVE_NORMAL;
168
		game.player.move_mode = (game.player.move_mode == MOVE_NORMAL) ? MOVE_FLY : MOVE_NORMAL;
216
		TraceLog(LOG_INFO, "Movement mode: %s", (game.move_mode == MOVE_NORMAL) ? "NORMAL" : "FLY");
  
217
	}
169
	}
218
  
170
  
219
	PlayerRotate();
171
	PlayerRotate();
220
  
172
  
221
	// Crouching logic
  
222
	bool canStand = true;
173
	bool canStand = true;
223
	if (game.crouch_amount > 0.1f) {
174
	if (game.player.crouch_amount > 0.1f) {
224
		Vector3 headPos = game.pos;
175
		Vector3 headPos = game.player.pos;
225
		Vector3 headEnd = Vector3Add(headPos, (Vector3){0, PLAYER_HEIGHT - PLAYER_EYE_HEIGHT + PLAYER_CROUCH_OFFSET, 0});
176
		Vector3 headEnd = Vector3Add(headPos, (Vector3){0, PLAYER_HEIGHT - PLAYER_EYE_HEIGHT + PLAYER_CROUCH_OFFSET, 0});
226
		RayCollision col;
177
		RayCollision col;
227
		if (CheckWorldCollision(headPos, headEnd, &col)) {
178
		if (CheckMapCollision(headPos, headEnd, &col)) {
228
			canStand = false;
179
			canStand = false;
229
		}
180
		}
230
	}
181
	}
231
  
182
  
232
	float targetCrouch = 0.0f;
183
	float targetCrouch = 0.0f;
233
	if (IsKeyDown(KEY_LEFT_CONTROL) || !canStand) targetCrouch = 1.0f;
184
	if (IsKeyDown(KEY_LEFT_CONTROL) || !canStand) targetCrouch = 1.0f;
234
	game.crouch_amount = Lerp(game.crouch_amount, targetCrouch, PLAYER_CROUCH_SPEED * dt);
185
	game.player.crouch_amount = Lerp(game.player.crouch_amount, targetCrouch, PLAYER_CROUCH_SPEED * dt);
235
  
186
  
236
	// Leaning logic
  
237
	float targetLean = 0.0f;
187
	float targetLean = 0.0f;
238
	if (IsKeyDown(KEY_Q)) targetLean -= 1.0f;
188
	if (IsKeyDown(KEY_Q)) targetLean -= 1.0f;
239
	if (IsKeyDown(KEY_E)) targetLean += 1.0f;
189
	if (IsKeyDown(KEY_E)) targetLean += 1.0f;
240
  
190
	game.player.lean_amount = Lerp(game.player.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt);
241
	game.lean_amount = Lerp(game.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt);
  
242
  
191
  
243
	if (game.move_mode == MOVE_FLY) {
192
	if (game.player.move_mode == MOVE_FLY) {
244
		MoveFly(dt);
193
		MoveFly(dt);
245
	} else {
194
	} else {
246
		MoveNormal(dt);
195
		MoveNormal(dt);
247
	}
196
	}
248
  
197
  
249
	// Apply lean as a pivot from the neck/waist
198
	float leanAngle = game.player.lean_amount * PLAYER_LEAN_ANGLE * DEG2RAD;
250
	float leanAngle = game.lean_amount * PLAYER_LEAN_ANGLE * DEG2RAD;
199
	Vector3 bodyForward = { sinf(game.player.yaw), 0, cosf(game.player.yaw) };
251
	Vector3 bodyForward = { sinf(game.yaw), 0, cosf(game.yaw) };
  
252
  
200
  
253
	float currentEyeHeight = PLAYER_EYE_HEIGHT - (game.crouch_amount * PLAYER_CROUCH_OFFSET);
201
	float currentEyeHeight = PLAYER_EYE_HEIGHT - (game.player.crouch_amount * PLAYER_CROUCH_OFFSET);
254
	float pivotDist = fminf(PLAYER_LEAN_PIVOT_DISTANCE, currentEyeHeight * 0.8f);
202
	float pivotDist = fminf(PLAYER_LEAN_PIVOT_DISTANCE, currentEyeHeight * 0.8f);
255
  
203
  
256
	Vector3 neckToEye = { 0, pivotDist, 0 };
204
	Vector3 neckToEye = { 0, pivotDist, 0 };
257
	Vector3 rotatedOffset = Vector3RotateByAxisAngle(neckToEye, bodyForward, leanAngle);
205
	Vector3 rotatedOffset = Vector3RotateByAxisAngle(neckToEye, bodyForward, leanAngle);
258
	Vector3 pivot = Vector3Subtract(game.pos, (Vector3){ 0, pivotDist, 0 });
206
	Vector3 pivot = Vector3Subtract(game.player.pos, (Vector3){ 0, pivotDist, 0 });
259
  
207
  
260
	// Update camera based on physical pos + visual lean pivot
  
261
	game.camera.position = Vector3Add(pivot, rotatedOffset);
208
	game.camera.position = Vector3Add(pivot, rotatedOffset);
262
  
209
  
263
	// Apply roll to up vector
  
264
	Vector3 forward = {
210
	Vector3 forward = {
265
		cosf(game.pitch) * sinf(game.yaw),
211
		cosf(game.player.pitch) * sinf(game.player.yaw),
266
		-sinf(game.pitch),
212
		-sinf(game.player.pitch),
267
		cosf(game.pitch) * cosf(game.yaw)
213
		cosf(game.player.pitch) * cosf(game.player.yaw)
268
	};
214
	};
269
  
215
  
270
	// Use a stable world-up vector and apply lean roll
  
271
	Vector3 up = { 0, 1, 0 };
216
	Vector3 up = { 0, 1, 0 };
272
	up = Vector3RotateByAxisAngle(up, forward, leanAngle);
217
	up = Vector3RotateByAxisAngle(up, forward, leanAngle);
273
	game.camera.up = up;
218
	game.camera.up = up;
274
  
  
275
	// Use a longer target distance for better precision in the projection matrix
  
276
	game.camera.target = Vector3Add(game.camera.position, Vector3Scale(forward, 20.0f));
219
	game.camera.target = Vector3Add(game.camera.position, Vector3Scale(forward, 20.0f));
277
  
220
  
278
	// Calculate horizontal speed for UI
  
279
	if (dt > 0) {
221
	if (dt > 0) {
280
		Vector2 velH = { game.pos.x - oldPos.x, game.pos.z - oldPos.z };
222
		Vector2 velH = { game.player.pos.x - oldPos.x, game.player.pos.z - oldPos.z };
281
		game.horizontal_speed = Vector2Length(velH) / dt;
223
		game.player.horizontal_speed = Vector2Length(velH) / dt;
282
	} else {
224
	} else {
283
		game.horizontal_speed = 0;
225
		game.player.horizontal_speed = 0;
284
	}
226
	}
285
}
227
}