aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--README.md13
-rw-r--r--all.h161
-rw-r--r--assets.c81
-rw-r--r--config.h5
-rw-r--r--game.c333
-rw-r--r--interface.c14
-rw-r--r--main.c106
-rw-r--r--map.c597
-rw-r--r--maps/demo1.map4
-rw-r--r--maps/demo3.map234
-rw-r--r--menu.c39
-rw-r--r--player.c184
-rwxr-xr-xtbrun.sh38
-rw-r--r--trenchbroom/stalag/Entities.fgd29
-rw-r--r--trenchbroom/stalag/GameConfig.cfg25
-rw-r--r--trenchbroom/stalag/GameEngineProfiles.cfg10
17 files changed, 1178 insertions, 697 deletions
diff --git a/Makefile b/Makefile
index 0eee797..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 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/README.md b/README.md
new file mode 100644
index 0000000..802e567
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
1## Trenchbroom setup
2
3```sh
4ln -s ~/Projects/stalag/trenchbroom/stalag/ ~/.TrenchBroom/games/
5```
6
7- Check that `path` in `GameEngineProfiles.cfg` points to game executable.
8- Open Trenchbroom and go to `View>Preferences` and check that `Game Path`
9 points to project root.
10
11## Trenchbroom entities
12
13- https://developer.valvesoftware.com/wiki/FGD
diff --git a/all.h b/all.h
index ac97717..26ccffb 100644
--- a/all.h
+++ b/all.h
@@ -15,122 +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;
79 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);
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
85typedef struct { 107typedef struct {
86 Camera camera; 108 MovementMode move_mode;
87 Model *world_models; 109 Vector3 pos;
88 int world_model_count; 110 Vector3 velocity;
89 bool cursor_captured; 111 bool is_grounded;
90 bool vsync; 112 float yaw;
91 Font font_ui; 113 float pitch;
92 114 float lean_amount;
93 MovementMode move_mode; 115 float crouch_amount;
94 Vector3 pos; 116 float horizontal_speed;
95 Vector3 velocity; 117} PlayerState;
96 bool is_grounded; 118
97 float yaw; 119void UpdatePlayer(void);
98 float pitch; 120
99 float lean_amount; 121// Game State Module
100 float crouch_amount; 122typedef enum {
101 float horizontal_speed; 123 STATE_MENU,
124 STATE_PLAYING
125} GameStateMode;
126
127typedef struct {
128 GameStateMode mode;
129 char map_path[256];
130 Camera camera;
131 bool cursor_captured;
132 bool vsync;
133 Font font_ui;
134
135 PlayerState player;
136 MapState map;
102} GameState; 137} GameState;
103 138
104extern GameState game; 139extern GameState game;
105 140
106// --- Prototypes --- 141void InitGame(void);
107 142void SetMap(const char *path);
108// Map 143void UpdateGame(void);
109char map_peek(MapParser *p); 144void DrawGame(void);
110char map_get(MapParser *p);
111void map_skip_whitespace(MapParser *p);
112bool map_expect(MapParser *p, char expected);
113void map_parse_token(MapParser *p, char *buffer, int size);
114Vector3 map_parse_vector(MapParser *p);
115Map ParseMap(const char *filename);
116void FreeMap(Map map);
117 145
118// Geometry 146// Geometry Helpers
119Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3); 147Plane PlaneFromPoints(Vector3 p1, Vector3 p2, Vector3 p3);
120void PolyFree(Polygon p); 148void PolyFree(Polygon p);
121Polygon PolyClip(Polygon poly, Plane plane); 149Polygon PolyClip(Polygon poly, Plane plane);
122Polygon CreateLargeQuad(Plane plane); 150Polygon CreateLargeQuad(Plane plane);
123Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane); 151Vector2 GetUV(Vector3 p, MapPlane *mp, Plane plane);
124 152
125// Game 153// Interface
126Texture2D GetTexture(const char *name); 154void DrawCrosshair(void);
127void InitGame(void); 155void DrawDebugInfo(void);
128void UpdateGame(void);
129void DrawGame(void);
130bool LoadMap(const char *filename);
131void UnloadMap(void);
132
133// Player
134void UpdatePlayer(void);
135 156
136#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 168cae4..40d1876 100644
--- a/game.c
+++ b/game.c
@@ -1,298 +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 6
43 if (tex.id == 0) { 7void InitGame(void) {
44 TraceLog(LOG_WARNING, "Failed to load texture: '%s' (tried path: '%s')", name, path); 8 memset(&game, 0, sizeof(game));
45 Image img = GenImageChecked(64, 64, 8, 8, (Color){128, 128, 128, 255}, (Color){200, 200, 200, 255}); 9
46 tex = LoadTextureFromImage(img); 10 game.font_ui = LoadFontVFS(UI_FONT_PATH, UI_FONT_SIZE);
47 UnloadImage(img); 11 game.mode = STATE_MENU;
48 } 12 game.cursor_captured = false;
49 13 EnableCursor();
50 if (tex.id != 0) { 14
51 GenTextureMipmaps(&tex); 15 game.player.move_mode = MOVE_NORMAL;
52 SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); 16 game.player.pos = (Vector3){ 0, 0, 0 };
53 SetTextureWrap(tex, TEXTURE_WRAP_REPEAT); 17 game.player.velocity = (Vector3){ 0, 0, 0 };
54 18 game.player.is_grounded = false;
55 CachedTexture cached; 19
56 strncpy(cached.name, name, sizeof(cached.name)); 20 // Camera setup
57 cached.tex = tex; 21 game.camera.up = (Vector3){ 0, 1, 0 };
58 array_push(texture_cache, cached); 22 game.camera.fovy = PLAYER_FOV;
59 } 23 game.camera.projection = CAMERA_PERSPECTIVE;
60
61 return tex;
62} 24}
63 25
64void UnloadMap(void) { 26void SetMap(const char *path) {
65 if (game.world_models) { 27 strncpy(game.map_path, path, sizeof(game.map_path) - 1);
66 for (int i = 0; i < game.world_model_count; i++) { 28 game.map_path[sizeof(game.map_path) - 1] = '\0';
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} 29}
79 30
80bool LoadMap(const char *filename) { 31void UpdateGame(void) {
81 TraceLog(LOG_INFO, "Loading map: %s", filename); 32 if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
82 33 game.cursor_captured = !game.cursor_captured;
83 Map map = ParseMap(filename); 34 if (game.cursor_captured) DisableCursor();
84 if (map.entity_count == 0) { 35 else EnableCursor();
85 TraceLog(LOG_ERROR, "Failed to load map or map is empty: %s", filename); 36 }
86 return false; 37
87 } 38 if (IsKeyPressed(KEY_V)) {
88 39 game.vsync = !game.vsync;
89 UnloadMap(); 40 if (game.vsync) {
90 41 SetWindowState(FLAG_VSYNC_HINT);
91 array(TextureGroup) groups; 42 SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
92 array_init(groups); 43 } else {
93 44 ClearWindowState(FLAG_VSYNC_HINT);
94 // Default camera if no start found 45 SetTargetFPS(0);
95 game.camera.position = (Vector3){ 0, 10, 0 }; 46 }
96 game.camera.target = (Vector3){ 1, 10, 0 }; 47 }
97 game.camera.up = (Vector3){ 0, 1, 0 }; 48
98 game.camera.fovy = PLAYER_FOV; 49 UpdatePlayer();
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
226void InitGame(void) {
227 array_init(texture_cache);
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
241 LoadMap("maps/demo1.map");
242
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} 50}
250 51
251void UpdateGame(void) { 52void DrawGame(void) {
252 if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { 53 BeginDrawing();
253 game.cursor_captured = !game.cursor_captured; 54 ClearBackground(DARKGRAY);
254 if (game.cursor_captured) DisableCursor();
255 else EnableCursor();
256 }
257 55
258 if (IsKeyPressed(KEY_ONE)) LoadMap("maps/demo1.map"); 56 BeginMode3D(game.camera);
259 if (IsKeyPressed(KEY_TWO)) LoadMap("maps/demo2.map");
260 57
261 if (IsKeyPressed(KEY_V)) { 58 rlEnableBackfaceCulling();
262 game.vsync = !game.vsync; 59 for (int i = 0; i < game.map.count; i++) {
263 if (game.vsync) { 60 DrawModel(game.map.models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE);
264 SetWindowState(FLAG_VSYNC_HINT); 61 }
265 SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor()));
266 } else {
267 ClearWindowState(FLAG_VSYNC_HINT);
268 SetTargetFPS(0);
269 }
270 }
271 62
272 UpdatePlayer(); 63 EndMode3D();
273}
274 64
275void DrawGame(void) { 65 DrawCrosshair();
276 BeginDrawing(); 66 DrawDebugInfo();
277 ClearBackground(DARKGRAY);
278 BeginMode3D(game.camera);
279
280 // Enable backface culling to hide interior faces of brushes
281 rlEnableBackfaceCulling();
282 for (int i = 0; i < game.world_model_count; i++) {
283 DrawModel(game.world_models[i], (Vector3){ 0, 0, 0 }, 1.0f, WHITE);
284 }
285
286 EndMode3D();
287
288 int screenWidth = GetScreenWidth();
289 int screenHeight = GetScreenHeight();
290 DrawLine(screenWidth / 2 - 10, screenHeight / 2, screenWidth / 2 + 10, screenHeight / 2, GREEN);
291 DrawLine(screenWidth / 2, screenHeight / 2 - 10, screenWidth / 2, screenHeight / 2 + 10, GREEN);
292
293 DrawTextEx(game.font_ui, TextFormat("%i FPS", GetFPS()), (Vector2){ 10, 10 }, 20, 2, GREEN);
294 DrawTextEx(game.font_ui, TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), (Vector2){ 10, 35 }, 20, 2, GREEN);
295 DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN);
296 67
297 EndDrawing(); 68 EndDrawing();
298} 69}
diff --git a/interface.c b/interface.c
new file mode 100644
index 0000000..58d8721
--- /dev/null
+++ b/interface.c
@@ -0,0 +1,14 @@
1#include "all.h"
2
3void DrawCrosshair(void) {
4 int screenWidth = GetScreenWidth();
5 int screenHeight = GetScreenHeight();
6 DrawLine(screenWidth / 2 - 10, screenHeight / 2, screenWidth / 2 + 10, screenHeight / 2, GREEN);
7 DrawLine(screenWidth / 2, screenHeight / 2 - 10, screenWidth / 2, screenHeight / 2 + 10, GREEN);
8}
9
10void DrawDebugInfo(void) {
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);
13 DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.player.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN);
14}
diff --git a/main.c b/main.c
index 481f5f3..9f32ecc 100644
--- a/main.c
+++ b/main.c
@@ -1,29 +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"
5 4
6int main(void) { 5#include <getopt.h>
7 SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI);
8 InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE);
9 6
10 int monitor = GetCurrentMonitor(); 7int main(int argc, char *argv[]) {
11 SetWindowPosition((GetMonitorWidth(monitor) - GetScreenWidth()) / 2, 8 stringb map_path;
12 (GetMonitorHeight(monitor) - GetScreenHeight()) / 2); 9 sb_init(&map_path, 256);
13 10 sb_append_cstr(&map_path, "maps/demo3.map");
14 SetTargetFPS(GetMonitorRefreshRate(monitor));
15 11
16 vfs_init("data.pak"); 12 bool skip_title = false;
17 InitGame();
18 game.vsync = true;
19 13
20 while (!WindowShouldClose()) { 14 static struct option long_options[] = {
21 UpdateGame(); 15 {"map", required_argument, 0, 'm'},
22 DrawGame(); 16 {0, 0, 0, 0}
23 } 17 };
24 18
25 vfs_shutdown(); 19 int opt;
26 CloseWindow(); 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 }
27 31
28 return 0; 32 SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI);
33 InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE);
34
35 int monitor = GetCurrentMonitor();
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);
42
43 if (skip_title) {
44 if (LoadMap(game.map_path)) {
45 game.mode = STATE_PLAYING;
46 game.cursor_captured = true;
47 DisableCursor();
48 }
49 }
50
51 game.vsync = true;
52
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 }
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;
29} 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/maps/demo1.map b/maps/demo1.map
index f7f73f7..a4e2168 100644
--- a/maps/demo1.map
+++ b/maps/demo1.map
@@ -6,7 +6,7 @@
6// brush 0 6// brush 0
7{ 7{
8( -176 -64 -16 ) ( -176 -63 -16 ) ( -176 -64 -15 ) brushes/ground_068 0 0 0 1 1 8( -176 -64 -16 ) ( -176 -63 -16 ) ( -176 -64 -15 ) brushes/ground_068 0 0 0 1 1
9( -64 -64 -16 ) ( -64 -64 -15 ) ( -63 -64 -16 ) brushes/ground_068 0 0 0 1 1 9( -64 -112 -16 ) ( -64 -112 -15 ) ( -63 -112 -16 ) brushes/ground_068 0 0 0 1 1
10( -64 -64 -16 ) ( -63 -64 -16 ) ( -64 -63 -16 ) brushes/ground_068 0 0 0 1 1 10( -64 -64 -16 ) ( -63 -64 -16 ) ( -64 -63 -16 ) brushes/ground_068 0 0 0 1 1
11( 64 64 16 ) ( 64 65 16 ) ( 65 64 16 ) brushes/ground_068 0 0 0 1 1 11( 64 64 16 ) ( 64 65 16 ) ( 65 64 16 ) brushes/ground_068 0 0 0 1 1
12( 64 64 16 ) ( 65 64 16 ) ( 64 64 17 ) brushes/ground_068 0 0 0 1 1 12( 64 64 16 ) ( 65 64 16 ) ( 64 64 17 ) brushes/ground_068 0 0 0 1 1
@@ -15,7 +15,7 @@
15// brush 1 15// brush 1
16{ 16{
17( 64 176 16 ) ( 64 -80 16 ) ( 64 -80 -16 ) brushes/bricks_076c 16 -16 0 1 1 17( 64 176 16 ) ( 64 -80 16 ) ( 64 -80 -16 ) brushes/bricks_076c 16 -16 0 1 1
18( 64 -80 16 ) ( 304 -80 0 ) ( 304 -80 -16 ) brushes/bricks_076c 0 0 0 1 1 18( 64 -80 16 ) ( 304 -80 0 ) ( 304 -80 -16 ) brushes/bricks_076c -21 0 0 1 1
19( 304 -80 -16 ) ( 304 176 -16 ) ( 64 176 -16 ) brushes/bricks_076c 0 0 0 1 1 19( 304 -80 -16 ) ( 304 176 -16 ) ( 64 176 -16 ) brushes/bricks_076c 0 0 0 1 1
20( 304 176 -16 ) ( 304 176 0 ) ( 64 176 16 ) brushes/bricks_076c 0 0 0 1 1 20( 304 176 -16 ) ( 304 176 0 ) ( 64 176 16 ) brushes/bricks_076c 0 0 0 1 1
21( 64 176 16 ) ( 304 176 0 ) ( 304 -80 0 ) brushes/bricks_076c 0 0 0 1 1 21( 64 176 16 ) ( 304 176 0 ) ( 304 -80 0 ) brushes/bricks_076c 0 0 0 1 1
diff --git a/maps/demo3.map b/maps/demo3.map
new file mode 100644
index 0000000..f12f33c
--- /dev/null
+++ b/maps/demo3.map
@@ -0,0 +1,234 @@
1// Game: Stalag
2// Format: Standard
3// entity 0
4{
5"classname" "worldspawn"
6// brush 0
7{
8( -592 -16 -32 ) ( -592 -15 -32 ) ( -592 -16 -31 ) brushes/bricks_076c -48 -16 0 1 1
9( -64 -384 -32 ) ( -64 -384 -31 ) ( -63 -384 -32 ) brushes/bricks_076c 0 -16 0 1 1
10( -64 -16 -16 ) ( -63 -16 -16 ) ( -64 -15 -16 ) brushes/bricks_076c 0 48 0 1 1
11( 64 112 0 ) ( 64 113 0 ) ( 65 112 0 ) brushes/bricks_076c 0 48 0 1 1
12( 64 336 0 ) ( 65 336 0 ) ( 64 336 1 ) brushes/bricks_076c 0 -16 0 1 1
13( 560 112 0 ) ( 560 112 1 ) ( 560 113 0 ) brushes/bricks_076c -48 -16 0 1 1
14}
15// brush 1
16{
17( -592 320 0 ) ( -592 321 0 ) ( -592 320 1 ) environment/planks_012 0 0 0 1 1
18( -592 320 0 ) ( -592 320 1 ) ( -591 320 0 ) environment/planks_012 0 0 0 1 1
19( -592 320 0 ) ( -591 320 0 ) ( -592 321 0 ) environment/planks_012 0 0 0 1 1
20( 560 336 272 ) ( 560 337 272 ) ( 561 336 272 ) environment/planks_012 0 0 0 1 1
21( 560 336 16 ) ( 561 336 16 ) ( 560 336 17 ) environment/planks_012 0 0 0 1 1
22( 560 336 16 ) ( 560 336 17 ) ( 560 337 16 ) environment/planks_012 0 0 0 1 1
23}
24// brush 2
25{
26( -592 -384 0 ) ( -592 -383 0 ) ( -592 -384 1 ) environment/planks_012 0 0 0 1 1
27( -592 -384 0 ) ( -592 -384 1 ) ( -591 -384 0 ) environment/planks_012 0 0 0 1 1
28( -592 -384 0 ) ( -591 -384 0 ) ( -592 -383 0 ) environment/planks_012 0 0 0 1 1
29( 560 -368 272 ) ( 560 -367 272 ) ( 561 -368 272 ) environment/planks_012 0 0 0 1 1
30( 560 -368 16 ) ( 561 -368 16 ) ( 560 -368 17 ) environment/planks_012 0 0 0 1 1
31( 560 -368 16 ) ( 560 -368 17 ) ( 560 -367 16 ) environment/planks_012 0 0 0 1 1
32}
33// brush 3
34{
35( -592 -368 0 ) ( -592 -367 0 ) ( -592 -368 1 ) environment/planks_012 0 0 0 1 1
36( -592 -368 0 ) ( -592 -368 1 ) ( -591 -368 0 ) environment/planks_012 0 0 0 1 1
37( -592 -368 0 ) ( -591 -368 0 ) ( -592 -367 0 ) environment/planks_012 0 0 0 1 1
38( -576 320 272 ) ( -576 321 272 ) ( -575 320 272 ) environment/planks_012 0 0 0 1 1
39( -576 320 16 ) ( -575 320 16 ) ( -576 320 17 ) environment/planks_012 0 0 0 1 1
40( -576 320 16 ) ( -576 320 17 ) ( -576 321 16 ) environment/planks_012 0 0 0 1 1
41}
42// brush 4
43{
44( 544 -368 0 ) ( 544 -367 0 ) ( 544 -368 1 ) environment/planks_012 0 0 0 1 1
45( 544 -368 0 ) ( 544 -368 1 ) ( 545 -368 0 ) environment/planks_012 0 0 0 1 1
46( 544 -368 0 ) ( 545 -368 0 ) ( 544 -367 0 ) environment/planks_012 0 0 0 1 1
47( 560 320 272 ) ( 560 321 272 ) ( 561 320 272 ) environment/planks_012 0 0 0 1 1
48( 560 320 16 ) ( 561 320 16 ) ( 560 320 17 ) environment/planks_012 0 0 0 1 1
49( 560 320 16 ) ( 560 320 17 ) ( 560 321 16 ) environment/planks_012 0 0 0 1 1
50}
51// brush 5
52{
53( -352 -352 -8 ) ( -352 -351 -8 ) ( -352 -352 -7 ) environment/paintedwood_008a -16 -8 0 1 1
54( -368 -352 -8 ) ( -368 -352 -7 ) ( -367 -352 -8 ) environment/paintedwood_008a 0 -8 0 1 1
55( -368 -352 8 ) ( -367 -352 8 ) ( -368 -351 8 ) environment/paintedwood_008a 0 16 0 1 1
56( -240 -144 24 ) ( -240 -143 24 ) ( -239 -144 24 ) environment/paintedwood_008a 0 16 0 1 1
57( -240 -144 8 ) ( -239 -144 8 ) ( -240 -144 9 ) environment/paintedwood_008a 0 -8 0 1 1
58( -240 -144 8 ) ( -240 -144 9 ) ( -240 -143 8 ) environment/paintedwood_008a -16 -8 0 1 1
59}
60// brush 6
61{
62( -256 -168 0 ) ( -256 -167 0 ) ( -256 -168 1 ) environment/planks_012 -24 0 0 1 1
63( -256 -168 0 ) ( -256 -168 1 ) ( -255 -168 0 ) environment/planks_012 0 0 0 1 1
64( -256 -168 0 ) ( -255 -168 0 ) ( -256 -167 0 ) environment/planks_012 0 24 0 1 1
65( -240 -152 8 ) ( -240 -151 8 ) ( -239 -152 8 ) environment/planks_012 0 24 0 1 1
66( -240 -152 16 ) ( -239 -152 16 ) ( -240 -152 17 ) environment/planks_012 0 0 0 1 1
67( -240 -152 16 ) ( -240 -152 17 ) ( -240 -151 16 ) environment/planks_012 -24 0 0 1 1
68}
69// brush 7
70{
71( -352 -168 0 ) ( -352 -167 0 ) ( -352 -168 1 ) environment/planks_012 -24 0 0 1 1
72( -352 -168 0 ) ( -352 -168 1 ) ( -351 -168 0 ) environment/planks_012 96 0 0 1 1
73( -352 -168 0 ) ( -351 -168 0 ) ( -352 -167 0 ) environment/planks_012 96 24 0 1 1
74( -336 -152 8 ) ( -336 -151 8 ) ( -335 -152 8 ) environment/planks_012 96 24 0 1 1
75( -336 -152 16 ) ( -335 -152 16 ) ( -336 -152 17 ) environment/planks_012 96 0 0 1 1
76( -336 -152 16 ) ( -336 -152 17 ) ( -336 -151 16 ) environment/planks_012 -24 0 0 1 1
77}
78// brush 8
79{
80( -352 -344 0 ) ( -352 -343 0 ) ( -352 -344 1 ) environment/planks_012 152 0 0 1 1
81( -352 -344 0 ) ( -352 -344 1 ) ( -351 -344 0 ) environment/planks_012 96 0 0 1 1
82( -352 -344 0 ) ( -351 -344 0 ) ( -352 -343 0 ) environment/planks_012 96 -152 0 1 1
83( -336 -328 8 ) ( -336 -327 8 ) ( -335 -328 8 ) environment/planks_012 96 -152 0 1 1
84( -336 -328 16 ) ( -335 -328 16 ) ( -336 -328 17 ) environment/planks_012 96 0 0 1 1
85( -336 -328 16 ) ( -336 -328 17 ) ( -336 -327 16 ) environment/planks_012 152 0 0 1 1
86}
87// brush 9
88{
89( -256 -344 0 ) ( -256 -343 0 ) ( -256 -344 1 ) environment/planks_012 152 0 0 1 1
90( -256 -344 0 ) ( -256 -344 1 ) ( -255 -344 0 ) environment/planks_012 0 0 0 1 1
91( -256 -344 0 ) ( -255 -344 0 ) ( -256 -343 0 ) environment/planks_012 0 -152 0 1 1
92( -240 -328 8 ) ( -240 -327 8 ) ( -239 -328 8 ) environment/planks_012 0 -152 0 1 1
93( -240 -328 16 ) ( -239 -328 16 ) ( -240 -328 17 ) environment/planks_012 0 0 0 1 1
94( -240 -328 16 ) ( -240 -328 17 ) ( -240 -327 16 ) environment/planks_012 152 0 0 1 1
95}
96// brush 10
97{
98( -328 -328 32 ) ( -344 -344 16 ) ( -344 -280 16 ) environment/roofingtiles_012b 8 -344 0 1 1
99( -272 -328 40 ) ( -256 -344 24 ) ( -352 -344 24 ) environment/roofingtiles_012b 208 -376 0 1 1
100( -256 -344 24 ) ( -256 -280 24 ) ( -352 -280 24 ) environment/roofingtiles_012b 208 24 0 1 1
101( -336 -296 32 ) ( -272 -296 32 ) ( -272 -328 32 ) environment/roofingtiles_012b 208 24 0 1 1
102( -272 -296 40 ) ( -336 -296 40 ) ( -352 -280 24 ) environment/roofingtiles_012b 208 -312 0 1 1
103( -272 -296 40 ) ( -256 -280 24 ) ( -256 -344 24 ) environment/roofingtiles_012b -80 -344 0 1 1
104}
105// brush 11
106{
107( -160 -328 32 ) ( -176 -344 16 ) ( -176 -280 16 ) environment/roofingtiles_012b -160 -344 0 1 1
108( -104 -328 40 ) ( -88 -344 24 ) ( -184 -344 24 ) environment/roofingtiles_012b 40 -376 0 1 1
109( -88 -344 24 ) ( -88 -280 24 ) ( -184 -280 24 ) environment/roofingtiles_012b 40 24 0 1 1
110( -168 -296 32 ) ( -104 -296 32 ) ( -104 -328 32 ) environment/roofingtiles_012b 40 24 0 1 1
111( -104 -296 40 ) ( -168 -296 40 ) ( -184 -280 24 ) environment/roofingtiles_012b 40 -312 0 1 1
112( -104 -296 40 ) ( -88 -280 24 ) ( -88 -344 24 ) environment/roofingtiles_012b -248 -344 0 1 1
113}
114// brush 12
115{
116( -184 -352 -8 ) ( -184 -351 -8 ) ( -184 -352 -7 ) environment/paintedwood_008a -16 -8 0 1 1
117( -200 -352 -8 ) ( -200 -352 -7 ) ( -199 -352 -8 ) environment/paintedwood_008a -168 -8 0 1 1
118( -200 -352 8 ) ( -199 -352 8 ) ( -200 -351 8 ) environment/paintedwood_008a -168 16 0 1 1
119( -72 -144 24 ) ( -72 -143 24 ) ( -71 -144 24 ) environment/paintedwood_008a -168 16 0 1 1
120( -72 -144 8 ) ( -71 -144 8 ) ( -72 -144 9 ) environment/paintedwood_008a -168 -8 0 1 1
121( -72 -144 8 ) ( -72 -144 9 ) ( -72 -143 8 ) environment/paintedwood_008a -16 -8 0 1 1
122}
123// brush 13
124{
125( 0 -328 32 ) ( -16 -344 16 ) ( -16 -280 16 ) environment/roofingtiles_012b -320 -344 0 1 1
126( 56 -328 40 ) ( 72 -344 24 ) ( -24 -344 24 ) environment/roofingtiles_012b -120 -376 0 1 1
127( 72 -344 24 ) ( 72 -280 24 ) ( -24 -280 24 ) environment/roofingtiles_012b -120 24 0 1 1
128( -8 -296 32 ) ( 56 -296 32 ) ( 56 -328 32 ) environment/roofingtiles_012b -120 24 0 1 1
129( 56 -296 40 ) ( -8 -296 40 ) ( -24 -280 24 ) environment/roofingtiles_012b -120 -312 0 1 1
130( 56 -296 40 ) ( 72 -280 24 ) ( 72 -344 24 ) environment/roofingtiles_012b -408 -344 0 1 1
131}
132// brush 14
133{
134( -24 -352 -8 ) ( -24 -351 -8 ) ( -24 -352 -7 ) environment/paintedwood_008a -16 -8 0 1 1
135( -40 -352 -8 ) ( -40 -352 -7 ) ( -39 -352 -8 ) environment/paintedwood_008a -328 -8 0 1 1
136( -40 -352 8 ) ( -39 -352 8 ) ( -40 -351 8 ) environment/paintedwood_008a -328 16 0 1 1
137( 88 -144 24 ) ( 88 -143 24 ) ( 89 -144 24 ) environment/paintedwood_008a -328 16 0 1 1
138( 88 -144 8 ) ( 89 -144 8 ) ( 88 -144 9 ) environment/paintedwood_008a -328 -8 0 1 1
139( 88 -144 8 ) ( 88 -144 9 ) ( 88 -143 8 ) environment/paintedwood_008a -16 -8 0 1 1
140}
141// brush 15
142{
143( -184 -344 0 ) ( -184 -343 0 ) ( -184 -344 1 ) environment/planks_012 152 0 0 1 1
144( -184 -344 0 ) ( -184 -344 1 ) ( -183 -344 0 ) environment/planks_012 -72 0 0 1 1
145( -184 -344 0 ) ( -183 -344 0 ) ( -184 -343 0 ) environment/planks_012 -72 -152 0 1 1
146( -168 -328 8 ) ( -168 -327 8 ) ( -167 -328 8 ) environment/planks_012 -72 -152 0 1 1
147( -168 -328 16 ) ( -167 -328 16 ) ( -168 -328 17 ) environment/planks_012 -72 0 0 1 1
148( -168 -328 16 ) ( -168 -328 17 ) ( -168 -327 16 ) environment/planks_012 152 0 0 1 1
149}
150// brush 16
151{
152( -88 -344 0 ) ( -88 -343 0 ) ( -88 -344 1 ) environment/planks_012 152 0 0 1 1
153( -88 -344 0 ) ( -88 -344 1 ) ( -87 -344 0 ) environment/planks_012 -168 0 0 1 1
154( -88 -344 0 ) ( -87 -344 0 ) ( -88 -343 0 ) environment/planks_012 -168 -152 0 1 1
155( -72 -328 8 ) ( -72 -327 8 ) ( -71 -328 8 ) environment/planks_012 -168 -152 0 1 1
156( -72 -328 16 ) ( -71 -328 16 ) ( -72 -328 17 ) environment/planks_012 -168 0 0 1 1
157( -72 -328 16 ) ( -72 -328 17 ) ( -72 -327 16 ) environment/planks_012 152 0 0 1 1
158}
159// brush 17
160{
161( -24 -344 0 ) ( -24 -343 0 ) ( -24 -344 1 ) environment/planks_012 152 0 0 1 1
162( -24 -344 0 ) ( -24 -344 1 ) ( -23 -344 0 ) environment/planks_012 -232 0 0 1 1
163( -24 -344 0 ) ( -23 -344 0 ) ( -24 -343 0 ) environment/planks_012 -232 -152 0 1 1
164( -8 -328 8 ) ( -8 -327 8 ) ( -7 -328 8 ) environment/planks_012 -232 -152 0 1 1
165( -8 -328 16 ) ( -7 -328 16 ) ( -8 -328 17 ) environment/planks_012 -232 0 0 1 1
166( -8 -328 16 ) ( -8 -328 17 ) ( -8 -327 16 ) environment/planks_012 152 0 0 1 1
167}
168// brush 18
169{
170( 72 -344 0 ) ( 72 -343 0 ) ( 72 -344 1 ) environment/planks_012 152 0 0 1 1
171( 72 -344 0 ) ( 72 -344 1 ) ( 73 -344 0 ) environment/planks_012 -328 0 0 1 1
172( 72 -344 0 ) ( 73 -344 0 ) ( 72 -343 0 ) environment/planks_012 -328 -152 0 1 1
173( 88 -328 8 ) ( 88 -327 8 ) ( 89 -328 8 ) environment/planks_012 -328 -152 0 1 1
174( 88 -328 16 ) ( 89 -328 16 ) ( 88 -328 17 ) environment/planks_012 -328 0 0 1 1
175( 88 -328 16 ) ( 88 -328 17 ) ( 88 -327 16 ) environment/planks_012 152 0 0 1 1
176}
177// brush 19
178{
179( -24 -168 0 ) ( -24 -167 0 ) ( -24 -168 1 ) environment/planks_012 -24 0 0 1 1
180( -24 -168 0 ) ( -24 -168 1 ) ( -23 -168 0 ) environment/planks_012 -232 0 0 1 1
181( -24 -168 0 ) ( -23 -168 0 ) ( -24 -167 0 ) environment/planks_012 -232 24 0 1 1
182( -8 -152 8 ) ( -8 -151 8 ) ( -7 -152 8 ) environment/planks_012 -232 24 0 1 1
183( -8 -152 16 ) ( -7 -152 16 ) ( -8 -152 17 ) environment/planks_012 -232 0 0 1 1
184( -8 -152 16 ) ( -8 -152 17 ) ( -8 -151 16 ) environment/planks_012 -24 0 0 1 1
185}
186// brush 20
187{
188( 72 -168 0 ) ( 72 -167 0 ) ( 72 -168 1 ) environment/planks_012 -24 0 0 1 1
189( 72 -168 0 ) ( 72 -168 1 ) ( 73 -168 0 ) environment/planks_012 -328 0 0 1 1
190( 72 -168 0 ) ( 73 -168 0 ) ( 72 -167 0 ) environment/planks_012 -328 24 0 1 1
191( 88 -152 8 ) ( 88 -151 8 ) ( 89 -152 8 ) environment/planks_012 -328 24 0 1 1
192( 88 -152 16 ) ( 89 -152 16 ) ( 88 -152 17 ) environment/planks_012 -328 0 0 1 1
193( 88 -152 16 ) ( 88 -152 17 ) ( 88 -151 16 ) environment/planks_012 -24 0 0 1 1
194}
195// brush 21
196{
197( -184 -168 0 ) ( -184 -167 0 ) ( -184 -168 1 ) environment/planks_012 -24 0 0 1 1
198( -184 -168 0 ) ( -184 -168 1 ) ( -183 -168 0 ) environment/planks_012 -72 0 0 1 1
199( -184 -168 0 ) ( -183 -168 0 ) ( -184 -167 0 ) environment/planks_012 -72 24 0 1 1
200( -168 -152 8 ) ( -168 -151 8 ) ( -167 -152 8 ) environment/planks_012 -72 24 0 1 1
201( -168 -152 16 ) ( -167 -152 16 ) ( -168 -152 17 ) environment/planks_012 -72 0 0 1 1
202( -168 -152 16 ) ( -168 -152 17 ) ( -168 -151 16 ) environment/planks_012 -24 0 0 1 1
203}
204// brush 22
205{
206( -88 -168 0 ) ( -88 -167 0 ) ( -88 -168 1 ) environment/planks_012 -24 0 0 1 1
207( -88 -168 0 ) ( -88 -168 1 ) ( -87 -168 0 ) environment/planks_012 -168 0 0 1 1
208( -88 -168 0 ) ( -87 -168 0 ) ( -88 -167 0 ) environment/planks_012 -168 24 0 1 1
209( -72 -152 8 ) ( -72 -151 8 ) ( -71 -152 8 ) environment/planks_012 -168 24 0 1 1
210( -72 -152 16 ) ( -71 -152 16 ) ( -72 -152 17 ) environment/planks_012 -168 0 0 1 1
211( -72 -152 16 ) ( -72 -152 17 ) ( -72 -151 16 ) environment/planks_012 -24 0 0 1 1
212}
213}
214// entity 1
215{
216"classname" "light"
217"origin" "152 -280 232"
218}
219// entity 2
220{
221"classname" "light"
222"origin" "-312 24 232"
223}
224// entity 3
225{
226"classname" "info_player_start"
227"origin" "-464 0 16"
228}
229// entity 4
230{
231"classname" "my_entity"
232"origin" "8 -72 8"
233"speed" "100"
234}
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}
diff --git a/tbrun.sh b/tbrun.sh
new file mode 100755
index 0000000..d933b6d
--- /dev/null
+++ b/tbrun.sh
@@ -0,0 +1,38 @@
1#!/bin/sh
2
3SCRIPT="$0"
4case "$SCRIPT" in
5 /*) ;;
6 *) SCRIPT="$(pwd)/$SCRIPT" ;;
7esac
8
9# resolve symlinks
10while [ -L "$SCRIPT" ]; do
11 LINK="$(readlink "$SCRIPT")"
12 case "$LINK" in
13 /*) SCRIPT="$LINK" ;;
14 *) SCRIPT="$(dirname "$SCRIPT")/$LINK" ;;
15 esac
16done
17
18SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)"
19echo "$SCRIPT_DIR"
20
21MAP=""
22while [ $# -gt 0 ]; do
23 case "$1" in
24 --map)
25 shift
26 MAP="$1"
27 ;;
28 --map=*)
29 MAP="${1#--map=}"
30 ;;
31 --) shift; break;;
32 *) ;;
33 esac
34 shift
35done
36
37cd "$SCRIPT_DIR"
38./bin/stalag --map "$MAP"
diff --git a/trenchbroom/stalag/Entities.fgd b/trenchbroom/stalag/Entities.fgd
new file mode 100644
index 0000000..abf5256
--- /dev/null
+++ b/trenchbroom/stalag/Entities.fgd
@@ -0,0 +1,29 @@
1@BaseClass = Targetname
2[
3 targetname(string) : "Name"
4]
5
6@BaseClass = Origin
7[
8 origin(string) : "Origin"
9]
10
11@SolidClass = worldspawn : "Worldspawn"
12[
13 message(string) : "Map title"
14]
15
16@PointClass base(Targetname, Origin) size(-16 -16 -16, 16 16 16) = info_player_start : "Player Start"
17[
18 angle(integer) : "Angle" : 0
19]
20
21@PointClass base(Targetname, Origin) size(-8 -8 -8, 8 8 8) = light : "Light"
22[
23 light(integer) : "Brightness" : 300
24]
25
26@PointClass base(Targetname) = my_entity : "My Entity"
27[
28 speed(integer) : "Speed" : 100
29]
diff --git a/trenchbroom/stalag/GameConfig.cfg b/trenchbroom/stalag/GameConfig.cfg
new file mode 100644
index 0000000..f061b4a
--- /dev/null
+++ b/trenchbroom/stalag/GameConfig.cfg
@@ -0,0 +1,25 @@
1{
2 "version": 9,
3 "name": "Stalag",
4
5 "fileformats": [
6 { "format": "Standard" }
7 ],
8
9 "filesystem": {
10 "searchpath": ".",
11 "packageformat": { "extension": ".pak", "format": "idpak" }
12 },
13
14 "materials": {
15 "root": "textures",
16 "extensions": [ ".png", ".jpg", ".tga" ]
17 },
18
19 "entities": {
20 "definitions": [ "Entities.fgd" ],
21 "defaultcolor": "0.6 0.6 0.6 1.0"
22 },
23
24 "softMapBounds": "-4096 -4096 -4096 4096 4096 4096"
25} \ No newline at end of file
diff --git a/trenchbroom/stalag/GameEngineProfiles.cfg b/trenchbroom/stalag/GameEngineProfiles.cfg
new file mode 100644
index 0000000..41cc5fb
--- /dev/null
+++ b/trenchbroom/stalag/GameEngineProfiles.cfg
@@ -0,0 +1,10 @@
1{
2 "profiles": [
3 {
4 "name": "Stalag",
5 "parameters": "--map maps/${MAP_BASE_NAME}.map",
6 "path": "/home/m/Projects/stalag/tbrun.sh"
7 }
8 ],
9 "version": 1
10}