summaryrefslogtreecommitdiff
path: root/game.c
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-04-28 07:50:31 +0200
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-04-28 09:23:47 +0200
commit63eb46698b1e19d3f36944992b948c54a7a3740b (patch)
tree1aa8b686039e2f9291d680c21a47427de5daa12b /game.c
parent0ed91795a2db720e688fd2daefd22f7e9c754c2f (diff)
downloadstalag-63eb46698b1e19d3f36944992b948c54a7a3740b.tar.gz
Compiler settings and macOS port
Diffstat (limited to 'game.c')
-rw-r--r--game.c306
1 files changed, 306 insertions, 0 deletions
diff --git a/game.c b/game.c
new file mode 100644
index 0000000..db04ff7
--- /dev/null
+++ b/game.c
@@ -0,0 +1,306 @@
1#include "all.h"
2#include <stdio.h>
3#include <string.h>
4#include <ctype.h>
5
6GameState 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 = 75.0f;
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.camera.position = (Vector3){ x, z, -y };
116 game.camera.target = Vector3Add(game.camera.position, (Vector3){0, 0, 1});
117 TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f", game.camera.position.x, game.camera.position.y, game.camera.position.z);
118 }
119 }
120 }
121
122 if (is_world) {
123 total_brushes += e->brush_count;
124 for (int j = 0; j < e->brush_count; j++) {
125 MapBrush *brush = &e->brushes[j];
126 for (int p_idx = 0; p_idx < brush->plane_count; p_idx++) {
127 MapPlane *mp = &brush->planes[p_idx];
128 Plane plane = PlaneFromPoints(mp->p[0], mp->p[1], mp->p[2]);
129
130 if (Vector3Length(plane.normal) < 0.5f) continue;
131
132 Polygon poly = CreateLargeQuad(plane);
133 for (int k = 0; k < brush->plane_count; k++) {
134 if (p_idx == k) continue;
135 Plane clipPlane = PlaneFromPoints(brush->planes[k].p[0], brush->planes[k].p[1], brush->planes[k].p[2]);
136 if (Vector3Length(clipPlane.normal) < 0.5f) continue;
137
138 Polygon next = PolyClip(poly, clipPlane);
139 PolyFree(poly);
140 poly = next;
141 }
142 if (poly.count >= 3) {
143 TextureGroup *group = NULL;
144 for (int g = 0; g < groups.length; g++) {
145 if (strcmp(groups.data[g].texture, mp->texture) == 0) {
146 group = &groups.data[g];
147 break;
148 }
149 }
150 if (!group) {
151 TextureGroup new_group = { 0 };
152 strncpy(new_group.texture, mp->texture, sizeof(new_group.texture));
153 array_init(new_group.vertices);
154 array_init(new_group.texcoords);
155 array_init(new_group.normals);
156 array_push(groups, new_group);
157 group = &groups.data[groups.length - 1];
158 }
159 for (int v = 1; v < poly.count - 1; v++) {
160 Vector3 v0 = poly.verts[0];
161 Vector3 v1 = poly.verts[v+1];
162 Vector3 v2 = poly.verts[v];
163 array_push(group->vertices, v0.x); array_push(group->vertices, v0.y); array_push(group->vertices, v0.z);
164 array_push(group->vertices, v1.x); array_push(group->vertices, v1.y); array_push(group->vertices, v1.z);
165 array_push(group->vertices, v2.x); array_push(group->vertices, v2.y); array_push(group->vertices, v2.z);
166 Vector2 uv0 = GetUV(v0, mp, plane);
167 Vector2 uv1 = GetUV(v1, mp, plane);
168 Vector2 uv2 = GetUV(v2, mp, plane);
169 array_push(group->texcoords, uv0.x); array_push(group->texcoords, uv0.y);
170 array_push(group->texcoords, uv1.x); array_push(group->texcoords, uv1.y);
171 array_push(group->texcoords, uv2.x); array_push(group->texcoords, uv2.y);
172 Vector3 out_normal = Vector3Scale(plane.normal, -1.0f);
173 array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
174 array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
175 array_push(group->normals, out_normal.x); array_push(group->normals, out_normal.y); array_push(group->normals, out_normal.z);
176 }
177 }
178 PolyFree(poly);
179 }
180 }
181 }
182 }
183
184 array(Model) final_models;
185 array_init(final_models);
186
187 for (int i = 0; i < groups.length; i++) {
188 TextureGroup *g = &groups.data[i];
189 if (g->vertices.length == 0) continue;
190 Mesh mesh = { 0 };
191 mesh.vertexCount = (int)g->vertices.length / 3;
192 mesh.triangleCount = mesh.vertexCount / 3;
193 mesh.vertices = (float *)malloc(g->vertices.length * sizeof(float));
194 memcpy(mesh.vertices, g->vertices.data, g->vertices.length * sizeof(float));
195 mesh.texcoords = (float *)malloc(g->texcoords.length * sizeof(float));
196 memcpy(mesh.texcoords, g->texcoords.data, g->texcoords.length * sizeof(float));
197 mesh.normals = (float *)malloc(g->normals.length * sizeof(float));
198 memcpy(mesh.normals, g->normals.data, g->normals.length * sizeof(float));
199 UploadMesh(&mesh, false);
200 Model model = LoadModelFromMesh(mesh);
201 model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = GetTexture(g->texture);
202 array_push(final_models, model);
203 array_free(g->vertices);
204 array_free(g->texcoords);
205 array_free(g->normals);
206 }
207
208 game.world_models = final_models.data;
209 game.world_model_count = (int)final_models.length;
210 array_free(groups);
211
212 FreeMap(map);
213 TraceLog(LOG_INFO, "Processed %d brushes into %d models", total_brushes, game.world_model_count);
214 return true;
215}
216
217void InitGame(void) {
218 array_init(texture_cache);
219 game.world_models = NULL;
220 game.world_model_count = 0;
221
222 LoadMap("maps/demo1.map");
223
224 game.cursor_captured = true;
225 DisableCursor();
226}
227
228void UpdateGame(void) {
229 float dt = GetFrameTime();
230
231 if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
232 game.cursor_captured = !game.cursor_captured;
233 if (game.cursor_captured) DisableCursor();
234 else EnableCursor();
235 }
236
237 if (IsKeyPressed(KEY_ONE)) LoadMap("maps/demo1.map");
238 if (IsKeyPressed(KEY_TWO)) LoadMap("maps/demo2.map");
239
240 if (IsKeyPressed(KEY_V)) {
241 game.vsync = !game.vsync;
242 if (game.vsync) {
243 SetWindowState(FLAG_VSYNC_HINT);
244 SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
245 } else {
246 ClearWindowState(FLAG_VSYNC_HINT);
247 SetTargetFPS(0);
248 }
249 }
250
251 // Manual first-person movement
252 Vector3 forward = Vector3Normalize(Vector3Subtract(game.camera.target, game.camera.position));
253 Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, game.camera.up));
254
255 Vector3 move = { 0 };
256 if (IsKeyDown(KEY_W)) move = Vector3Add(move, forward);
257 if (IsKeyDown(KEY_S)) move = Vector3Subtract(move, forward);
258 if (IsKeyDown(KEY_D)) move = Vector3Add(move, right);
259 if (IsKeyDown(KEY_A)) move = Vector3Subtract(move, right);
260
261 if (Vector3Length(move) > 0) {
262 move = Vector3Scale(Vector3Normalize(move), PLAYER_MOVE_SPEED * dt);
263 game.camera.position = Vector3Add(game.camera.position, move);
264 game.camera.target = Vector3Add(game.camera.target, move);
265 }
266
267 if (game.cursor_captured) {
268 // Manual rotation
269 Vector2 mouseDelta = GetMouseDelta();
270 float yaw = -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY;
271 float pitch = -mouseDelta.y * PLAYER_MOUSE_SENSITIVITY;
272
273 Vector3 view = Vector3Subtract(game.camera.target, game.camera.position);
274
275 // Yaw
276 view = Vector3RotateByAxisAngle(view, game.camera.up, yaw);
277
278 // Pitch
279 Vector3 axis = Vector3Normalize(Vector3CrossProduct(view, game.camera.up));
280 view = Vector3RotateByAxisAngle(view, axis, pitch);
281
282 game.camera.target = Vector3Add(game.camera.position, view);
283 }
284}
285
286void DrawGame(void) {
287 BeginDrawing();
288 ClearBackground(DARKGRAY);
289 BeginMode3D(game.camera);
290
291 // Enable backface culling to hide interior faces of brushes
292 rlEnableBackfaceCulling();
293 for (int i = 0; i < game.world_model_count; i++) {
294 DrawModel(game.world_models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE);
295 }
296
297 EndMode3D();
298
299 int screenWidth = GetScreenWidth();
300 int screenHeight = GetScreenHeight();
301 DrawLine(screenWidth / 2 - 10, screenHeight / 2, screenWidth / 2 + 10, screenHeight / 2, GREEN);
302 DrawLine(screenWidth / 2, screenHeight / 2 - 10, screenWidth / 2, screenHeight / 2 + 10, GREEN);
303 DrawFPS(10, 10);
304 DrawText(TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), 10, 30, 20, GREEN);
305 EndDrawing();
306}