Entropie git · main
Entropie festival game; first steps
C 88.5% Markdown 9.1% GLSL 2.3%git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gzentropie/src/main.c raw
/*
* 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 <math.h>
#include <stdio.h>
#include <string.h>
#include <SDL3/SDL.h>
#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;
}