aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-04-30 19:19:27 +0200
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-04-30 19:44:07 +0200
commitb4d0ad9e95226d225d5361b1182866884aaa6366 (patch)
tree5440b34d97e89a08fc01abd49bb80e5b50dda945
parentddde2e8fdf05efac5914ca2b812b7762b78ba9ec (diff)
downloadstalag-b4d0ad9e95226d225d5361b1182866884aaa6366.tar.gz
Structural refactor
-rw-r--r--Makefile2
-rw-r--r--all.h161
-rw-r--r--assets.c81
-rw-r--r--config.h5
-rw-r--r--game.c359
-rw-r--r--interface.c6
-rw-r--r--main.c143
-rw-r--r--map.c597
-rw-r--r--menu.c39
-rw-r--r--player.c184
10 files changed, 809 insertions, 768 deletions
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
16GAME := bin/stalag 16GAME := bin/stalag
17HEXDUMP := bin/hexdump 17HEXDUMP := bin/hexdump
18PACKER := bin/packer 18PACKER := bin/packer
19SOURCES := main.c map.c game.c player.c interface.c 19SOURCES := main.c map.c game.c player.c interface.c assets.c menu.c
20 20
21ifeq ($(SYSTEM), linux_amd64) 21ifeq ($(SYSTEM), linux_amd64)
22 LDFLAGS += -lX11 22 LDFLAGS += -lX11
diff --git a/all.h b/all.h
index 59bdd32..26ccffb 100644
--- a/all.h
+++ b/all.h
@@ -15,134 +15,143 @@
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
20typedef struct { 22typedef 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
28typedef struct { 30typedef struct {
29 MapPlane *planes; 31 MapPlane *planes;
30 int plane_count; 32 int plane_count;
31} MapBrush; 33} MapBrush;
32 34
33typedef struct { 35typedef struct {
34 char key[64]; 36 char key[64];
35 char value[256]; 37 char value[256];
36} MapProperty; 38} MapProperty;
37 39
38typedef struct { 40typedef 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
45typedef struct { 47typedef struct {
46 MapEntity *entities; 48 MapEntity *entities;
47 int entity_count; 49 int entity_count;
48} Map; 50} Map;
49 51
50typedef struct { 52typedef 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
56typedef struct { 58typedef struct {
57 Vector3 normal; 59 Vector3 normal;
58 float dist; 60 float dist;
59} Plane; 61} Plane;
60 62
61typedef struct { 63typedef struct {
62 Vector3 *verts; 64 Vector3 *verts;
63 int count; 65 int count;
64} Polygon; 66} Polygon;
65 67
66typedef struct { 68typedef 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
73typedef struct { 75typedef 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
81Texture2D GetTexture(const char *name);
82void UnloadAssets(void);
83Font LoadFontVFS(const char *path, int fontSize);
84
85// Map Module
86typedef struct {
87 Model *models;
88 int count;
89} MapState;
90
91bool LoadMap(const char *filename);
92void UnloadMap(void);
93bool CheckMapCollision(Vector3 start, Vector3 end, RayCollision *outCollision);
94Map ParseMap(const char *filename);
95void FreeMap(Map map);
96
97// Menu Module
98void UpdateMenu(void);
99void DrawMenu(void);
79 100
101// Player Module
80typedef enum { 102typedef enum {
81 MOVE_NORMAL, 103 MOVE_NORMAL,
82 MOVE_FLY 104 MOVE_FLY
83} MovementMode; 105} MovementMode;
84 106
107typedef 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
119void UpdatePlayer(void);
120
121// Game State Module
85typedef enum { 122typedef enum {
86 STATE_TITLE, 123 STATE_MENU,
87 STATE_PLAYING 124 STATE_PLAYING
88} GameStateMode; 125} GameStateMode;
89 126
90typedef struct { 127typedef 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; 134
98 Font font_ui; 135 PlayerState player;
99 136 MapState map;
100 MovementMode move_mode;
101 Vector3 pos;
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
111extern GameState game; 139extern GameState game;
112 140
113// --- Prototypes --- 141void InitGame(void);
114 142void SetMap(const char *path);
115// Map 143void UpdateGame(void);
116char map_peek(MapParser *p); 144void DrawGame(void);
117char map_get(MapParser *p);
118void map_skip_whitespace(MapParser *p);
119bool map_expect(MapParser *p, char expected);
120void map_parse_token(MapParser *p, char *buffer, int size);
121Vector3 map_parse_vector(MapParser *p);
122Map ParseMap(const char *filename);
123void FreeMap(Map map);
124 145
125// Geometry 146// Geometry Helpers
126Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3); 147Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3);
127void PolyFree(Polygon p); 148void PolyFree(Polygon p);
128Polygon PolyClip(Polygon poly, Plane plane); 149Polygon PolyClip(Polygon poly, Plane plane);
129Polygon CreateLargeQuad(Plane plane); 150Polygon CreateLargeQuad(Plane plane);
130Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane); 151Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane);
131 152
132// Game
133Texture2D GetTexture(const char *name);
134void InitGame(void);
135void SetMap(const char *path);
136void UpdateGame(void);
137void DrawGame(void);
138bool LoadMap(const char *filename);
139void UnloadMap(void);
140
141// Interface 153// Interface
142void DrawCrosshair(void); 154void DrawCrosshair(void);
143void DrawDebugInfo(void); 155void DrawDebugInfo(void);
144 156
145// Player
146void UpdatePlayer(void);
147
148#endif 157#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 @@
1#include "all.h"
2
3#include <stdio.h>
4#include <string.h>
5#include <ctype.h>
6
7static array(CachedTexture) texture_cache;
8
9Texture2D 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
65void 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
72Font 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
index 7013074..9536ddd 100644
--- a/config.h
+++ b/config.h
@@ -5,6 +5,11 @@
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
index 67767d3..40d1876 100644
--- a/game.c
+++ b/game.c
@@ -1,332 +1,69 @@
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
6GameState game; 5GameState game;
7static array(CachedTexture) texture_cache;
8
9Texture2D 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
64void 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
80bool 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
226void InitGame(void) { 7void InitGame(void) {
227 array_init(texture_cache); 8 memset(&game, 0, sizeof(game));
228 game.world_models = NULL; 9
229 game.world_model_count = 0; 10 game.font_ui = LoadFontVFS(UI_FONT_PATH, UI_FONT_SIZE);
230 11 game.mode = STATE_MENU;
231 // Load UI Font 12 game.cursor_captured = false;
232 size_t font_size = 0; 13 EnableCursor();
233 void *font_data = vfs_read("fonts/LiberationSans-Bold.ttf", &font_size); 14
234 if (font_data) { 15 game.player.move_mode = MOVE_NORMAL;
235 game.font_ui = LoadFontFromMemory(".ttf", font_data, (int)font_size, 20, NULL, 0); 16 game.player.pos = (Vector3){ 0, 0, 0 };
236 vfs_free(font_data); 17 game.player.velocity = (Vector3){ 0, 0, 0 };
237 } else { 18 game.player.is_grounded = false;
238 game.font_ui = GetFontDefault(); 19
239 } 20 // Camera setup
240 21 game.camera.up = (Vector3){ 0, 1, 0 };
241 game.mode = STATE_TITLE; 22 game.camera.fovy = PLAYER_FOV;
242 23 game.camera.projection = CAMERA_PERSPECTIVE;
243 game.cursor_captured = false;
244 EnableCursor();
245
246 game.move_mode = MOVE_NORMAL;
247 game.velocity = (Vector3){ 0, 0, 0 };
248 game.is_grounded = false;
249} 24}
250 25
251void SetMap(const char *path) { 26void 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
256void UpdateGame(void) { 31void 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 } 37
263 return; 38 if (IsKeyPressed(KEY_V)) {
264 } 39 game.vsync = !game.vsync;
265 40 if (game.vsync) {
266 if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { 41 SetWindowState(FLAG_VSYNC_HINT);
267 game.cursor_captured = !game.cursor_captured; 42 SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
268 if (game.cursor_captured) DisableCursor(); 43 } else {
269 else EnableCursor(); 44 ClearWindowState(FLAG_VSYNC_HINT);
270 } 45 SetTargetFPS(0);
271 46 }
272 if (IsKeyPressed(KEY_ONE)) LoadMap("maps/demo1.map"); 47 }
273 if (IsKeyPressed(KEY_TWO)) LoadMap("maps/demo2.map"); 48
274 if (IsKeyPressed(KEY_THREE)) LoadMap("maps/demo3.map"); 49 UpdatePlayer();
275
276 if (IsKeyPressed(KEY_V)) {
277 game.vsync = !game.vsync;
278 if (game.vsync) {
279 SetWindowState(FLAG_VSYNC_HINT);
280 SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
281 } else {
282 ClearWindowState(FLAG_VSYNC_HINT);
283 SetTargetFPS(0);
284 }
285 }
286
287 UpdatePlayer();
288} 50}
289 51
290void DrawGame(void) { 52void 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
index d2cb4e0..58d8721 100644
--- a/interface.c
+++ b/interface.c
@@ -8,7 +8,7 @@ void DrawCrosshair(void) {
8} 8}
9 9
10void DrawDebugInfo(void) { 10void 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
index e497e73..9f32ecc 100644
--- a/main.c
+++ b/main.c
@@ -1,60 +1,97 @@
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
8int main(int argc, char *argv[]) { 7int 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);
11 10 sb_append_cstr(&map_path, "maps/demo3.map");
12 static struct option long_options[] = { 11
13 {"map", required_argument, 0, 'm'}, 12 bool skip_title = false;
14 {0, 0, 0, 0} 13
15 }; 14 static struct option long_options[] = {
16 15 {"map", required_argument, 0, 'm'},
17 int opt; 16 {0, 0, 0, 0}
18 int option_index = 0; 17 };
19 while ((opt = getopt_long_only(argc, argv, "m:", long_options, &option_index)) != -1) { 18
20 switch (opt) { 19 int opt;
21 case 'm': 20 int option_index = 0;
22 strncpy(map_path, optarg, sizeof(map_path) - 1); 21 while ((opt = getopt_long_only(argc, argv, "m:", long_options, &option_index)) != -1) {
23 skip_title = true; 22 switch (opt) {
24 break; 23 case 'm':
25 } 24 sb_free(&map_path);
26 } 25 sb_init(&map_path, 256);
27 26 sb_append_cstr(&map_path, optarg);
28 SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI); 27 skip_title = true;
29 InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); 28 break;
30 29 }
31 int monitor = GetCurrentMonitor(); 30 }
32 SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, 31
33 (GetMonitorHeight(monitor) - GetScreenHeight()) / 2); 32 SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI);
34 33 InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE);
35 SetTargetFPS(GetMonitorRefreshRate(monitor)); 34
36 35 int monitor = GetCurrentMonitor();
37 vfs_init("data.pak"); 36 SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, (GetMonitorHeight(monitor) - GetScreenHeight()) / 2);
38 InitGame(); 37 SetTargetFPS(GetMonitorRefreshRate(monitor));
39 SetMap(map_path); 38
40 39 vfs_init(VFS_DATA_PAK);
41 if (skip_title) { 40 InitGame();
42 if (LoadMap(game.map_path)) { 41 SetMap(map_path.data);
43 game.mode = STATE_PLAYING; 42
44 game.cursor_captured = true; 43 if (skip_title) {
45 DisableCursor(); 44 if (LoadMap(game.map_path)) {
46 } 45 game.mode = STATE_PLAYING;
47 } 46 game.cursor_captured = true;
48 47 DisableCursor();
49 game.vsync = true; 48 }
50 49 }
51 while (!WindowShouldClose()) { 50
52 UpdateGame(); 51 game.vsync = true;
53 DrawGame(); 52
54 } 53 while (!WindowShouldClose()) {
55 54 // Global Map Switching (Debug/Demo)
56 vfs_shutdown(); 55 if (IsKeyPressed(KEY_ONE)) {
57 CloseWindow(); 56 if (LoadMap("maps/demo1.map")) {
58 57 game.mode = STATE_PLAYING;
59 return 0; 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 }
76
77 // Game State Switcher
78 switch (game.mode) {
79 case STATE_MENU:
80 UpdateMenu();
81 DrawMenu();
82 break;
83 case STATE_PLAYING:
84 UpdateGame();
85 DrawGame();
86 break;
87 }
88 }
89
90 UnloadMap();
91 UnloadAssets();
92 vfs_shutdown();
93 CloseWindow();
94 sb_free(&map_path);
95
96 return 0;
60} 97}
diff --git a/map.c b/map.c
index 12d40f9..4e2b83c 100644
--- a/map.c
+++ b/map.c
@@ -1,246 +1,437 @@
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
6char map_peek(MapParser *p) { 9char 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
11char map_get(MapParser *p) { 14char 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
16void map_skip_whitespace(MapParser *p) { 19void 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
29bool map_expect(MapParser *p, char expected) { 32bool 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
35void map_parse_token(MapParser *p, char *buffer, int size) { 38void 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
61Vector3 map_parse_vector(MapParser *p) { 64Vector3 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
74Map ParseMap(const char *filename) { 77Map 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;
79 82
80 MapParser p = { data, size, 0 }; 83 MapParser p = { data, size, 0 };
81 array(MapEntity) entities; 84 array(MapEntity) entities;
82 array_init(entities); 85 array_init(entities);
83 86
84 while (true) { 87 while (true) {
85 map_skip_whitespace(&p); 88 map_skip_whitespace(&p);
86 if (map_peek(&p) == 0) break; 89 if (map_peek(&p) == 0) break;
87 90
88 if (map_expect(&p, '{')) { 91 if (map_expect(&p, '{')) {
89 MapEntity entity = { 0 }; 92 MapEntity entity = { 0 };
90 array(MapProperty) props; 93 array(MapProperty) props;
91 array(MapBrush) brushes; 94 array(MapBrush) brushes;
92 array_init(props); 95 array_init(props);
93 array_init(brushes); 96 array_init(brushes);
94 97
95 while (true) { 98 while (true) {
96 map_skip_whitespace(&p); 99 map_skip_whitespace(&p);
97 char c = map_peek(&p); 100 char c = map_peek(&p);
98 if (c == '}') { 101 if (c == '}') {
99 map_get(&p); 102 map_get(&p);
100 break; 103 break;
101 } else if (c == '"') { 104 } else if (c == '"') {
102 MapProperty prop; 105 MapProperty prop;
103 map_parse_token(&p, prop.key, sizeof(prop.key)); 106 map_parse_token(&p, prop.key, sizeof(prop.key));
104 map_parse_token(&p, prop.value, sizeof(prop.value)); 107 map_parse_token(&p, prop.value, sizeof(prop.value));
105 array_push(props, prop); 108 array_push(props, prop);
106 } else if (c == '{') { 109 } else if (c == '{') {
107 map_get(&p); 110 map_get(&p);
108 MapBrush brush = { 0 }; 111 MapBrush brush = { 0 };
109 array(MapPlane) planes; 112 array(MapPlane) planes;
110 array_init(planes); 113 array_init(planes);
111 while (true) { 114 while (true) {
112 map_skip_whitespace(&p); 115 map_skip_whitespace(&p);
113 if (map_peek(&p) == '}') { 116 if (map_peek(&p) == '}') {
114 map_get(&p); 117 map_get(&p);
115 break; 118 break;
116 } 119 }
117 MapPlane plane; 120 MapPlane plane;
118 plane.p[0] = map_parse_vector(&p); 121 plane.p[0] = map_parse_vector(&p);
119 plane.p[1] = map_parse_vector(&p); 122 plane.p[1] = map_parse_vector(&p);
120 plane.p[2] = map_parse_vector(&p); 123 plane.p[2] = map_parse_vector(&p);
121 map_parse_token(&p, plane.texture, sizeof(plane.texture)); 124 map_parse_token(&p, plane.texture, sizeof(plane.texture));
122 125
123 char buf[64]; 126 char buf[64];
124 map_parse_token(&p, buf, sizeof(buf)); plane.shift[0] = (float)atof(buf); 127 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); 128 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); 129 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); 130 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); 131 map_parse_token(&p, buf, sizeof(buf)); plane.scale[1] = (float)atof(buf);
129 132
130 array_push(planes, plane); 133 array_push(planes, plane);
131 } 134 }
132 brush.planes = planes.data; 135 brush.planes = planes.data;
133 brush.plane_count = (int)planes.length; 136 brush.plane_count = (int)planes.length;
134 array_push(brushes, brush); 137 array_push(brushes, brush);
135 } else { 138 } else {
136 map_get(&p); 139 map_get(&p);
137 } 140 }
138 } 141 }
139 entity.properties = props.data; 142 entity.properties = props.data;
140 entity.property_count = (int)props.length; 143 entity.property_count = (int)props.length;
141 entity.brushes = brushes.data; 144 entity.brushes = brushes.data;
142 entity.brush_count = (int)brushes.length; 145 entity.brush_count = (int)brushes.length;
143 array_push(entities, entity); 146 array_push(entities, entity);
144 } 147 }
145 } 148 }
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
153void FreeMap(Map map) { 156void 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
165Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3) { 168Plane 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
172void PolyFree(Polygon p) { 175void PolyFree(Polygon p) {
173 if (p.verts) free(p.verts); 176 if (p.verts) free(p.verts);
174} 177}
175 178
176Polygon PolyClip(Polygon poly, Plane plane) { 179Polygon 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; 193
191 194 if (d1 >= -eps) {
192 if (d1 >= -eps) { 195 if (d2 >= -eps) {
193 if (d2 >= -eps) { 196 array_push(out_verts, p2);
194 array_push(out_verts, p2); 197 } else {
195 } else { 198 float t = d1 / (d1 - d2);
196 float t = d1 / (d1 - d2); 199 Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t));
197 Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t)); 200 array_push(out_verts, intersect);
198 array_push(out_verts, intersect); 201 }
199 } 202 } else {
200 } else { 203 if (d2 >= -eps) {
201 if (d2 >= -eps) { 204 float t = d1 / (d1 - d2);
202 float t = d1 / (d1 - d2); 205 Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t));
203 Vector3 intersect = Vector3Add(p1, Vector3Scale(Vector3Subtract(p2, p1), t)); 206 array_push(out_verts, intersect);
204 array_push(out_verts, intersect); 207 array_push(out_verts, p2);
205 array_push(out_verts, p2); 208 }
206 } 209 }
207 } 210 }
208 } 211
209 212 Polygon res;
210 Polygon res; 213 res.verts = out_verts.data;
211 res.verts = out_verts.data; 214 res.count = (int)out_verts.length;
212 res.count = (int)out_verts.length; 215 return res;
213 return res;
214} 216}
215 217
216Polygon CreateLargeQuad(Plane plane) { 218Polygon 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
236Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane) { 238Vector2 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
250void 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
261bool 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
409bool 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
new file mode 100644
index 0000000..c1db1f7
--- /dev/null
+++ b/menu.c
@@ -0,0 +1,39 @@
1#include "all.h"
2
3void 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
13void 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
index 01c56ca..52788f1 100644
--- a/player.c
+++ b/player.c
@@ -1,55 +1,24 @@
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
5static void PlayerRotate(void) { 6static 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
12 // Clamp pitch to avoid gimbal lock/flipping (approx 89 degrees) 13 if (game.player.pitch > 89.0f * DEG2RAD) game.player.pitch = 89.0f * DEG2RAD;
13 if (game.pitch > 89.0f * DEG2RAD) game.pitch = 89.0f * DEG2RAD; 14 if (game.player.pitch < -89.0f * DEG2RAD) game.player.pitch = -89.0f * DEG2RAD;
14 if (game.pitch < -89.0f * DEG2RAD) game.pitch = -89.0f * DEG2RAD;
15}
16
17static 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
40 if (closest.hit) {
41 if (outCollision) *outCollision = closest;
42 return true;
43 }
44 return false;
45} 15}
46 16
47static void MoveNormal(float dt) { 17static 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,24 +28,19 @@ static void MoveNormal(float dt) {
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,7 +48,7 @@ static void MoveNormal(float dt) {
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,8 +57,7 @@ static void MoveNormal(float dt) {
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,13 +68,11 @@ static void MoveNormal(float dt) {
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,68 +83,61 @@ static void MoveNormal(float dt) {
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
179static void MoveFly(float dt) { 134static 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,7 +145,6 @@ static void MoveFly(float dt) {
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,86 +153,75 @@ static void MoveFly(float dt) {
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
210void UpdatePlayer(void) { 163void 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;
190 game.player.lean_amount = Lerp(game.player.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt);
240 191
241 game.lean_amount = Lerp(game.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt); 192 if (game.player.move_mode == MOVE_FLY) {
242
243 if (game.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}