]> git.lizzy.rs Git - nothing.git/blob - src/game/level_picker.c
(#1221) Disable wrapping around in 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_MOUSEWHEEL: {
250         if (event->wheel.y < 0) {
251             level_picker_cursor_down(level_picker);
252         } else if (event->wheel.y > 0) {
253             level_picker_cursor_up(level_picker);
254         }
255     } break;
256
257     case SDL_MOUSEMOTION: {
258         const Vec2f mouse_pos = vec((float) event->motion.x, (float) event->motion.y);
259         Vec2f position = vec_sum(
260             level_picker->items_position,
261             level_picker->items_scroll);
262
263         for (size_t i = 0; i < level_picker->items.count; ++i) {
264             const char *item_text = dynarray_pointer_at(
265                 &level_picker->items,
266                 i);
267
268             Rect boundary_box = sprite_font_boundary_box(
269                 position,
270                 LEVEL_PICKER_LIST_FONT_SCALE,
271                 item_text);
272
273             if (rect_contains_point(boundary_box, mouse_pos)) {
274                 level_picker->items_cursor = i;
275             }
276
277             position.y += boundary_box.h + LEVEL_PICKER_LIST_PADDING_BOTTOM;
278         }
279     } break;
280
281     case SDL_MOUSEBUTTONDOWN: {
282         switch (event->button.button) {
283         case SDL_BUTTON_LEFT: {
284             // check if the click position was actually inside...
285             // note: make sure there's actually stuff in the list! tsoding likes
286             // to remove all levels and change title to "SMOL BREAK"...
287             if (level_picker->items.count == 0)
288                 break;
289
290             // note: this assumes that all list items are the same height!
291             // this is probably a valid assumption as long as we use a sprite font.
292             float single_item_height =
293                 FONT_CHAR_HEIGHT * LEVEL_PICKER_LIST_FONT_SCALE.y + LEVEL_PICKER_LIST_PADDING_BOTTOM;
294
295             Vec2f position = vec_sum(
296                 level_picker->items_position,
297                 level_picker->items_scroll);
298             vec_add(&position, vec(0.0f, (float) level_picker->items_cursor * single_item_height));
299
300             const char *item_text =
301                 dynarray_pointer_at(
302                     &level_picker->items,
303                     level_picker->items_cursor);
304
305             Rect boundary_box = sprite_font_boundary_box(
306                 position,
307                 LEVEL_PICKER_LIST_FONT_SCALE,
308                 item_text);
309
310             const Vec2f mouse_pos = vec((float) event->motion.x, (float) event->motion.y);
311             if (rect_contains_point(boundary_box, mouse_pos)) {
312                 level_picker->selected_item = (int) level_picker->items_cursor;
313             }
314         } break;
315         }
316     } break;
317     }
318
319     return 0;
320 }
321
322 int level_picker_input(LevelPicker *level_picker,
323                        const Uint8 *const keyboard_state,
324                        SDL_Joystick *the_stick_of_joy)
325 {
326     trace_assert(level_picker);
327     trace_assert(keyboard_state);
328     (void) the_stick_of_joy;
329     return 0;
330 }
331
332 const char *level_picker_selected_level(const LevelPicker *level_picker)
333 {
334     trace_assert(level_picker);
335
336     if (level_picker->selected_item < 0) {
337         return NULL;
338     }
339
340     return dynarray_pointer_at(
341         &level_picker->items,
342         (size_t)level_picker->selected_item);
343 }
344
345 void level_picker_clean_selection(LevelPicker *level_picker)
346 {
347     trace_assert(level_picker);
348     level_picker->selected_item = -1;
349 }
350
351 int level_picker_enter_camera_event(LevelPicker *level_picker,
352                                     Camera *camera)
353 {
354     camera_center_at(camera, level_picker->camera_position);
355     return 0;
356 }
357
358 void level_picker_cursor_up(LevelPicker *level_picker)
359 {
360     trace_assert(level_picker);
361     if (level_picker->items_cursor > 0) {
362         level_picker->items_cursor--;
363     }
364 }
365
366 void level_picker_cursor_down(LevelPicker *level_picker)
367 {
368     trace_assert(level_picker);
369      if (level_picker->items_cursor + 1 < level_picker->items.count) {
370         level_picker->items_cursor++;
371     }
372 }