/* * Entropie - small first-person sandbox (SDL3 + OpenGL 3.3), * painted in the rotoscope style of "Waking Life". * * Layers: core (platform, gl, input) -> render -> game -> main */ #include "core/audio.h" #include "game/sound.h" #include #include #include #include #include "build_info.h" /* generated by the Makefile */ #include "core/input.h" #include "core/platform.h" #include "game/game.h" #include "render/renderer.h" #include "render/skybox.h" #include "render/ui.h" #include "game/save.h" #define APP_NAME "Entropie" #define WINDOW_WIDTH 800 #define WINDOW_HEIGHT 800 #define UI_FONT "fonts/GochiHand-Regular.ttf" /* SIL Open Font License, see fonts/OFL.txt */ #define UI_FONT_SIZE 27.0f #define CONTROLS_HELP "I keys Tab you" #define NEEDS_LABEL 3 #define KEYS_LABEL 4 /* first UI label slot of the key list */ #define BUBBLE_LABEL 32 /* speech bubbles */ #define BUBBLE_MAX 8 #define CHOICE_LABEL 40 /* answers in a conversation */ #define HINT_LABEL 46 /* what to do now (someone follows you) */ #define STATS_LABEL 48 /* the stats panel */ #define STATS_MAX 12 #define DEV_LABEL 64 /* the dev menu (F1) */ #define MENU_LABEL 104 /* the menus (start, Esc, load, save as): up to 16 slots */ #define POCKET_LABEL 88 /* your pockets (B) */ #define CREW_LABEL 120 /* the crew overview (F2) */ #define CREW_LINES 20 static const char *const KEYS[] = { /* proportional font: no column padding */ "W A S D: walk, mouse: look around", "Shift: jog (tap it again and again: sprint)", "Space: jump (higher with a run-up)", "Left click: take what's in front (heavy things: hold on / lift)", "Left click again: drink / eat, or work with a tool", "Fireplace: click to put wood in / light it / take food off the grill", "Bag in hand: click on trash", "X: talk to who's in front, else shout for someone; 1 - 5: answer", "Hold left click: keep going", "Right click: put down / let go", "E / Q: take / drop with right / left hand", "F (hold): push / pull; look left / right to turn it", "Ctrl (hold): look around (holding on: without turning it)", "E / Q while holding on: lift together / put down", "T: toss what you carry (hold T: wind up, throw harder overhand)", "P: pee (hold Shift: further; aim with the mouse)", "Ladder in hand: click to slide it out, at a wall to lean it there", "Leaning ladder: click to climb; E / Q or right click: take it down", "Axe or chainsaw at a tree stump: chop it out", "Z: sleep / get up (asleep, hold Space: faster)", "C: sit down (by the fire) / get up", "G: one more person helps you, R: call someone over", "H: everybody stops helping", "Backspace: call off showing someone how / where; at a meeting: skip it", "V: first / third person", "I: show / hide keys", "F1: dev menu (change anybody's stats)", "F2: the crew overview (Left / Right: who's who, now, the projects)", "Tab: your stats (arrow keys: next page)", "K: smoke; B: your pockets (Up / Down, Enter: take out or give); J: your notebook", "Esc: menu (resume, save, load, keys, quit); F5 quick save, F9 quick load; M sound off / on", }; #define KEY_COUNT ((int)(sizeof(KEYS) / sizeof(KEYS[0]))) #define MAX_FRAME_DT 0.1f /* avoids tunneling after stalls (window drag etc.) */ /* Your stats on a painted sheet of paper under the clock. */ static void drawStats(Ui *ui, const Game *game, int width, int height) { StatBar bars[STATS_MAX]; char about[UI_TEXT_MAX], head[UI_TEXT_MAX]; const char *title = ""; int n = Game_stats(game, bars, STATS_MAX, &title, about, sizeof(about)); snprintf(head, sizeof(head), "%s - %s (%d/%d, arrow keys)", game->player->name, title, game->stats_page + 1, STATS_PAGES); const float row = 32.0f, pw = 380.0f, ph = 70.0f + row * (float)n + 46.0f; const float x = (float)width - pw - 18.0f, top = (float)height - 58.0f; Ui_drawPatch(ui, x, top - ph, pw, ph, 0.93f, 0.85f, 0.67f, 0.93f, 1.0f, width, height); Ui_drawTextLeft(ui, STATS_LABEL, head, x + 18.0f, top - 46.0f, width, height); const float bar_x = x + 130.0f, bar_w = pw - 150.0f; for (int i = 0; i < n && STATS_LABEL + 2 + i < UI_LABELS; i++) { float y = top - 60.0f - row * (float)(i + 1); Ui_drawTextLeft(ui, STATS_LABEL + 2 + i, bars[i].label, x + 22.0f, y, width, height); /* Good is green; a need goes over ochre to red as it grows */ float v = clampf(bars[i].value, 0.0f, 1.0f); float bad = bars[i].need ? v : 1.0f - v; float r = 0.42f + 0.45f * bad, g = 0.62f - 0.30f * bad * bad, b = 0.30f - 0.08f * bad; Ui_drawPatch(ui, bar_x, y + 6.0f, bar_w, 16.0f, 0.86f, 0.80f, 0.66f, 1.0f, 0.6f, width, height); if (v > 0.02f) Ui_drawPatch(ui, bar_x + 1.0f, y + 7.0f, (bar_w - 2.0f) * v, 14.0f, r, g, b, 0.95f, 0.0f, width, height); } Ui_drawTextLeft(ui, STATS_LABEL + 1, about, x + 18.0f, top - ph + 14.0f, width, height); } /* The dev menu (F1) on a sheet of paper on the left. */ static void drawDev(Ui *ui, const Game *game, int width, int height) { char lines[DEV_LINES][96]; int chosen = -1; int n = Game_devLines(game, lines, DEV_LINES, &chosen); if (n == 0) return; const float row = 30.0f, pw = 560.0f, ph = 30.0f + row * (float)n; const float x = 18.0f, top = (float)height * 0.5f + ph * 0.5f; Ui_drawPatch(ui, x, top - ph, pw, ph, 0.88f, 0.90f, 0.80f, 0.93f, 1.0f, width, height); for (int i = 0; i < n && DEV_LABEL + i < UI_LABELS; i++) { float y = top - 38.0f - row * (float)i; if (i == chosen) Ui_drawPatch(ui, x + 10.0f, y - 2.0f, pw - 20.0f, row - 2.0f, 0.95f, 0.78f, 0.45f, 0.8f, 0.0f, width, height); Ui_drawTextLeft(ui, DEV_LABEL + i, lines[i], x + 22.0f, y, width, height); } } /* The crew overview (F2): a big sheet in the middle. */ static void drawCrew(Ui *ui, const Game *game, int width, int height) { char lines[CREW_LINES][128]; const char *title = ""; int n = Game_crewLines(game, lines, CREW_LINES - 1, &title); const float row = 28.0f, pw = fminf(1240.0f, (float)width - 36.0f), ph = 60.0f + row * (float)n; const float x = ((float)width - pw) * 0.5f, top = (float)height * 0.5f + ph * 0.5f; Ui_drawPatch(ui, x, top - ph, pw, ph, 0.90f, 0.88f, 0.78f, 0.95f, 1.0f, width, height); Ui_drawTextLeft(ui, CREW_LABEL, title, x + 22.0f, top - 38.0f, width, height); for (int i = 0; i < n && CREW_LABEL + 1 + i < UI_LABELS; i++) Ui_drawTextLeft(ui, CREW_LABEL + 1 + i, lines[i], x + 22.0f, top - 72.0f - row * (float)i, width, height); } /* Your notebook (J): the same sheet (the crew overview's places: never both). */ static void drawNotes(Ui *ui, const Game *game, int width, int height) { char lines[CREW_LINES][128]; const char *title = ""; int n = Game_notesLines(game, lines, CREW_LINES - 1, &title); const float row = 28.0f, pw = fminf(1240.0f, (float)width - 36.0f), ph = 60.0f + row * (float)n; const float x = ((float)width - pw) * 0.5f, top = (float)height * 0.5f + ph * 0.5f; Ui_drawPatch(ui, x, top - ph, pw, ph, 0.93f, 0.91f, 0.82f, 0.95f, 1.0f, width, height); Ui_drawTextLeft(ui, CREW_LABEL, title, x + 22.0f, top - 38.0f, width, height); for (int i = 0; i < n && CREW_LABEL + 1 + i < UI_LABELS; i++) Ui_drawTextLeft(ui, CREW_LABEL + 1 + i, lines[i], x + 22.0f, top - 72.0f - row * (float)i, width, height); } /* Your pockets (B) on a sheet of paper on the right. */ static void drawPockets(Ui *ui, Game *game, int width, int height) { char lines[POCKET_LINES][96]; int chosen = -1; int n = Game_pocketLines(game, lines, POCKET_LINES, &chosen); if (n == 0) return; const float row = 30.0f, pw = 520.0f, ph = 30.0f + row * (float)n; const float x = (float)width - pw - 18.0f, top = (float)height * 0.5f + ph * 0.5f; Ui_drawPatch(ui, x, top - ph, pw, ph, 0.90f, 0.86f, 0.74f, 0.93f, 1.0f, width, height); for (int i = 0; i < n && POCKET_LABEL + i < UI_LABELS; i++) { float y = top - 38.0f - row * (float)i; if (i == chosen) Ui_drawPatch(ui, x + 10.0f, y - 2.0f, pw - 20.0f, row - 2.0f, 0.95f, 0.78f, 0.45f, 0.8f, 0.0f, width, height); Ui_drawTextLeft(ui, POCKET_LABEL + i, lines[i], x + 22.0f, y, width, height); } } /* ---------- Menus: the start, Esc (the game waits meanwhile), loading, naming a save ---------- */ typedef enum { SCREEN_NONE, SCREEN_START, SCREEN_PAUSE, SCREEN_LOAD, SCREEN_NAME } Screen; typedef enum { ACT_NONE, ACT_NEW, ACT_CONTINUE, ACT_LOAD_LIST, ACT_QUIT, ACT_RESUME, ACT_SAVE_AS, ACT_QUICK_SAVE, ACT_KEYS, ACT_LOAD, ACT_BACK, ACT_NAME, ACT_SAVE_NAMED } Action; #define MENU_ITEMS 14 #define MENU_W 600.0f #define MENU_BH 44.0f /* button height */ #define MENU_GAP 10.0f #define AUTOSAVE_EVERY 300.0f /* s of play between autosaves */ #define QUICK_SAVE "quicksave" typedef struct { Screen screen, back; /* back: where "Back" / Esc goes from the load list or the name */ int chosen, count; char item[MENU_ITEMS][UI_TEXT_MAX]; Action act[MENU_ITEMS]; int arg[MENU_ITEMS]; /* ACT_LOAD: which save */ const char *title; SaveInfo saves[SAVE_LIST_MAX]; int save_count; char name[SAVE_NAME_MAX]; /* a save's name being typed */ char dir[512]; /* where the saves go */ } Menu; static void menuAdd(Menu *m, Action act, int arg, const char *text) { if (m->count >= MENU_ITEMS) return; snprintf(m->item[m->count], UI_TEXT_MAX, "%s", text); m->act[m->count] = act; m->arg[m->count] = arg; m->count++; } /* Fills the menu for a screen. */ static void menuOpen(Menu *m, Screen screen) { m->screen = screen; m->count = 0; m->chosen = 0; char line[UI_TEXT_MAX]; switch (screen) { case SCREEN_START: { m->title = APP_NAME " " APP_VERSION; m->save_count = Save_list(m->dir, m->saves, SAVE_LIST_MAX); int autosave = -1; for (int i = 0; i < m->save_count; i++) if (!strcmp(m->saves[i].name, SAVE_AUTO)) autosave = i; if (autosave >= 0) { snprintf(line, sizeof line, "Continue %.40s", m->saves[autosave].label); menuAdd(m, ACT_CONTINUE, autosave, line); } menuAdd(m, ACT_NEW, 0, "New game"); if (m->save_count > 0) menuAdd(m, ACT_LOAD_LIST, 0, "Load a save"); menuAdd(m, ACT_QUIT, 0, "Quit"); break; } case SCREEN_PAUSE: m->title = "Paused"; menuAdd(m, ACT_RESUME, 0, "Resume"); menuAdd(m, ACT_SAVE_AS, 0, "Save as ..."); menuAdd(m, ACT_QUICK_SAVE, 0, "Quick save (F5; F9 loads it)"); menuAdd(m, ACT_LOAD_LIST, 0, "Load a save"); menuAdd(m, ACT_KEYS, 0, "Keys (show / hide)"); menuAdd(m, ACT_QUIT, 0, "Quit (autosaves)"); break; case SCREEN_LOAD: m->title = "Load a save"; m->save_count = Save_list(m->dir, m->saves, SAVE_LIST_MAX); for (int i = 0; i < m->save_count && m->count < MENU_ITEMS - 1; i++) { snprintf(line, sizeof line, "%.20s %.40s", m->saves[i].name, m->saves[i].label); menuAdd(m, ACT_LOAD, i, line); } menuAdd(m, ACT_BACK, 0, "Back"); break; case SCREEN_NAME: m->title = "Save as (type a name, Enter)"; snprintf(line, sizeof line, "%s_", m->name); menuAdd(m, ACT_NAME, 0, line); menuAdd(m, ACT_SAVE_NAMED, 0, "Save"); menuAdd(m, ACT_BACK, 0, "Back"); m->chosen = 1; break; case SCREEN_NONE: break; } } /* Button k's box (pixels, y from the bottom). */ static void menuButton(const Menu *m, int k, int width, int height, float *x, float *y) { float total = (float)m->count * (MENU_BH + MENU_GAP) - MENU_GAP; *x = ((float)width - MENU_W) * 0.5f + 20.0f; *y = (float)height * 0.5f + total * 0.5f - (float)(k + 1) * MENU_BH - (float)k * MENU_GAP - 20.0f; } /* The button under the pointer, or -1. */ static int menuHit(const Menu *m, const Platform *platform, const Input *in, int width, int height) { int ww, wh; Platform_windowSize(platform, &ww, &wh); if (ww <= 0 || wh <= 0) return -1; float mx = in->mouse_x * (float)width / (float)ww, my = (float)height - in->mouse_y * (float)height / (float)wh; for (int k = 0; k < m->count; k++) { float x, y; menuButton(m, k, width, height, &x, &y); if (mx >= x && mx <= x + MENU_W - 40.0f && my >= y && my <= y + MENU_BH) return k; } return -1; } static void drawMenu(Ui *ui, const Menu *m, const char *note, int width, int height) { float total = (float)m->count * (MENU_BH + MENU_GAP) - MENU_GAP + 40.0f + 50.0f + (note ? 36.0f : 0.0f); float px = ((float)width - MENU_W) * 0.5f, py = (float)height * 0.5f - total * 0.5f + 25.0f - (note ? 36.0f : 0.0f); Ui_drawPatch(ui, px, py - 20.0f, MENU_W, total, 0.93f, 0.85f, 0.67f, 0.95f, 1.0f, width, height); Ui_drawTextLeft(ui, MENU_LABEL, m->title, px + 24.0f, py + total - 70.0f, width, height); for (int k = 0; k < m->count; k++) { float x, y; menuButton(m, k, width, height, &x, &y); bool on = k == m->chosen; Ui_drawPatch(ui, x, y, MENU_W - 40.0f, MENU_BH, on ? 0.95f : 0.86f, on ? 0.72f : 0.80f, on ? 0.40f : 0.66f, 1.0f, 0.6f, width, height); Ui_drawTextLeft(ui, MENU_LABEL + 1 + k, m->item[k], x + 18.0f, y + 9.0f, width, height); } if (note) Ui_drawTextLeft(ui, MENU_LABEL + 1 + MENU_ITEMS, note, px + 24.0f, py - 6.0f, width, height); } /* A name typed in: letters, digits, - and _ (spaces become -). */ static void typeName(char *name, const Input *in) { size_t n = strlen(name); if (in->erase && n > 0) name[--n] = '\0'; for (const char *c = in->text; *c && n + 1 < SAVE_NAME_MAX - 1; c++) { char ch = *c == ' ' ? '-' : *c; if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_') { name[n++] = ch; name[n] = '\0'; } } } int main(int argc, char **argv) { (void)argc; (void)argv; const char *title = APP_NAME " " APP_VERSION " (build " APP_BUILD ")"; SDL_Log("%s", title); int exit_code = 1; Platform platform = { 0 }; Renderer renderer = { 0 }; Skybox skybox = { 0 }; Ui ui = { 0 }; Game game = { 0 }; bool game_ready = false; if (!Platform_init(&platform, title, WINDOW_WIDTH, WINDOW_HEIGHT)) goto cleanup; if (!Renderer_init(&renderer)) goto cleanup; if (!Skybox_init(&skybox)) goto cleanup; if (!Ui_init(&ui, UI_FONT, UI_FONT_SIZE)) goto cleanup; if (!(game_ready = Game_init(&game))) goto cleanup; Audio_init(); /* (no sound device: silent, that's all) */ Platform_frameDelta(&platform); /* reset timer after loading */ float anim_time = 0.0f; /* real seconds, drives the painted animation */ bool look_settle = false; float menu_mouse_x = -1.0f, menu_mouse_y = -1.0f, autosave_t = 0.0f, note_time = 0.0f; char note[UI_TEXT_MAX] = ""; static Menu menu; if (!Platform_saveDir(menu.dir, sizeof menu.dir)) snprintf(menu.dir, sizeof menu.dir, "saves"); menuOpen(&menu, SCREEN_START); /* (the world waits behind it: a new game starts from here) */ Platform_captureMouse(&platform, false); for (;;) { float dt = Platform_frameDelta(&platform); if (dt > MAX_FRAME_DT) dt = MAX_FRAME_DT; anim_time += dt; note_time = fmaxf(0.0f, note_time - dt); Input input; Input_poll(&input); bool playing = menu.screen == SCREEN_NONE || menu.screen == SCREEN_PAUSE || menu.back == SCREEN_PAUSE; if (input.quit) { char err[160]; if (playing && menu.screen != SCREEN_START) Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err); /* (closing the window: autosaved) */ break; } int width, height; Platform_drawableSize(&platform, &width, &height); Action act = ACT_NONE; int arg = 0; if (input.escape) { /* the menu, or back to the game (from a list: back a step) */ if (menu.screen == SCREEN_NONE) menuOpen(&menu, SCREEN_PAUSE); else if (menu.screen == SCREEN_PAUSE) act = ACT_RESUME; else if (menu.screen == SCREEN_LOAD || menu.screen == SCREEN_NAME) act = ACT_BACK; Platform_captureMouse(&platform, menu.screen == SCREEN_NONE); look_settle = true; } if (menu.screen == SCREEN_NONE && (input.quick_save || input.quick_load)) { char err[160]; if (input.quick_save) { bool ok = Save_write(&game, menu.dir, QUICK_SAVE, err, sizeof err); snprintf(note, sizeof note, ok ? "Quick saved" : "Not saved: %s", err); } else { bool ok = Save_read(&game, menu.dir, QUICK_SAVE, err, sizeof err); snprintf(note, sizeof note, ok ? "Quick save loaded" : "Not loaded: %s", err); look_settle = true; } note_time = 3.0f; } if (menu.screen != SCREEN_NONE && act == ACT_NONE) { int hit = menuHit(&menu, &platform, &input, width, height); if (hit >= 0 && (input.mouse_x != menu_mouse_x || input.mouse_y != menu_mouse_y)) menu.chosen = hit; menu_mouse_x = input.mouse_x; menu_mouse_y = input.mouse_y; if (input.menu_move) menu.chosen = (menu.chosen + input.menu_move + menu.count) % menu.count; if (menu.screen == SCREEN_NAME) { /* typing the name */ typeName(menu.name, &input); snprintf(menu.item[0], UI_TEXT_MAX, "%s_", menu.name); } int pick = input.enter ? menu.chosen : (input.click && hit >= 0 ? hit : -1); if (menu.screen == SCREEN_NAME && input.enter) pick = 1; /* (Enter: save it) */ if (pick >= 0 && pick < menu.count) { act = menu.act[pick]; arg = menu.arg[pick]; } } char err[160] = ""; switch (act) { case ACT_NEW: case ACT_RESUME: menu.screen = SCREEN_NONE; menu.back = SCREEN_NONE; Platform_captureMouse(&platform, true); look_settle = true; break; case ACT_CONTINUE: case ACT_LOAD: if (Save_read(&game, menu.dir, menu.saves[arg].name, err, sizeof err)) { snprintf(note, sizeof note, "Loaded %s", menu.saves[arg].name); menu.screen = SCREEN_NONE; menu.back = SCREEN_NONE; Platform_captureMouse(&platform, true); look_settle = true; autosave_t = 0.0f; } else snprintf(note, sizeof note, "Not loaded: %.80s", err); note_time = 3.0f; break; case ACT_LOAD_LIST: menu.back = menu.screen; menuOpen(&menu, SCREEN_LOAD); break; case ACT_SAVE_AS: if (Save_blocked(&game)) { snprintf(note, sizeof note, "%s", Save_blocked(&game)); note_time = 3.0f; break; } menu.back = SCREEN_PAUSE; menu.name[0] = '\0'; menuOpen(&menu, SCREEN_NAME); Platform_typing(&platform, true); break; case ACT_SAVE_NAMED: if (!menu.name[0]) break; if (Save_write(&game, menu.dir, menu.name, err, sizeof err)) snprintf(note, sizeof note, "Saved as %s", menu.name); else snprintf(note, sizeof note, "Not saved: %.80s", err); note_time = 3.0f; Platform_typing(&platform, false); menuOpen(&menu, SCREEN_PAUSE); menu.back = SCREEN_NONE; break; case ACT_QUICK_SAVE: if (Save_write(&game, menu.dir, QUICK_SAVE, err, sizeof err)) snprintf(note, sizeof note, "Quick saved"); else snprintf(note, sizeof note, "Not saved: %.80s", err); note_time = 3.0f; break; case ACT_KEYS: game.show_keys = !game.show_keys; break; case ACT_BACK: if (menu.screen == SCREEN_NAME) Platform_typing(&platform, false); menuOpen(&menu, menu.back == SCREEN_NONE ? SCREEN_PAUSE : menu.back); menu.back = SCREEN_NONE; break; case ACT_QUIT: if (menu.screen == SCREEN_PAUSE) Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err); goto done; case ACT_NONE: case ACT_NAME: break; } if (menu.screen == SCREEN_NONE) { if (look_settle) { /* (the first mouse move after the menu is a jump: ignored) */ input.look_yaw = input.look_pitch = 0.0f; look_settle = false; } Game_update(&game, &input, dt, (float)width / (float)height); if (input.mute) { /* M: sound off / on */ Audio_mute(!Audio_muted()); snprintf(note, sizeof note, Audio_muted() ? "Sound off (M)" : "Sound on"); note_time = 2.0f; } if ((autosave_t += dt) >= AUTOSAVE_EVERY && !Save_blocked(&game)) { /* (every few minutes, quietly) */ autosave_t = 0.0f; if (Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err)) { snprintf(note, sizeof note, "Autosaved"); note_time = 2.0f; } } } Sound_update(&game, dt, menu.screen != SCREEN_NONE); Renderer_beginFrame(&renderer, width, height, anim_time); Game_render(&game, &renderer); Skybox_draw(&skybox, &renderer, &game.camera, &game.daycycle.light); Game_renderAir(&game, &renderer); /* (rain and smoke against the sky too) */ Renderer_endFrame(&renderer); char clock_text[UI_TEXT_MAX]; DayCycle_format(&game.daycycle, clock_text, sizeof(clock_text)); Ui_drawText(&ui, 0, clock_text, UI_TOP_RIGHT, width, height); Ui_setTime(&ui, anim_time); char needs_text[UI_TEXT_MAX]; Game_needsText(&game, needs_text, sizeof(needs_text)); if (game.show_stats) drawStats(&ui, &game, width, height); else if (needs_text[0]) Ui_drawTextLine(&ui, NEEDS_LABEL, needs_text, 1, UI_TOP_RIGHT, width, height); if (game.show_dev) drawDev(&ui, &game, width, height); if (game.show_crew) drawCrew(&ui, &game, width, height); else if (game.show_notes) drawNotes(&ui, &game, width, height); if (game.show_pockets) drawPockets(&ui, &game, width, height); if (game.show_keys) for (int k = 0; k < KEY_COUNT; k++) Ui_drawTextLine(&ui, KEYS_LABEL + k, KEYS[k], k, UI_TOP_LEFT, width, height); else Ui_drawText(&ui, 1, CONTROLS_HELP, UI_TOP_LEFT, width, height); const char *status = Game_status(&game); if (status) Ui_drawText(&ui, 2, status, UI_CENTER, width, height); /* Who says what, and what you can answer */ Bubble bubbles[BUBBLE_MAX]; int said = Game_bubbles(&game, bubbles, BUBBLE_MAX, width, height); for (int b = 0; b < said; b++) Ui_drawTextAt(&ui, BUBBLE_LABEL + b, bubbles[b].text, bubbles[b].x, bubbles[b].y, width, height); const char *choices[TALK_CHOICES]; int answers = Game_choices(&game, choices); for (int c = 0; c < answers; c++) { char line[UI_TEXT_MAX]; snprintf(line, sizeof(line), "%d %s", c + 1, choices[c]); Ui_drawTextLine(&ui, CHOICE_LABEL + c, line, answers - 1 - c, UI_BOTTOM_LEFT, width, height); } const char *hint = Game_hint(&game); if (hint) Ui_drawTextLine(&ui, HINT_LABEL, hint, answers, UI_BOTTOM_LEFT, width, height); if (menu.screen != SCREEN_NONE) drawMenu(&ui, &menu, note_time > 0.0f ? note : NULL, width, height); else if (note_time > 0.0f) Ui_drawText(&ui, MENU_LABEL + 1 + MENU_ITEMS, note, UI_CENTER, width, height); Platform_swap(&platform); } done: exit_code = 0; cleanup: Audio_shutdown(); if (game_ready) Game_shutdown(&game); if (platform.gl_context) { Ui_shutdown(&ui); Skybox_shutdown(&skybox); Renderer_shutdown(&renderer); } Platform_shutdown(&platform); return exit_code; }