/* * 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); } }