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/game/game.c raw
/*
* The game: one frame of camera, time, talk and drawing.
* What the keys and clicks do lives in game_actions.c.
*/
#include "game/game_internal.h"
#include "world/clothes.h"
#include "world/car.h"
#include "people/condition.h"
#define START_HOUR 13.0f
#define ARRIVE_BY_CAR 1 /* (testing) 0: start standing in the yard, no car */
#define START_AT_COURSE 0 /* (testing) 1: start at the obstacle course at the chicken farm; 0: in the yard (the course is there either way) */
#define TIME_SCALE 3.0f /* game seconds per real second */
#define CAMERA_FOV 70.0f
#define WORN_OUT 0.12f /* energy below this: "worn out" ... */
#define WORN_OUT_UNTIL 0.3f /* ... until it is back above this */
#define SHADOW_FOCUS_AHEAD 15.0f /* center shadows a bit ahead of the camera */
#define GRAB_LOOK_LIMIT 75.0f /* degrees you can look away from a grabbed object */
#define THIRD_PERSON_DISTANCE 3.2f
#define THIRD_PERSON_SHOULDER 0.45f
#define SLEEP_FAST_FORWARD 60.0f /* game time runs this much faster while you sleep holding Space */
/* The camera never goes into a wall: what's between the head and where it
would be pulls it in (at once), and it eases back out when there's room. */
#define CAMERA_CLEAR 0.25f /* m kept off what it would go into */
#define CAMERA_OUT_RATE 2.5f /* m/s back out */
#define CAMERA_FAN 0.15f /* m: rays round the middle one (the view's corners) */
static bool cameraSkips(const Object *o, const void *ctx) {
(void)ctx;
if (o->is_character || o->is_held || o->type == OBJ_DOG) return true;
if (!o->is_static) return true; /* (loose things don't shove the view about) */
return Object_reach(o) < 0.35f; /* (nor poles and posts) */
}
static float clearDistance(const World *world, Vec3 from, Vec3 dir, float want, Vec3 right, Vec3 up) {
float best = want;
const Vec3 fan[5] = { vec3_zero(), vec3_scale(right, CAMERA_FAN), vec3_scale(right, -CAMERA_FAN),
vec3_scale(up, CAMERA_FAN), vec3_scale(up, -CAMERA_FAN) };
for (int k = 0; k < 5; k++) {
float t = World_rayCast(world, vec3_add(from, fan[k]), dir, want + CAMERA_CLEAR, cameraSkips, NULL, NULL, NULL);
if (t >= 0.0f) best = fminf(best, t - CAMERA_CLEAR);
}
/* (the ground along the way) */
for (int k = 1; k <= 4; k++) {
float d = best * (float)k / 4.0f;
Vec3 at = vec3_add(from, vec3_scale(dir, d));
if (at.y < World_groundHeight(world, at.x, at.z, from.y) + CAMERA_CLEAR) { best = fmaxf(0.0f, d - CAMERA_CLEAR); break; }
}
return fmaxf(best, 0.0f);
}
static void followPlayer(Game *game, float dt) {
Camera *cam = &game->camera;
const Object *p = game->player;
Vec3 eye = vec3_add(p->position, vec3(0, PLAYER_EYE_HEIGHT, 0));
if (p->riding) { cam->position = Car_eye(p->riding, p->ride_seat); return; } /* (in the car: from the seat) */
if (Body_isDown(&p->body)) {
/* On the ground: see from the head, wherever it is */
Vec3 facing = Object_facing(p);
Vec3 head = Figure_toWorld(p->position, facing, p->skeleton.joints[JOINT_HEAD]);
Vec3 face = Figure_toWorld(vec3_zero(), facing, p->skeleton.head_forward);
eye = vec3_add(head, vec3_add(vec3_scale(face, 0.1f), vec3(0.0f, 0.05f, 0.0f)));
}
if (game->third_person) {
/* Over the right shoulder, behind the player: the boom pulled in by walls */
Vec3 want = vec3_add(vec3_add(vec3_scale(cam->front, -THIRD_PERSON_DISTANCE), vec3_scale(cam->right, THIRD_PERSON_SHOULDER)),
vec3(0.0f, 0.25f, 0.0f));
float len = vec3_length(want);
Vec3 dir = vec3_scale(want, 1.0f / len);
Vec3 up = vec3_normalize(vec3_cross(cam->right, dir));
float room = clearDistance(&game->world, eye, dir, len, cam->right, up);
if (game->camera_reach <= 0.0f || room < game->camera_reach) game->camera_reach = room;
else game->camera_reach = fminf(room, game->camera_reach + CAMERA_OUT_RATE * dt);
eye = vec3_add(eye, vec3_scale(dir, game->camera_reach));
}
cam->position = eye;
}
/* Holding on to something (F): looking left or right turns it that way (you and
your helpers walk round with it), as fast as you're able to. Ctrl: look
around freely; let go of Ctrl and the view swings back to it. */
#define GRAB_TURN_FREE 12.0f /* degrees you can look aside without turning it */
#define GRAB_LOOK_BACK 300.0f /* degrees/s the view swings back after looking around */
static void grabLook(Game *game, const Input *input, float dt) {
static bool looked_away = false;
if (!Hands_isGrabbing(game->player)) { looked_away = false; return; }
Vec3 h = game->player->hands.grab_heading;
float heading = atan2f(h.z, h.x) * 180.0f / PI_F;
float away = fmodf(game->camera.yaw - heading + 540.0f, 360.0f) - 180.0f;
if (input->free_look) { looked_away = true; return; }
if (looked_away) { /* back to it first, no turning meanwhile */
float back = GRAB_LOOK_BACK * dt;
if (fabsf(away) <= GRAB_TURN_FREE + back) looked_away = false;
away -= clampf(away, -back, back);
} else if (fabsf(away) > GRAB_TURN_FREE) {
float want = (away - (away > 0.0f ? GRAB_TURN_FREE : -GRAB_TURN_FREE)) * PI_F / 180.0f;
float turned = Hands_turnGrabbed(game->player, game->world.objects, game->world.count, want, dt);
heading += turned * 180.0f / PI_F;
away -= turned * 180.0f / PI_F;
}
game->camera.yaw = heading + clampf(away, -GRAB_LOOK_LIMIT, GRAB_LOOK_LIMIT);
Camera_rotate(&game->camera, 0.0f, 0.0f);
}
/* Drunk: the view sways and tilts, a stumble jolts it. */
static void swayView(Game *game) {
const Object *p = game->player;
Camera *cam = &game->camera;
float d = fminf(p->drunk, 1.0f);
float t = p->body.clock;
float jolt = p->body.stumble > 0.0f ? sinf(p->body.stumble * 9.0f) * p->body.stumble : 0.0f;
float hit = p->work_state.impact / 0.18f; /* the tool bites into the wood */
cam->sway_yaw = d * 4.0f * sinf(t * 0.55f) + jolt * 3.0f;
cam->sway_pitch = d * 2.5f * sinf(t * 0.83f + 1.0f) - fabsf(jolt) * 4.0f - 1.2f * hit * hit;
cam->roll = d * 6.0f * sinf(t * 0.41f + 2.0f) + jolt * 5.0f + 0.8f * hit * hit;
}
const char *Game_status(const Game *game) {
const Body *b = &game->player->body;
if (b->asleep && b->pose == POSE_DOWN)
return game->fast_sleep ? "z z z >>>" : "z z z (hold Space to sleep faster)";
if (b->vomit > 0.0f) return "*bleargh*";
if (b->pee > 0.0f) return game->player->needs.bladder >= 1.0f ? "Oops... couldn't hold it"
: b->pee_power > 0.5f ? "*PSSSHHHH*" : "*psssh* (Shift: further - aim with the mouse)";
if (b->burp > 0.0f) return "*burp*";
if (game->message_time > 0.0f) return game->message;
return NULL;
}
/* Word for how bad a need is: nothing, then mild, then strong. */
static const char *needWord(float value, const char *mild, const char *strong) {
if (value >= 0.85f) return strong;
if (value >= 0.6f) return mild;
return NULL;
}
void Game_needsText(const Game *game, char *out, size_t size) {
const Needs *n = &game->player->needs;
const char *words[] = {
game->worn_out ? "worn out" : NULL,
needWord(n->hunger, NULL, "starving"),
needWord(n->thirst, NULL, "parched"),
needWord(n->sleepiness, NULL, "exhausted"),
needWord(n->bladder, NULL, "bursting"),
game->player->smoker > 0.0f && game->player->craving > 0.85f && !Body_isSmoking(&game->player->body) ? "dying for a smoke" : NULL,
};
int k = 0;
if (size > 0) out[0] = '\0';
for (unsigned i = 0; i < sizeof(words) / sizeof(words[0]); i++)
if (words[i] && k >= 0 && (size_t)k < size) k += snprintf(out + k, size - (size_t)k, "%s%s", k ? " " : "", words[i]);
}
/* You, working hard in the midday sun: off with the shirt after a while (a top
if you're a woman), on again when you've cooled down (as the others do). */
static void sunShirt(Game *game, float dt) {
Object *p = game->player;
float hour = game->daycycle.seconds / 3600.0f;
bool sunny = hour >= 11.0f && hour < 17.5f, working = p->work_state.since < 3.0f;
float warm = sunny && working ? 1.0f + 0.5f * (1.0f - p->needs.stamina) : sunny ? -0.5f : -3.0f;
p->ai.sun_heat = clampf(p->ai.sun_heat + warm * dt, 0.0f, 135.0f);
if (p->look.top == TOP_SHIRT && p->ai.sun_heat >= 90.0f) {
p->look.top = p->female ? TOP_CROP : TOP_BARE;
say(game, "Hot work - you take your shirt off");
} else if ((p->look.top == TOP_BARE || p->look.top == TOP_CROP) && p->ai.sun_heat < 40.0f) {
p->look.top = TOP_SHIRT;
say(game, "You put your shirt back on");
}
}
int Game_stats(const Game *game, StatBar *bars, int max, const char **title, char *about, size_t size) {
const Object *p = game->player;
const Needs *n = &p->needs;
const StatBar body[] = {
{ "Energy", n->stamina, false },
{ "Hunger", n->hunger, true },
{ "Thirst", n->thirst, true },
{ "Bladder", n->bladder, true },
{ "Sleepy", n->sleepiness, true },
{ "Worn (days)", p->worn, true },
{ "Drunk", fminf(p->drunk, 1.0f), true },
{ "Dirty", p->dirt, true },
{ "Wet", p->look.wet, true },
{ "Sporty", p->fitness, false },
};
const StatBar skills[] = { /* grow with practice */
{ "Firewood", p->skill[ORDER_FIREWOOD], false },
{ "Felling", p->skill[ORDER_TREES], false },
{ "Tidying", p->skill[ORDER_TRASH], false },
{ "Fire", p->skill[ORDER_FIRE], false },
{ "Ladder (one hand)", p->ladder_skill, false },
};
const bool skill_page = game->stats_page == 1;
const StatBar *all = skill_page ? skills : body;
const int total = skill_page ? (int)(sizeof(skills) / sizeof(skills[0])) : (int)(sizeof(body) / sizeof(body[0]));
int count = 0;
for (int i = 0; i < total && count < max; i++) bars[count++] = all[i];
*title = skill_page ? "Skills" : "Body";
if (skill_page) {
char lighter[24] = "no lighter";
if (p->lighter) snprintf(lighter, sizeof lighter, p->lighter_uses > 0 ? "lighter (%d)" : "empty lighter", p->lighter_uses);
snprintf(about, size, "Pockets: %d cigarettes, tobacco for %d, %d papers, %d filters, %s",
p->cigarettes, p->tobacco, p->papers, p->filters, lighter);
} else {
float jump = Object_jumpSpeed(p);
snprintf(about, size, "Sprint %.1f m/s Jump %d cm %d kg %s-handed",
Object_topSpeed(p), (int)(jump * jump / (2.0f * 9.81f) * 100.0f + 0.5f), (int)(p->weight + 0.5f),
p->main_hand == HAND_LEFT ? "left" : "right");
}
return count;
}
bool Game_init(Game *game) { return Game_initWorld(game, true); }
bool Game_initWorld(Game *game, bool arrive) {
game->dark = 0.0f;
game->show_keys = false;
game->fast_sleep = false;
game->message = NULL;
game->message_time = 0.0f;
game->third_person = true;
game->camera_reach = 0.0f;
game->holding = false;
game->click_action = 0;
game->info[0] = '\0';
game->info_about = NULL;
memset(&game->talk, 0, sizeof(game->talk));
World_init(&game->world);
DayCycle_init(&game->daycycle, START_HOUR, TIME_SCALE);
Camera_init(&game->camera, vec3(0.0f, 1.8f, 5.0f), -90.0f, 0.0f, CAMERA_FOV);
game->player = Scene_load(&game->world);
if (!game->player || !World_buildTerrain(&game->world) || !Site_finish(&game->world) || !Scene_finish(&game->world)) {
SDL_Log("Scene loading failed");
Game_shutdown(game);
return false;
}
/* Start looking at your building (or down the obstacle course) */
Vec3 to = vec3_sub(Site_landmark(), game->player->position);
float look = atan2f(to.z, to.x);
if (!Course_build(&game->world, START_AT_COURSE ? game->player : NULL, &look)) {
Game_shutdown(game);
return false;
}
Camera_init(&game->camera, game->camera.position, look * 180.0f / PI_F, 0.0f, CAMERA_FOV);
{ /* two umbrellas: by the main building's door and in your garages' kitchen (last: the scene's own come first) */
Vec3 at[2] = { vec3_add(Site_houseDoor(), vec3(0.9f, 0.0f, 0.4f)), vec3_add(Site_drinksSpot(), vec3(-0.8f, 0.0f, 0.6f)) };
for (int k = 0; k < 2; k++) {
at[k].y = World_groundHeight(&game->world, at[k].x, at[k].z, at[k].y + 3.0f) + 0.06f;
Object *u = Object_create(OBJ_UMBRELLA, PALETTE_WARM, at[k], vec3_zero(), (float)k);
if (!u) continue;
Object_setRotation(u, vec3(PI_F / 2.0f, 0.4f + 1.3f * (float)k, 0.0f)); /* (lying there) */
if (!World_add(&game->world, u)) continue;
}
}
game->marker = Shapes_marker(1.8f, 0.8f);
game->quest_mesh = Shapes_questMarker(0.7f, vec3(1.0f, 0.82f, 0.15f));
for (int i = 0; i < game->world.count; i++) { /* (what the scene made: a save finds it again by this) */
Object *o = game->world.objects[i];
o->scene_id = i + 1;
o->spawn = o->position;
o->spawn_type = o->type;
}
memset(&game->arrival, 0, sizeof(game->arrival));
if (arrive && ARRIVE_BY_CAR && !START_AT_COURSE) game_meetingStart(game); /* (the story starts in Berlin; then the drive in) */
followPlayer(game, 0.0f);
return true;
}
void Game_shutdown(Game *game) {
Meadow_destroy(&game->meadow);
World_destroy(&game->world);
Mesh_destroy(&game->marker);
Mesh_destroy(&game->quest_mesh);
Dog_shutdown();
game->player = NULL;
}
/* Putting a ladder up: you stand still, both hands on it, where it passes your chest. */
static void puttingUp(Game *game) {
Object *p = game->player, *l = game->putting_up;
if (!l) return;
if (l->placing <= 0.0f) { game->putting_up = NULL; return; }
p->movement = p->move_local = vec3_zero();
Vec3 axis = l->colliders[0].axes[1];
float len = 2.0f * l->colliders[0].half_extents.y;
Vec3 chest = vec3_add(p->position, vec3(0.0f, 1.15f * p->look.height / 1.75f, 0.0f));
float along = clampf(vec3_dot(vec3_sub(chest, l->position), axis), 0.3f, fmaxf(0.3f, len - 0.2f));
Vec3 grip = vec3_add(l->position, vec3_scale(axis, along));
Vec3 across = l->colliders[0].axes[0];
if (vec3_dot(across, vec3_cross(Object_facing(p), vec3(0.0f, 1.0f, 0.0f))) < 0.0f) across = vec3_scale(across, -1.0f);
Hands_holdEdge(p, grip, across, 0.0f);
}
/* Rain: the light duller, the sky grey, no sharp shadows; everything wet. */
static void rainLight(Game *game) {
Lighting *l = &game->daycycle.light;
float r = game->world.rain;
const Vec3 grey = { 0.56f, 0.58f, 0.62f };
float bright = 0.35f + 0.65f * l->day_factor;
l->light_color = vec3_scale(l->light_color, 1.0f - 0.45f * r);
l->sky_top = vec3_lerp(l->sky_top, vec3_scale(grey, 0.8f * bright), fminf(1.0f, 1.3f * r));
l->sky_horizon = vec3_lerp(l->sky_horizon, vec3_scale(grey, bright), fminf(1.0f, 1.2f * r));
l->shadow_strength *= 1.0f - 0.85f * r;
float f = game->world.flash; /* (lightning: everything lit up white for a moment) */
l->light_color = vec3_add(l->light_color, vec3_scale(vec3(1.4f, 1.45f, 1.6f), f));
l->sky_top = vec3_lerp(l->sky_top, vec3(0.9f, 0.92f, 1.0f), 0.7f * f);
l->sky_horizon = vec3_lerp(l->sky_horizon, vec3(0.95f, 0.95f, 1.0f), 0.7f * f);
l->wet = game->world.wet;
/* clouding over: before the rain comes (and staying a while after), with it fully */
float coming = clampf(game->world.rain_goal, 0.0f, 1.0f);
game->overcast += (fminf(1.0f, fmaxf(1.6f * r, 0.7f * coming)) - game->overcast) * fminf(1.0f, game->world.game_dt / 600.0f);
l->overcast = fmaxf(game->overcast, fminf(1.0f, 1.6f * r));
}
/* Worn out with something heavy in your arms (a crate): down it goes for a moment. */
static void tooHeavy(Game *game) {
Object *p = game->player;
if (p->needs.stamina > 0.06f || Condition_load(p) < 0.15f) return;
for (int h = 0; h < 2; h++) if (p->hands.state[h] == HAND_HOLDING) { Hands_drop(p, h); break; }
say(game, "Too heavy - you have to put it down for a moment (or get somebody to take the other side: F, then E)");
}
void Game_update(Game *game, const Input *input, float dt, float aspect) {
/* Asleep: hold Space and the hours fly by */
const Body *pb = &game->player->body;
game->fast_sleep = pb->asleep && pb->pose == POSE_DOWN && input->jump;
const float speed = game->fast_sleep ? SLEEP_FAST_FORWARD : 1.0f;
DayCycle_update(&game->daycycle, dt * speed);
rainLight(game);
{ /* worn out: shown until a bit rested again */
float stamina = game->player->needs.stamina;
if (stamina < WORN_OUT) game->worn_out = true;
else if (stamina > WORN_OUT_UNTIL) game->worn_out = false;
}
Camera_rotate(&game->camera, input->look_yaw, input->look_pitch);
game->dt = dt;
game_handleActions(game, input);
game_talk(game, input, dt);
grabLook(game, input, dt);
Player_update(game->player, input, &game->camera, dt);
puttingUp(game);
game_climbLadder(game, input, dt);
game_climb(game, input, dt);
game_touch(game, dt); /* (after your move: holding on to something keeps you near it) */
game->world.eye = game->camera.position;
if (game->world.rain > 0.3f && !game->rain_told) {
game->rain_told = true;
say(game, "It's starting to rain - under a roof (the house, your garages) or into your tent");
} else if (game->world.rain < 0.05f) game->rain_told = false;
if (game->world.thunder) { /* (the thunder: said, and somebody near reacts) */
static const char *const ROLL[] = { "*KRRRAKK* - thunder, close by", "*rumble* - thunder rolls over the farm", "*BOOM* - that one was close" };
say(game, ROLL[(int)(game->daycycle.seconds) % 3]);
for (int i = 0; i < game->world.count; i++) {
Object *o = game->world.objects[i];
if (o->type != OBJ_HUMAN || o->body.asleep || o->indoors > 0.0f || o->ai.speech_time > 0.0f || ((int)(game->daycycle.seconds) + i) % 4) continue;
Ai_say(o, (i % 2) ? "Whoa! That was close." : "Did you see that one?");
break;
}
}
game->blow_told = fmaxf(0.0f, game->blow_told - dt);
if (game->world.tent_blowing && game->blow_told <= 0.0f) { /* (a tent not pegged down, the wind pushing it about) */
const Object *t = game->world.tent_blowing, *o = t->owner;
static char line[96];
snprintf(line, sizeof(line), "%s%s tent is blowing about in the wind - peg it down!", !o ? "A" : o == game->player ? "Your" : o->name, o && o != game->player ? "'s" : "");
say(game, line);
game->blow_told = 60.0f;
}
if (game->player->look.wet > 0.6f && !game->wet_told) {
game->wet_told = true;
say(game, "You're wet through - by a fire you dry quickest");
} else if (game->player->look.wet < 0.2f) game->wet_told = false;
game_arrival(game, dt);
game_meeting(game, input, dt);
if (!game->meeting.kind && !Game_inBerlin(game->player)) game_story(game, game->world.game_dt); /* (the hours on the ground: on the ground) */
game_shop(game, dt);
game->quest_t += dt;
if (!game->meeting.kind) game_why(game, dt);
game_festival(game, dt);
game_cleanup(game, dt);
if (!game->meeting.kind) { game_bigTree(game, dt); game_injury(game, dt); }
Ai_setSettled(game->arrival.task == TASK_NONE);
tooHeavy(game);
bool warm = game->player->look.top == TOP_JACKET;
World_update(&game->world, dt, dt * game->daycycle.time_scale * speed);
Object *you = game->player;
if (!warm && you->look.top == TOP_JACKET) {
say(game, "Brr, it's getting cold - you pull your pullover on (B: off again)");
} else if (you->chill > 0.6f && you->pullover_down && you->look.top != TOP_JACKET && !you->jacket_told) {
static char line[96];
you->jacket_told = true;
snprintf(line, sizeof(line), "Brr, it's getting cold - your pullover is %s", Clothes_where(you));
say(game, line);
}
if (game->player->body.stepped_off) {
game->player->body.stepped_off = false;
say(game, "Whoa - you lose your balance and step off");
}
for (int i = 0; i < game->world.count; i++) { /* a ladder that fell against something near you */
Object *l = game->world.objects[i];
if (l->type != OBJ_LADDER || !l->just_caught) continue;
l->just_caught = false;
if (vec3_length(vec3_sub(l->position, game->player->position)) < 15.0f)
say(game, l->lean_top > 0.0f ? "It falls against it and rests there - not too steady (G: somebody holds it)"
: "It falls against it and stays leaning there - not too steady");
}
if (game->player->carry_blocked) { /* what you carry hit something: the view doesn't turn on either */
game->camera.yaw = (game->player->rotation.y - PI_F / 2.0f) * 180.0f / PI_F;
Camera_rotate(&game->camera, 0.0f, 0.0f);
}
Object *grilled = Camp_update(game->world.objects, game->world.count, dt, dt * game->daycycle.time_scale * speed);
if (grilled && (grilled->on_grill || grilled == game->player->hands.item[0] || grilled == game->player->hands.item[1]))
tell(game, grilled);
game->bag_timer = fmaxf(0.0f, game->bag_timer - dt);
game_holdHose(game);
if (camp_doused) {
camp_doused = false;
say(game, "Pssssshhh... the fire is out");
}
Ai_setHour(game->daycycle.seconds / 3600.0f);
game->world.hour = game->daycycle.seconds / 3600.0f;
sunShirt(game, dt);
if (game->player->on_fire > 0.0f && (game->fire_nag -= dt) <= 0.0f) {
say(game, "You're on fire!");
game->fire_nag = 4.0f;
}
game->message_time = fmaxf(0.0f, game->message_time - dt);
/* Your own nose */
float stink = Object_stinkAt(game->world.objects, game->world.count, game->player->position);
game->sniff_time = fmaxf(0.0f, game->sniff_time - dt);
if (stink > 0.15f) {
game->player->body.disgust = fmaxf(game->player->body.disgust, fminf(1.0f, stink * 1.5f));
if (game->sniff_time <= 0.0f && game->message_time <= 0.0f) {
say(game, stink > 0.6f ? "Ugh, it really stinks here" : "Something smells...");
game->sniff_time = 20.0f;
}
}
/* Falling asleep: the world fades to dark, and back when waking up */
const Body *b = &game->player->body;
float dark_goal = b->asleep && b->pose != POSE_UP ? 1.0f : 0.0f;
game->dark += (dark_goal - game->dark) * fminf(1.0f, 0.8f * dt);
followPlayer(game, dt);
swayView(game);
Camera_updateMatrices(&game->camera, aspect);
}
/* World point -> screen pixels (y up from the bottom); false if behind the camera. */
static bool toScreen(const Camera *cam, Vec3 p, int w, int h, float *x, float *y) {
Mat4 m = mat4_mul(cam->projection, cam->view);
float cx = m.m[0] * p.x + m.m[4] * p.y + m.m[8] * p.z + m.m[12];
float cy = m.m[1] * p.x + m.m[5] * p.y + m.m[9] * p.z + m.m[13];
float cw = m.m[3] * p.x + m.m[7] * p.y + m.m[11] * p.z + m.m[15];
if (cw < 0.1f) return false;
*x = (cx / cw * 0.5f + 0.5f) * (float)w;
*y = (cy / cw * 0.5f + 0.5f) * (float)h;
return *x > -200.0f && *x < (float)w + 200.0f && *y > -50.0f && *y < (float)h + 50.0f;
}
static Vec3 aboveHead(const Object *o) {
Vec3 head = Figure_toWorld(o->position, Object_facing(o), o->skeleton.joints[JOINT_HEAD]);
return vec3_add(head, vec3(0.0f, 0.35f, 0.0f));
}
int Game_bubbles(const Game *game, Bubble *out, int max, int screen_w, int screen_h) {
int n = 0;
for (int i = 0; i < game->world.count && n < max; i++) {
const Object *o = game->world.objects[i];
if ((o->type != OBJ_HUMAN && o->type != OBJ_DOG) || o->ai.speech_time <= 0.0f) continue;
Bubble *b = &out[n];
if (!toScreen(&game->camera, aboveHead(o), screen_w, screen_h, &b->x, &b->y)) continue;
snprintf(b->text, sizeof(b->text), "%s: %s", o->name, o->ai.speech);
n++;
}
/* The player's own answer: above the head, or low in the middle in first person */
const Talk *t = &game->talk;
if (t->player_time > 0.0f && n < max) {
Bubble *b = &out[n];
bool shown = game->third_person &&
toScreen(&game->camera, aboveHead(game->player), screen_w, screen_h, &b->x, &b->y);
if (!shown) {
b->x = (float)screen_w * 0.5f;
b->y = (float)screen_h * 0.2f;
}
snprintf(b->text, sizeof(b->text), "You: %s", t->player_line);
n++;
}
return n;
}
void Game_renderAir(Game *game, Renderer *renderer) {
Renderer_beginAirPass(renderer);
World_drawAir(&game->world, renderer);
}
void Game_render(Game *game, Renderer *renderer) {
/* A campfire lights its surroundings, the more the darker it is */
Lighting lit = game->daycycle.light;
float fire = Camp_firelight(game->world.objects, game->world.count, &lit.fire_position);
lit.fire_strength = fire * (0.25f + 1.6f * (1.0f - lit.day_factor));
const Lighting *light = &lit;
const Camera *cam = &game->camera;
Vec3 ahead = vec3_normalize(vec3(cam->front.x, 0.0f, cam->front.z));
Vec3 focus = vec3_add(cam->position, vec3_scale(ahead, SHADOW_FOCUS_AHEAD));
Renderer_beginShadowPass(renderer, light, focus);
World_drawShadows(&game->world, renderer);
Renderer_endShadowPass(renderer);
renderer->drunk = fminf(game->player->drunk, 1.0f);
renderer->dark = fmaxf(game->dark * (game->third_person ? 0.55f : 0.92f), game->meeting.fade); /* eyes shut; going to Berlin and back */
Renderer_beginScenePass(renderer, &game->camera, light);
World_draw(&game->world, renderer, game->third_person && game->camera_reach > 0.6f); /* (pulled right in: from the head) */
Meadow_draw(&game->meadow, &game->world, renderer, game->camera.position);
/* Showing where the firewood goes: the spot, on the ground in front of you */
if (follower(game, SHOW_PLACE) && game->marker.vao) {
const Object *p = game->player;
Vec3 at = vec3_add(p->position, vec3_scale(Object_facing(p), TALK_PILE_AHEAD));
at.y = Terrain_height(&game->world.terrain, at.x, at.z) + 0.04f;
Mat4 model = mat4_mul(mat4_translate(at), mat4_rotate_y(p->rotation.y));
Renderer_drawDecal(renderer, &game->marker, model, TERRAIN_SIZE);
}
/* Who or what to go to next: a golden diamond over it, bobbing and turning */
Vec3 quest;
if (game->quest_mesh.vao && Game_questMarker(game, &quest)) {
float t = game->quest_t;
Mat4 model = mat4_mul(mat4_translate(vec3_add(quest, vec3(0.0f, 0.08f * sinf(t * 2.6f), 0.0f))), mat4_rotate_y(t * 1.8f));
Renderer_drawMeshTinted(renderer, &game->quest_mesh, model, vec3(1.0f, 1.0f, 1.0f), 0.55f);
}
}