]> git.lizzy.rs Git - nothing.git/blob - src/game/level_picker.c
(#1221) Introduce Emacs keybindings to level picker
[nothing.git] / src / game / level_picker.c
1 #include <stdio.h>
2
3 #include "./level_picker.h"
4
5 #include "game/sprite_font.h"
6 #include "system/lt.h"
7 #include "system/nth_alloc.h"
8 #include "system/stacktrace.h"
9 #include "system/str.h"
10 #include "system/log.h"
11 #include "system/file.h"
12
13 #define TITLE_MARGIN_TOP 100.0f
14 #define TITLE_MARGIN_BOTTOM 100.0f
15
16 #define LEVEL_PICKER_LIST_FONT_SCALE vec(5.0f, 5.0f)
17 #define LEVEL_PICKER_LIST_PADDING_BOTTOM 50.0f
18
19 #define ITEM_HEIGHT (FONT_CHAR_HEIGHT * LEVEL_PICKER_LIST_FONT_SCALE.y + LEVEL_PICKER_LIST_PADDING_BOTTOM)
20
21 #define SCROLLBAR_WIDTH 20
22 #define SCROLLING_SPEED_FRACTION 0.25f
23
24 void level_picker_populate(LevelPicker *level_picker,
25                            const char *dirpath)
26 {
27     trace_assert(level_picker);
28     trace_assert(dirpath);
29
30     level_picker->background.base_color = hexstr("073642");
31     level_picker->camera_position = vec(0.0f, 0.0f);
32
33     {
34         dynarray_clear(&level_picker->items);
35         level_picker->items = create_dynarray_malloc(METADATA_FILEPATH_MAX_SIZE);
36
37         DIR *level_dir = opendir(dirpath);
38         if (level_dir == NULL) {
39             log_fail("Can't open asset folder: %s\n", dirpath);
40             abort();
41         }
42
43         char filepath[METADATA_FILEPATH_MAX_SIZE];
44         for (struct dirent *d = readdir(level_dir);
45              d != NULL;
46              d = readdir(level_dir)) {
47             if (*d->d_name == '.') continue;
48
49             snprintf(filepath, METADATA_FILEPATH_MAX_SIZE,
50                      "%s/%s", dirpath, d->d_name);
51             dynarray_push(&level_picker->items, filepath);
52         }
53         closedir(level_dir);
54     }
55
56     level_picker->wiggly_text = (WigglyText) {
57         .text = "Select Level",
58         .scale = {10.0f, 10.0f},
59         .color = COLOR_WHITE,
60     };
61 }
62
63 int level_picker_render(const LevelPicker *level_picker,
64                         const Camera *camera)
65 {
66     trace_assert(level_picker);
67
68     const Rect viewport = camera_view_port_screen(camera);
69
70     if (background_render(&level_picker->background, camera) < 0) {
71         return -1;
72     }
73
74     const Vec2f title_size = wiggly_text_size(&level_picker->wiggly_text);
75     const float scrolling_area_height = viewport.h - ITEM_HEIGHT - level_picker->items_position.y;
76
77     wiggly_text_render(
78         &level_picker->wiggly_text,
79         camera,
80         vec(viewport.w * 0.5f - title_size.x * 0.5f, TITLE_MARGIN_TOP));
81
82     const float proportional_scroll = level_picker->items_scroll.y * scrolling_area_height / level_picker->items_size.y;
83     const float number_of_items_in_scrolling_area = scrolling_area_height / ITEM_HEIGHT;
84     const float percent_of_visible_items = number_of_items_in_scrolling_area / ((float) level_picker->items.count - 1);
85
86     if(level_picker->items.count > 0 && percent_of_visible_items < 1) {
87         SDL_Rect scrollbar = rect_for_sdl(
88             rect_from_vecs(
89                 vec(level_picker->items_position.x + level_picker->items_size.x, level_picker->items_position.y),
90                 vec(SCROLLBAR_WIDTH, scrolling_area_height)));
91
92         SDL_Rect scrollbar_thumb = rect_for_sdl(
93             rect_from_vecs(
94                 vec(level_picker->items_position.x + level_picker->items_size.x, level_picker->items_position.y - proportional_scroll),
95                 vec(SCROLLBAR_WIDTH, scrolling_area_height * percent_of_visible_items)));
96
97         if (SDL_SetRenderDrawColor(camera->renderer, 255, 255, 255, 255) < 0) {
98             return -1;
99         }
100
101         if (SDL_RenderDrawRect(camera->renderer, &scrollbar) < 0) {
102             return -1;
103         }
104
105         if (SDL_RenderFillRect(camera->renderer, &scrollbar_thumb) < 0) {
106             return -1;
107         }
108     }
109
110     for (size_t i = 0; i < level_picker->items.count; ++i) {
111         const Vec2f current_position = vec_sum(
112             level_picker->items_position,
113             vec(0.0f, (float) i * ITEM_HEIGHT + level_picker->items_scroll.y));
114
115         if(current_position.y > level_picker->items_position.y + scrolling_area_height ||
116             current_position.y < level_picker->items_position.y) {
117             continue;
118         }
119
120         const char *item_text = dynarray_pointer_at(&level_picker->items, i);
121
122         sprite_font_render_text(
123             &camera->font,
124             camera->renderer,
125             current_position,
126             LEVEL_PICKER_LIST_FONT_SCALE,
127             rgba(1.0f, 1.0f, 1.0f, 1.0f),
128             item_text);
129
130         if (i == level_picker->items_cursor) {
131             SDL_Rect boundary_box = rect_for_sdl(
132                 sprite_font_boundary_box(
133                     current_position,
134                     LEVEL_PICKER_LIST_FONT_SCALE,
135                     item_text));
136             if (SDL_SetRenderDrawColor(camera->renderer, 255, 255, 255, 255) < 0) {
137                 return -1;
138             }
139
140             if (SDL_RenderDrawRect(camera->renderer, &boundary_box) < 0) {
141                 return -1;
142             }
143         }
144     }
145
146     {
147         /* CSS */
148         const float padding = 20.0f;
149         const Vec2f size = vec(3.0f, 3.0f);
150         const Vec2f position = vec(0.0f, viewport.h - size.y * FONT_CHAR_HEIGHT);
151
152         /* HTML */
153         camera_render_text_screen(
154             camera,
155             "Press 'N' to create new level",
156             size,
157             COLOR_WHITE,
158             vec(position.x + padding,
159                 position.y - padding));
160     }
161
162     return 0;
163 }
164
165 int level_picker_update(LevelPicker *level_picker,
166                         Camera *camera,
167                         float delta_time)
168 {
169     trace_assert(level_picker);
170
171     const Rect viewport = camera_view_port_screen(camera);
172     const float scrolling_area_height = viewport.h - ITEM_HEIGHT - level_picker->items_position.y;
173
174     if ((float) level_picker->items_cursor * ITEM_HEIGHT + level_picker->items_scroll.y > scrolling_area_height) {
175         level_picker->items_scroll.y -= ITEM_HEIGHT * SCROLLING_SPEED_FRACTION;
176     }
177     if ((float) level_picker->items_cursor * ITEM_HEIGHT + level_picker->items_scroll.y < 0) {
178         level_picker->items_scroll.y += ITEM_HEIGHT * SCROLLING_SPEED_FRACTION;
179     }
180
181     vec_add(&level_picker->camera_position,
182             vec(50.0f * delta_time, 0.0f));
183
184     if (wiggly_text_update(&level_picker->wiggly_text, delta_time) < 0) {
185         return -1;
186     }
187
188     return 0;
189 }
190
191 static
192 Vec2f level_picker_list_size(const LevelPicker *level_picker)
193 {
194     trace_assert(level_picker);
195
196     Vec2f result = vec(0.0f, 0.0f);
197
198     for (size_t i = 0; i < level_picker->items.count; ++i) {
199         const char *item_text = dynarray_pointer_at(
200             &level_picker->items,
201             i);
202
203         Rect boundary_box = sprite_font_boundary_box(
204             vec(0.0f, 0.0f),
205             LEVEL_PICKER_LIST_FONT_SCALE,
206             item_text);
207
208         result.x = fmaxf(result.x, boundary_box.w);
209         result.y += boundary_box.h + LEVEL_PICKER_LIST_PADDING_BOTTOM;
210     }
211
212     return result;
213 }
214
215
216 int level_picker_event(LevelPicker *level_picker,
217                        const SDL_Event *event)
218 {
219     trace_assert(level_picker);
220     trace_assert(event);
221
222     switch (event->type) {
223     case SDL_WINDOWEVENT: {
224         switch (event->window.event) {
225         case SDL_WINDOWEVENT_SHOWN:
226         case SDL_WINDOWEVENT_SIZE_CHANGED: {
227             int width;
228             SDL_GetRendererOutputSize(SDL_GetRenderer(SDL_GetWindowFromID(event->window.windowID)), &width, NULL);
229             const Vec2f title_size = wiggly_text_size(&level_picker->wiggly_text);
230             level_picker->items_size = level_picker_list_size(level_picker);
231
232             level_picker->items_position =
233                 vec((float)width * 0.5f - level_picker->items_size.x * 0.5f,
234                     TITLE_MARGIN_TOP + title_size.y + TITLE_MARGIN_BOTTOM);
235         } break;
236         }
237     } break;
238
239     case SDL_KEYDOWN: {
240         switch (event->key.keysym.sym) {
241         case SDLK_RETURN: {
242             if (level_picker->items_cursor < level_picker->items.count) {
243                 level_picker->selected_item = (int) level_picker->items_cursor;
244             }
245         } break;
246         }
247     } break;
248
249     case SDL_MOUSEMOTION: {
250         const Vec2f mouse_pos = vec((float) event->motion.x, (float) event->motion.y);
251         Vec2f position = vec_sum(
252             level_picker->items_position,
253             level_picker->items_scroll);
254
255         for (size_t i = 0; i < level_picker->items.count; ++i) {
256             const char *item_text = dynarray_pointer_at(
257                 &level_picker->items,
258                 i);
259
260             Rect boundary_box = sprite_font_boundary_box(
261                 position,
262                 LEVEL_PICKER_LIST_FONT_SCALE,
263                 item_text);
264
265             if (rect_contains_point(boundary_box, mouse_pos)) {
266                 level_picker->items_cursor = i;
267             }
268
269             position.y += boundary_box.h + LEVEL_PICKER_LIST_PADDING_BOTTOM;
270         }
271     } break;
272
273     case SDL_MOUSEBUTTONDOWN: {
274         switch (event->button.button) {
275         case SDL_BUTTON_LEFT: {
276             // check if the click position was actually inside...
277             // note: make sure there's actually stuff in the list! tsoding likes
278             // to remove all levels and change title to "SMOL BREAK"...
279             if (level_picker->items.count == 0)
280                 break;
281
282             // note: this assumes that all list items are the same height!
283             // this is probably a valid assumption as long as we use a sprite font.
284             float single_item_height =
285                 FONT_CHAR_HEIGHT * LEVEL_PICKER_LIST_FONT_SCALE.y + LEVEL_PICKER_LIST_PADDING_BOTTOM;
286
287             Vec2f position = vec_sum(
288                 level_picker->items_position,
289                 level_picker->items_scroll);
290             vec_add(&position, vec(0.0f, (float) level_picker->items_cursor * single_item_height));
291
292             const char *item_text =
293                 dynarray_pointer_at(
294                     &level_picker->items,
295                     level_picker->items_cursor);
296
297             Rect boundary_box = sprite_font_boundary_box(
298                 position,
299                 LEVEL_PICKER_LIST_FONT_SCALE,
300                 item_text);
301
302             const Vec2f mouse_pos = vec((float) event->motion.x, (float) event->motion.y);
303             if (rect_contains_point(boundary_box, mouse_pos)) {
304                 level_picker->selected_item = (int) level_picker->items_cursor;
305             }
306         } break;
307         }
308     } break;
309     }
310
311     return 0;
312 }
313
314 int level_picker_input(LevelPicker *level_picker,
315                        const Uint8 *const keyboard_state,
316                        SDL_Joystick *the_stick_of_joy)
317 {
318     trace_assert(level_picker);
319     trace_assert(keyboard_state);
320     (void) the_stick_of_joy;
321     return 0;
322 }
323
324 const char *level_picker_selected_level(const LevelPicker *level_picker)
325 {
326     trace_assert(level_picker);
327
328     if (level_picker->selected_item < 0) {
329         return NULL;
330     }
331
332     return dynarray_pointer_at(
333         &level_picker->items,
334         (size_t)level_picker->selected_item);
335 }
336
337 void level_picker_clean_selection(LevelPicker *level_picker)
338 {
339     trace_assert(level_picker);
340     level_picker->selected_item = -1;
341 }
342
343 int level_picker_enter_camera_event(LevelPicker *level_picker,
344                                     Camera *camera)
345 {
346     camera_center_at(camera, level_picker->camera_position);
347     return 0;
348 }
349
350 void level_picker_cursor_up(LevelPicker *level_picker)
351 {
352     trace_assert(level_picker);
353     if (level_picker->items_cursor == 0) {
354         level_picker->items_cursor = level_picker->items.count - 1;
355     } else {
356         level_picker->items_cursor--;
357     }
358 }
359
360 void level_picker_cursor_down(LevelPicker *level_picker)
361 {
362     trace_assert(level_picker);
363     level_picker->items_cursor++;
364     if (level_picker->items_cursor == level_picker->items.count) {
365         level_picker->items_cursor = 0;
366     }
367 }