#include "world/world.h" #include "world/injury.h" #include "world/camp.h" #include "world/clothes.h" #include "world/car.h" #include "world/tent.h" #include "world/weather.h" #include "people/pockets.h" #include "people/condition.h" #include "people/dog.h" #include "world/stage.h" #include "world/forestry.h" #include "world/ladder.h" #include "world/site.h" #include "world/physics.h" #include #include #include #define BALANCE_NARROW 0.3f /* m: a top this narrow is all balancing ... */ #define BALANCE_WIDE 0.75f /* ... this wide: none */ #define TRAMPLE_MIN_SPEED 0.5f /* standing still doesn't wear the grass */ #define BREAK_IMPACT 2.0f /* thrown bottles break when stopped this hard (m/s) */ #define BREAK_SPEED 5.0f /* ... or when they land while still this fast */ #define SICK_RELIEF 0.12f /* being sick takes off this much drunkenness */ #define KNOCK_DOWN 60.0f /* momentum (kg m/s) that floors a person */ #define KNOCK_STAGGER 6.0f /* ... that makes them stagger */ #define RUN_INTO 2.6f /* m/s: sober, running into something this fast trips or staggers you */ #define RUN_OVER_HIGH 0.75f /* lower than this (scaled) you go over it, higher you bounce off */ #define RUN_AT_CLIMB 2.05f /* ... up to this high (the hands reach the edge) you catch it to climb it */ #define SCRAPE_DEPTH 0.04f /* a leg this far into something (scaled): a scrape */ #define SCRAPE_COOL 2.5f /* seconds before the next one counts */ /* Distance along a downward ray from origin to an oriented box, or -1 if missed (also when the ray starts inside the box). */ static float rayDownToBox(Vec3 origin, Vec3 center, const Collider *c) { return Collider_ray(c, center, origin, vec3(0.0f, -1.0f, 0.0f), NULL); } float World_rayCast(const World *world, Vec3 origin, Vec3 dir, float max, RaySkip skip, const void *ctx, Object **hit, Vec3 *normal) { float best = -1.0f; for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->collider_count == 0) continue; if (skip ? skip(o, ctx) : (o->is_character || o->is_held)) continue; /* (far off the ray: not worth the boxes) */ Vec3 rel = vec3_sub(o->position, origin); float along = clampf(vec3_dot(rel, dir), 0.0f, max), reach = Object_reach(o); Vec3 off = vec3_sub(rel, vec3_scale(dir, along)); if (vec3_dot(off, off) > reach * reach) continue; for (int c = 0; c < o->collider_count; c++) { Vec3 n; float t = Object_rayToCollider(o, c, origin, dir, &n); if (t >= 0.0f && t <= max && (best < 0.0f || t < best)) { best = t; if (hit) *hit = o; if (normal) *normal = n; } } } return best; } /* The same for a round thing lying (a log): its round top, not the box round it. axis: the collider axis it lies along (k), radius across. */ static float rayDownToRound(Vec3 origin, Vec3 center, const Collider *c, int k, float radius) { const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z }; Vec3 a = c->axes[k], w0 = vec3_sub(origin, center); Vec3 d = vec3_sub(vec3(0.0f, 1.0f, 0.0f), vec3_scale(a, a.y)); /* (how the ray's going down moves it across the axis) */ Vec3 p0 = vec3_sub(w0, vec3_scale(a, vec3_dot(w0, a))); float qa = vec3_dot(d, d), qb = -2.0f * vec3_dot(p0, d), qc = vec3_dot(p0, p0) - radius * radius; if (qa < 1e-6f) return rayDownToBox(origin, center, c); /* (standing upright) */ float disc = qb * qb - 4.0f * qa * qc; if (disc < 0.0f) return -1.0f; float t = (-qb - sqrtf(disc)) / (2.0f * qa); if (t < 0.0f) return -1.0f; float along = vec3_dot(vec3_sub(w0, vec3(0.0f, t, 0.0f)), a); return fabsf(along) <= half[k] ? t : -1.0f; } static bool roundLying(const Object *o, int *k) { if (o->type != OBJ_LOG && o->type != OBJ_CYLINDER) return false; const Collider *c = &o->colliders[0]; const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z }; int along = 0; for (int i = 1; i < 3; i++) if (half[i] > half[along]) along = i; if (fabsf(c->axes[along].y) > 0.7f) return false; /* (standing up: flat top) */ *k = along; return true; } /* Things whose footprint comes within r of (x, z): from the cell grid, or (not built for what's there now) all of them. n < 0: look at all. */ #define NEAR_MAX 512 static int nearby(const World *world, float x, float z, float r, int *buf) { return Grid_near((Grid *)&world->grid, world->count, x, z, r, buf, NEAR_MAX); } float World_groundHeight(const World *world, float x, float z, float from_y) { float best = Terrain_height(&world->terrain, x, z); Vec3 origin = vec3(x, from_y, z); int near[NEAR_MAX], nn = nearby(world, x, z, 0.0f, near); for (int q = 0; q < (nn < 0 ? world->count : nn); q++) { const Object *o = world->objects[nn < 0 ? q : near[q]]; if (o->is_character || o->is_held || o->type == OBJ_TENT) continue; /* (nobody stands on a tent: it'd give) */ for (int c = 0; c < o->collider_count; c++) { const Collider *col = &o->colliders[c]; Vec3 center = Object_colliderCenter(o, c); float reach = vec3_length(col->half_extents); /* nowhere near: skip */ if (fabsf(center.x - x) > reach || fabsf(center.z - z) > reach) continue; int along; float t = c == 0 && roundLying(o, &along) ? rayDownToRound(origin, center, col, along, o->size.x * 0.5f) : rayDownToBox(origin, center, col); if (t >= 0.0f) best = fmaxf(best, from_y - t); } } return best; } static float groundQuery(const void *ctx, float x, float z, float from_y) { return World_groundHeight((const World *)ctx, x, z, from_y); } /* What stands within radius of (x, z) higher than the feet: how much higher (feet keep off it; low things are stepped over). What you stand on doesn't count, nor do people. */ static float clutterQuery(const void *ctx, float x, float z, float radius, float feet_y) { const World *world = ctx; float most = 0.0f; int near[NEAR_MAX], nn = nearby(world, x, z, radius, near); for (int q = 0; q < (nn < 0 ? world->count : nn); q++) { const Object *o = world->objects[nn < 0 ? q : near[q]]; if (o->is_character || o->is_held || o->lifted || o->collider_count == 0 || o->type == OBJ_DOG || o->broken) continue; for (int k = 0; k < o->collider_count; k++) { Vec3 c = Object_colliderCenter(o, k); const Collider *col = &o->colliders[k]; float reach = radius + vec3_length(col->half_extents); if (fabsf(c.x - x) > reach || fabsf(c.z - z) > reach) continue; const float half[3] = { col->half_extents.x, col->half_extents.y, col->half_extents.z }; float top = c.y + fabsf(col->axes[0].y) * half[0] + fabsf(col->axes[1].y) * half[1] + fabsf(col->axes[2].y) * half[2]; float bottom = 2.0f * c.y - top; if (fmaxf(fabsf(col->axes[0].y), fmaxf(fabsf(col->axes[1].y), fabsf(col->axes[2].y))) < 0.98f) { /* tilted (a roof, a box on its edge): its highest corner may be far off - what counts is its surface right there, or the nearest bit of it */ Vec3 to = vec3(c.x - x, 0.0f, c.z - z); float len = vec3_length(to), surface = -1e9f; for (int sIdx = 0; sIdx < 3; sIdx++) { float along = len > 1e-4f ? fminf(radius * 0.5f * (float)sIdx, len) / len : 0.0f; Vec3 from = vec3(x + to.x * along, top + 1.0f, z + to.z * along); float t = rayDownToBox(from, c, col); if (t >= 0.0f) surface = fmaxf(surface, from.y - t); } if (surface < -1e8f) continue; top = surface; } if (top <= feet_y + 0.05f || bottom > feet_y + 1.0f) continue; /* under the feet (standing on it), or overhead */ /* distance from (x, z) to the collider's footprint */ Vec3 rel = vec3(x - c.x, 0.0f, z - c.z); float gap = 0.0f; for (int a = 0; a < 3; a++) { Vec3 axis = vec3(col->axes[a].x, 0.0f, col->axes[a].z); float len = vec3_length(axis); if (len < 0.3f) continue; /* (an upright axis) */ Vec3 dir = vec3_scale(axis, 1.0f / len); float extent = fabsf(vec3_dot(dir, col->axes[0])) * half[0] + fabsf(vec3_dot(dir, col->axes[1])) * half[1] + fabsf(vec3_dot(dir, col->axes[2])) * half[2]; float d = fabsf(vec3_dot(rel, dir)) - extent; /* (its outline seen from above) */ gap = fmaxf(gap, d); } if (gap > radius) continue; most = fmaxf(most, top - feet_y); } } return fminf(most, 0.5f); } /* How far p has to go along dir to be out of anything solid: fixed things and heavy loose ones (what a hand can't just push aside). */ static float pushOutQuery(const void *ctx, Vec3 p, Vec3 dir, float radius) { const World *world = ctx; float most = 0.0f; int near[NEAR_MAX], nn = nearby(world, p.x, p.z, radius, near); for (int q = 0; q < (nn < 0 ? world->count : nn); q++) { const Object *o = world->objects[nn < 0 ? q : near[q]]; if (o->is_character || o->is_held || o->lifted || o->collider_count == 0 || o->broken) continue; if (o->type == OBJ_DOG || o->type == OBJ_BUSH || o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK) continue; if (o->type == OBJ_LADDER) continue; /* (hands go between the rungs) */ if (!o->is_static && o->weight < 40.0f) continue; for (int c = 0; c < o->collider_count; c++) { const Collider *col = &o->colliders[c]; Vec3 rel = vec3_sub(p, Object_colliderCenter(o, c)); float reach = vec3_length(col->half_extents) + radius; if (fabsf(rel.x) > reach || fabsf(rel.y) > reach || fabsf(rel.z) > reach) continue; const float half[3] = { col->half_extents.x + radius, col->half_extents.y + radius, col->half_extents.z + radius }; bool inside = true; float out = 1e9f; for (int k = 0; k < 3 && inside; k++) { float at = vec3_dot(rel, col->axes[k]), v = vec3_dot(dir, col->axes[k]); if (fabsf(at) >= half[k]) inside = false; else if (fabsf(v) > 1e-4f) out = fminf(out, ((v > 0.0f ? half[k] : -half[k]) - at) / v); } if (inside && out < 1e8f) most = fmaxf(most, out); } } return most; } /* Dirt shows on the clothes. */ static FigurePose figurePose(const Object *o) { FigureLook look = o->look; const Vec3 mud = { 0.38f, 0.30f, 0.20f }; float d = clampf(o->dirt, 0.0f, 1.0f); look.pants = vec3_lerp(look.pants, mud, 0.55f * d); look.shoes = vec3_lerp(look.shoes, mud, 0.7f * d); look.shirt = vec3_lerp(look.shirt, mud, 0.3f * d); return (FigurePose){ .position = o->position, .facing = Object_facing(o), .look = look, .skeleton = o->skeleton, }; } #define SICK_DRIES (8.0f * 3600.0f) /* game seconds until sick has soaked away */ #define STINK_UNTIL 0.25f /* freshness below which it stops smelling */ #define PEE_STINK 0.6f /* smell of pee in the ground: radius */ static float terrainAt(const void *ctx, float x, float z) { return Terrain_height(&((const World *)ctx)->terrain, x, z); } /* Sick lands on the ground in front of the face (the grass under it flattened), and smells. */ static void spawnSick(World *world, const Object *o) { Vec3 facing = Object_facing(o); Vec3 head = Figure_toWorld(o->position, facing, o->skeleton.joints[JOINT_HEAD]); Vec3 at = vec3_add(head, vec3_scale(facing, 0.3f)); const float radius = 0.35f; ShapeGround ground = { .height = terrainAt, .ctx = world }; Terrain_soak(&world->terrain, at.x, at.z, radius * 1.3f); at.y = Terrain_height(&world->terrain, at.x, at.z); Object *sick = Object_createOnGround(OBJ_PUDDLE, PALETTE_STANDARD, at, vec3(radius, SICK_DRIES, 0.0f), ground); if (!sick || !World_add(world, sick)) return; Object *stink = Object_createOnGround(OBJ_STINK, PALETTE_WARM, at, vec3(radius, 0.0f, 0.0f), ground); if (stink && World_add(world, stink)) sick->stink = stink; } /* Done with a disposable Urinella: now and then it goes wrong (drunk, clumsy: on the shoes); then it's wrapped up and into the bag (the tidy), dropped where she stands (the careless) or flung away (drunk and wild). */ static float worldDice(World *world) { world->rng = world->rng * 1103515245u + 12345u; return (float)((world->rng >> 8) & 0xFFFFu) / 65536.0f; } static void usedUrinella(World *world, Object *o) { o->urinellas--; const float drunk = fminf(o->drunk, 1.0f), clumsy = Body_clumsiness(&o->body); const bool talks = o->type == OBJ_HUMAN; if (o->urinella_tried == 0 && o->type == OBJ_HUMAN) { /* the first one: she likes it, or not (the shy and the proud less) */ float like = 0.6f - 0.3f * fmaxf(-o->temperament.pride, 0.0f) - 0.2f * fmaxf(o->temperament.pride - 0.5f, 0.0f) + 0.2f * o->temperament.energy; o->urinella_tried = worldDice(world) < like ? 1 : -1; if (o->urinella_tried < 0) { o->urinella = false; /* (squatting again; the rest stays in the pocket) */ Ai_say(o, "Hm. Not for me - I'll stick to squatting."); return; } Ai_say(o, "Oh, that's handy!"); return; } if (worldDice(world) < 0.04f + 0.25f * drunk + 0.12f * clumsy) { o->dirt = fminf(1.0f, o->dirt + 0.12f); if (talks) Ai_say(o, "Oops... that went on my shoes."); } float tidy = 0.75f - 0.6f * drunk - 0.25f * clumsy + 0.1f * o->temperament.pride; float r = worldDice(world); if (r < tidy) { if (talks && o->urinellas == 0) Ai_say(o, "That was my last Urinella."); return; /* (wrapped up, into the bag: binned later) */ } Vec3 f = Object_facing(o); bool fling = r > tidy + (1.0f - tidy) * (0.7f - 0.5f * drunk); /* (the wilder, the likelier it flies) */ Vec3 at = vec3_add(o->position, vec3_add(vec3_scale(f, fling ? 0.3f : 0.15f), vec3(0.0f, fling ? 1.2f : 0.5f, 0.0f))); Object *used = Object_create(OBJ_TRASH, PALETTE_STANDARD, at, vec3_zero(), 0.03f); if (!used) return; snprintf(used->name, sizeof(used->name), "Used Urinella"); if (fling) used->velocity = vec3_add(vec3_scale(f, 3.0f + 2.0f * worldDice(world)), vec3(0.0f, 4.0f, 0.0f)); /* (up and away) */ if (!World_add(world, used)) { Object_destroy(used); return; } if (talks) Ai_say(o, fling ? "Wheee - away with it!" : o->urinellas == 0 ? "That was my last one." : "Whatever..."); } /* Done peeing (it went into the ground while it ran: see animateCharacter). Where most of it came down, the smell rises (one smell for a spot). In a toilet: nothing, and out again. */ static void spawnPee(World *world, Object *o) { o->needs.bladder = 0.0f; if (o->toilet) { Vec3 front = o->toilet->colliders[0].axes[2]; o->position = vec3_add(o->toilet->position, vec3_scale(vec3(front.x, 0.0f, front.z), o->toilet->size.z * 0.5f + 0.45f)); o->rotation.y = atan2f(front.z, front.x) + PI_F / 2.0f; Object_syncColliders(o); o->toilet->dog.mode = 0; World_noise(world, NOISE_FLUSH, o->toilet->position, 0.8f); o->toilet = NULL; return; } bool squat = o->body.squat_pee; if (!squat && o->female && o->urinella && o->urinellas > 0) usedUrinella(world, o); Vec3 at = squat || vec3_length(vec3_sub(o->pee_hit, o->position)) > 4.0f ? vec3_add(o->position, vec3_scale(Object_facing(o), squat ? -0.05f : 0.4f)) : o->pee_hit; o->dirt = fminf(1.0f, o->dirt + (squat ? 0.15f : 0.06f)); for (int i = 0; i < world->count; i++) { const Object *s = world->objects[i]; if (s->type == OBJ_STINK && s->variant == 1 && !s->broken && vec3_length(vec3(s->position.x - at.x, 0.0f, s->position.z - at.z)) < PEE_STINK * 1.5f) return; } ShapeGround ground = { .height = terrainAt, .ctx = world }; at.y = Terrain_height(&world->terrain, at.x, at.z); Object *stink = Object_createOnGround(OBJ_STINK, PALETTE_WARM, at, vec3(0.3f, 0.0f, 0.0f), ground); if (!stink) return; stink->variant = 1; /* as strong as the pee in the ground */ stink->work = Wetness_pee(&world->terrain.wet, at.x, at.z, PEE_STINK); if (!World_add(world, stink)) Object_destroy(stink); } /* Spraying (hose) or pouring (watering can): the arc of water from the hand, and what it does where it lands. */ #define HOSE_SPEED 7.0f #define CAN_SPEED 1.4f #define CAN_POUR 0.6f /* liters a second */ #define HOSE_FLOW 0.3f #define HOSE_SPREAD 0.25f /* the spray lands this wide (m) */ #define CAN_SPREAD 0.12f #define PEE_FLOW 0.15f /* against fires (pushing hard: 2.5 times that) */ #define PEE_LITERS 0.04f /* what really comes out a second: a full bladder is half a liter */ #define PEE_SPREAD 0.06f static void water(World *world, Object *o, float dt) { int hand = -1; for (int h = 0; h < 2; h++) if (o->hands.item[h] && (o->hands.item[h]->type == OBJ_HOSE || o->hands.item[h]->type == OBJ_CAN)) hand = h; if (hand < 0) { o->spraying = false; return; } Object *tool = o->hands.item[hand]; bool can = tool->type == OBJ_CAN; if (can && tool->water <= 0.0f) { o->spraying = false; return; } /* (a melted hose leaks on the way, a burnt-through one gives nothing at the nozzle) */ const float flow = can ? 1.0f : Hose_flow(world, tool, dt); if (flow < 0.02f) { o->skeleton.stream_kind = STREAM_NONE; return; } float liters = can ? 0.0f : HOSE_FLOW * flow * dt; if (can && o->pour >= 0.6f) { liters = fminf(tool->water, CAN_POUR * dt); tool->water -= liters; tool->weight -= liters; } Vec3 facing = Object_facing(o); Skeleton *sk = &o->skeleton; Vec3 aim = vec3_length(o->work_state.aim) > 0.1f ? o->work_state.aim : facing; Vec3 dir = Figure_toLocal(vec3_zero(), facing, aim); Vec3 start = vec3_add(sk->joints[hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R], vec3_scale(dir, 0.12f)); if (can) { /* out of the spout, once the can is tipped far enough */ if (o->pour < 0.6f) { sk->stream_kind = STREAM_NONE; return; } Mat4 r = mat4_rotate_yxz(tool->rotation); Vec3 origin = vec3_add(tool->position, tool->pivot); start = Figure_toLocal(o->position, facing, vec3_add(origin, mat4_mul_dir(r, vec3(0.0f, 0.16f, 0.37f)))); dir = vec3_normalize(Figure_toLocal(vec3_zero(), facing, mat4_mul_dir(r, vec3(0.0f, 0.22f, 0.26f)))); } Vec3 v = vec3_scale(dir, can ? CAN_SPEED : HOSE_SPEED * sqrtf(flow)); /* (less pressure: a shorter arc) */ const float g = 9.81f; float land = (v.y + sqrtf(fmaxf(v.y * v.y + 2.0f * g * fmaxf(start.y, 0.05f), 0.0f))) / g; for (int k = 0; k < 4; k++) { float t = land * (float)k / 3.0f; sk->stream[k] = vec3(start.x + v.x * t, start.y + v.y * t - 0.5f * g * t * t, start.z + v.z * t); } sk->stream_kind = STREAM_WATER; sk->stream_width = can ? 0.025f : 0.022f; Vec3 hit = Figure_toWorld(o->position, facing, sk->stream[3]); Camp_water(world->objects, world->count, o, hit, (can ? CAN_POUR : HOSE_FLOW * flow) * dt, dt); Wetness_pour(&world->terrain, hit.x, hit.z, liters, can ? CAN_SPREAD : HOSE_SPREAD, 0.0f); /* the ground gets wet */ } /* Sick soaks away; the smell goes before it's gone. Pee smells as long as it's in the ground. */ static void dryPuddles(World *world, float game_dt) { for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->broken) continue; if (o->type == OBJ_STINK && o->variant == 1) { o->work = Wetness_pee(&world->terrain.wet, o->position.x, o->position.z, PEE_STINK); if (o->work < 0.02f) Object_useUp(o); continue; } if (o->type != OBJ_PUDDLE) continue; o->work -= game_dt / fmaxf(o->size.y, 1.0f); Object *stink = o->stink; if (stink && !stink->broken) { stink->work = clampf((o->work - STINK_UNTIL) / (1.0f - STINK_UNTIL), 0.0f, 1.0f); if (stink->work <= 0.0f) Object_useUp(stink); } if (o->work <= 0.0f) Object_useUp(o); } } /* Hit by something: stagger, or go down if it was big. */ static void takeKnock(Object *o) { Vec3 k = vec3(o->knock.x, 0.0f, o->knock.z); float momentum = vec3_length(o->knock); o->knock = vec3_zero(); if (momentum < KNOCK_STAGGER) return; Vec3 dir = vec3_length(k) > 1e-3f ? vec3_normalize(k) : Object_facing(o); if (momentum >= KNOCK_DOWN) Body_fall(&o->body, dir); else Body_stagger(&o->body, vec3_scale(dir, fminf(momentum / 20.0f, 2.0f))); } static float dice(World *world) { world->rng = world->rng * 1664525u + 1013904223u; return (float)(world->rng >> 8) / 16777216.0f; } #define BUMP_MIN 0.7f /* walking into something slower than this: nothing */ #define BUMP_WAIT 1.0f /* seconds before another bump counts */ /* Walked into something: drunk, that easily ends on the ground (back off it, or to the side); a bit tipsy, a stagger. Sober people just stop. */ /* Running into something: over it if it's low (a trip, or headlong over it), back off it if it's high (a stagger, or down on the backside or to the side you came in from). How badly: speed, and how fit, fresh and sure-footed. */ /* How easily someone loses their feet: clumsy and unsporty much more (about 1 for the average person, ~0.2 sporty and sure-footed, ~2.7 clumsy and unfit). */ static float unsure(const Object *o) { return (0.5f + 1.2f * Body_clumsiness(&o->body)) * (1.6f - 1.2f * o->fitness); } static void runInto(World *world, Object *o, float into) { const float s = o->look.height / 1.75f; Vec3 n = o->bump_normal, ahead = vec3_scale(n, -1.0f); Vec3 v = vec3(o->body.velocity.x, 0.0f, o->body.velocity.z); Vec3 along = vec3_sub(v, vec3_scale(n, vec3_dot(v, n))); /* (sliding along it: that side) */ Vec3 side = vec3_length(along) > 0.2f ? vec3_normalize(along) : vec3_scale(vec3_normalize(vec3_cross(n, vec3(0.0f, 1.0f, 0.0f))), dice(world) < 0.5f ? -1.0f : 1.0f); float shaky = clampf((0.2f + 0.4f * (1.0f - o->needs.stamina)) * unsure(o) + 0.8f * fminf(o->drunk, 1.0f), 0.0f, 1.5f); float hard = smoothstepf(RUN_INTO, RUN_INTO + 3.0f, into); float bad = hard * shaky; /* Something to climb, run at on purpose (sober, not worn out): the hands go onto it and you stop there, ready to pull up - no bouncing off */ if (o->type == OBJ_PLAYER && o->bump_top >= RUN_OVER_HIGH * s && o->bump_top <= RUN_AT_CLIMB * s && o->drunk < 0.3f && o->needs.stamina > 0.2f && dice(world) > 0.5f * bad) { Body_stagger(&o->body, vec3_scale(n, 0.15f + 0.2f * hard)); /* (a little give in the arms) */ return; } if (o->bump_top < RUN_OVER_HIGH * s) { /* low: over it */ if (dice(world) < 0.4f * bad) Body_fall(&o->body, vec3_normalize(vec3_add(ahead, vec3_scale(side, 0.3f * dice(world))))); else Body_trip(&o->body, 0, ahead, clampf(0.2f + 0.6f * bad, 0.0f, 1.0f)); } else { /* high: off it */ if (dice(world) < 0.4f * bad) Body_fall(&o->body, vec3_normalize(vec3_add(vec3_scale(n, 0.6f), vec3_scale(side, 0.8f)))); else Body_stagger(&o->body, vec3_scale(vec3_add(n, vec3_scale(side, 0.3f)), 0.4f + 0.6f * hard)); } } #define OVER_FEET_CLEAR 0.17f /* stepping over: the ankles this much over it (the soles about 10 cm) */ #define HALF_OVER_STRAIN 0.025f /* energy a second standing half over something (legs apart) ... */ #define HALF_OVER_LIFTED 0.06f /* ... and with a leg held up over it */ /* Standing still half over something is hard work, a leg held up over it harder still: it costs energy. Worn out, too long, or the legs too far apart: over you go (the body carries on); can't get over: down you go. */ static void halfOver(Object *o, float dt) { Body *b = &o->body; if (b->crossing && o->sitting) { b->crossing = b->cross_force = false; b->cross_held = 0.0f; } /* (feet shuffled under a table: no getting over anything) */ if (!b->crossing || o->type == OBJ_DOG) return; const float s = o->look.height / 1.75f; const Foot *lead = &b->feet[b->cross_foot], *other = &b->feet[1 - b->cross_foot]; float apart = vec3_length(vec3(lead->ankle.x - other->ankle.x, 0.0f, lead->ankle.z - other->ankle.z)); if (o->ground_speed > 0.12f) { b->cross_held = fmaxf(0.0f, b->cross_held - 2.0f * dt); return; } b->cross_held += dt; float strain = (lead->stepping ? HALF_OVER_LIFTED : HALF_OVER_STRAIN) * (1.4f - 0.8f * o->fitness); o->needs.stamina = fmaxf(0.0f, o->needs.stamina - strain * dt); float hold = 1.0f + 5.0f * o->fitness * o->needs.stamina; /* (how long anyone keeps that up) */ bool broken = apart > 0.95f * s; /* (a split: nobody stands like that) */ if (b->cross_held > 0.4f && (o->needs.stamina < 0.03f || b->cross_held > hold || broken)) b->cross_force = true; /* made to and still not getting over it: down */ if (b->cross_force && b->cross_held > hold + 1.5f) { Body_fall(b, b->cross_dir); b->crossing = b->cross_force = false; b->cross_held = 0.0f; } } static void takeBump(World *world, Object *o, float dt) { float into = o->bump; o->bump = 0.0f; o->bump_cool = fmaxf(0.0f, o->bump_cool - dt); float d = fminf(o->drunk, 1.0f); if (into < BUMP_MIN || o->bump_cool > 0.0f || Body_isBusy(&o->body) || o->climbing || o->on_ladder) return; if (o->bump_toe && into < RUN_INTO && o->type != OBJ_DOG) { /* kicked a ladder: it hardly moves */ o->bump_cool = 3.0f * BUMP_WAIT; Ai_say(o, into > 1.5f ? "Ouch! My toe!" : "Ow..."); if (d > 0.3f && dice(world) < d) Body_stagger(&o->body, vec3_scale(o->bump_normal, 0.4f)); return; } if (d < 0.1f) { /* sober: only running into it counts */ if (into < RUN_INTO) return; o->bump_cool = BUMP_WAIT; runInto(world, o, into); return; } o->bump_cool = BUMP_WAIT; Vec3 back = o->bump_normal; Vec3 side = vec3_scale(vec3_normalize(vec3_cross(back, vec3(0.0f, 1.0f, 0.0f))), dice(world) < 0.5f ? -1.0f : 1.0f); float chance = d * d * (0.4f + 0.45f * into) * (0.6f + 0.4f * unsure(o)) + 0.1f * d * Body_clumsiness(&o->body); if (dice(world) < chance) Body_fall(&o->body, vec3_add(back, vec3_scale(side, 0.3f + 0.7f * dice(world)))); else Body_stagger(&o->body, vec3_scale(vec3_add(back, vec3_scale(side, 0.4f)), (0.5f + 0.6f * into) * d)); } /* Point of the main collider of o closest to p. */ static Vec3 closestPoint(const Object *o, Vec3 p) { const Collider *c = &o->colliders[0]; Vec3 center = Object_colliderCenter(o, 0); Vec3 rel = vec3_sub(p, center); const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z }; Vec3 q = center; for (int k = 0; k < 3; k++) q = vec3_add(q, vec3_scale(c->axes[k], clampf(vec3_dot(rel, c->axes[k]), -half[k], half[k]))); return q; } /* A stepped-on branch snaps in two. */ static void snapBranch(World *world, Object *branch) { float half = branch->size.y * 0.5f; if (half < 0.25f) return; World_noise(world, NOISE_SNAP, branch->position, 0.4f + half); Vec3 center = Object_colliderCenter(branch, 0), along = branch->colliders[0].axes[1]; for (int k = -1; k <= 1; k += 2) { Vec3 at = vec3_add(center, vec3_scale(along, (float)k * half * 0.55f)); Object *piece = Object_create(OBJ_BRANCH, PALETTE_WARM, at, vec3(0.0f, half, 0.0f), branch->weight * 0.5f); if (!piece) continue; Object_setRotation(piece, vec3(PI_F / 2.0f, branch->rotation.y + 0.3f * (float)k, 0.0f)); World_add(world, piece); } Object_useUp(branch); } #define FOOT_REACH 0.07f /* a foot comes down on anything this close to its sole */ /* * A foot came down. Wet soft ground squelches into mud (muddy shoes; running * through it you may slip). A step over something may go wrong. Small things * under it get kicked away, some break * (bottles, dry branches), and whoever stepped on it may trip: more likely * drunk, clumsy or running. */ /* A leg that couldn't be kept clear of something (rushing over it, clumsy, tired): a scraped shin. It hurts a moment; at a run it may trip you. */ static void scraped(World *world, Object *who, Vec3 facing, float dt) { who->scrape_cool = fmaxf(0.0f, who->scrape_cool - dt); if (who->sitting && who->sit_moving) return; /* (sitting down: over a bench, carefully) */ const float s = who->look.height / 1.75f; /* (taking care, a leg only brushes it; in a hurry or clumsy it scrapes) */ float careless = fmaxf(smoothstepf(1.5f, 3.0f, who->ground_speed) * (1.2f - 0.6f * who->fitness), Body_clumsiness(&who->body)); if (who->body.scrape < SCRAPE_DEPTH * s * (2.0f - careless) || who->scrape_cool > 0.0f || who->type == OBJ_DOG || who->climbing) return; who->scrape_cool = SCRAPE_COOL; who->scrapes++; Ai_say(who, who->body.scrape > 2.0f * SCRAPE_DEPTH * s ? "Ouch! My shin..." : "Ouch!"); float run = smoothstepf(2.5f, 5.0f, who->ground_speed); if (dice(world) < 0.4f * run * unsure(who)) Body_trip(&who->body, 0, facing, clampf((0.25f + 0.3f * run) * (0.7f + 0.3f * unsure(who)), 0.0f, 1.0f)); } /* What a foot comes down on (for its sound). */ static int stepSurface(const World *world, const Object *who, Vec3 sole) { const Object *on = who->standing_on; if (on && sole.y > Terrain_height(&world->terrain, sole.x, sole.z) + 0.05f) { if (on->type == OBJ_LOG || on->type == OBJ_FIREWOOD || on->type == OBJ_LADDER || on->type == OBJ_FURNITURE || on->type == OBJ_TREE) return STEP_WOOD; return STEP_HARD; /* (floors, steps, walls, crates, roofs) */ } if (Wetness_mud(&world->terrain.wet, sole.x, sole.z) > 0.35f) return STEP_MUD; if (Wetness_water(&world->terrain.wet, sole.x, sole.z) > 3.0f) return STEP_WATER; SiteSurface s = Site_surface(sole.x, sole.z); if (s.asphalt + s.concrete > 0.5f) return STEP_HARD; if (s.gravel > 0.5f) return STEP_GRAVEL; if (s.earth > 0.5f || Site_grass(sole.x, sole.z) < 0.3f) return STEP_EARTH; return STEP_GRASS; } static void stepOn(World *world, Object *who, int foot, Vec3 ankle) { const float s = who->look.height / 1.75f; Vec3 sole = vec3_sub(ankle, vec3(0.0f, 0.075f * s, 0.0f)); Vec3 facing = Object_facing(who); float speed = vec3_length(vec3(who->velocity.x + who->movement.x, 0.0f, who->velocity.z + who->movement.z)); speed = fmaxf(speed, who->ground_speed); World_noiseOn(world, NOISE_STEP, sole, clampf(0.25f + 0.12f * speed, 0.2f, 1.0f) * clampf(who->weight / 75.0f, 0.4f, 1.3f), who->on_ladder ? STEP_WOOD : stepSurface(world, who, sole)); if (!who->on_ladder && sole.y < Terrain_height(&world->terrain, sole.x, sole.z) + 0.1f) { float mud = Wetness_step(&world->terrain.wet, sole.x, sole.z); who->dirt = fminf(1.0f, who->dirt + 0.015f * mud); float slip = mud * clampf((speed - 2.5f) / 3.0f, 0.0f, 1.0f) * (0.5f + who->body.unsteadiness) * unsure(who); if (dice(world) < 0.06f * slip) Body_trip(&who->body, foot, facing, clampf(0.3f + 0.5f * slip, 0.0f, 1.0f)); } /* That step went over something: it may go wrong, more so running, high, drunk, worn out or clumsy (and badly: down you go) */ float over = who->body.feet[foot].over; if (over > 0.04f * s && !who->on_ladder) { float run = clampf((speed - 1.5f) / 4.5f, 0.0f, 1.0f), high = clampf(over / (0.5f * s), 0.0f, 1.0f); float tired = (1.0f - who->needs.stamina) * (1.0f - who->needs.stamina), u = who->body.unsteadiness; float chance = (0.006f + 0.07f * run + 0.04f * high * run + 0.04f * tired) * (1.0f + 3.0f * u) * unsure(who); /* (sporty and sure-footed: much surer) */ if (dice(world) < chance) Body_trip(&who->body, foot, facing, clampf(0.15f + 0.1f * unsure(who) + 0.35f * run + 0.25f * high + 0.6f * u + 0.2f * tired, 0.0f, 1.0f)); } else if (!who->on_ladder && who->type != OBJ_DOG && speed > 2.0f) { /* clumsy and unfit: now and then over your own feet, jogging or running */ float run = clampf((speed - 2.0f) / 4.0f, 0.0f, 1.0f); if (dice(world) < 0.004f * run * fmaxf(0.0f, unsure(who) - 1.0f)) Body_trip(&who->body, foot, facing, clampf(0.2f + 0.15f * unsure(who) + 0.3f * run, 0.0f, 1.0f)); } for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o == who || o->is_static || o->is_character || o->type == OBJ_DOG || o->is_held || o->broken || o->lifted || o->collider_count == 0 || o->weight > 40.0f) continue; Vec3 q = closestPoint(o, sole); if (vec3_length(vec3_sub(q, sole)) > FOOT_REACH || q.y > sole.y + 0.12f) continue; /* stepped over it, clear of it: nothing (only a foot coming down on it does something) */ if (Physics_stepsOver(who, o) && vec3_length(vec3(q.x - sole.x, 0.0f, q.z - sole.z)) > 0.03f) continue; Vec3 kick = vec3_add(vec3_scale(facing, 0.6f + 0.3f * speed), vec3(0.0f, 0.4f, 0.0f)); float trip = 1.0f; switch (o->type) { case OBJ_BOTTLE: case OBJ_VODKA: case OBJ_WATER: /* on grass it just rolls away; on hard ground a heavy step may crack it */ if (Site_hard(o->position.x, o->position.z) && dice(world) < 0.08f + 0.04f * speed) { Object_shatter(o, Object_bottom(o)); World_noise(world, NOISE_SMASH, o->position, 0.6f); trip = 0.5f; } else { Object_topple(o, facing); o->velocity = vec3_add(o->velocity, kick); o->thrown_by = who; o->thrown_time = 0.0f; } break; case OBJ_BRANCH: if (dice(world) < 0.5f) snapBranch(world, o); trip = 0.8f; break; case OBJ_APPLE: trip = 0.6f; o->velocity = vec3_add(o->velocity, kick); break; default: /* firewood, crates, small logs, tools */ trip = 1.2f; if (o->weight < 15.0f) o->velocity = vec3_add(o->velocity, vec3_scale(kick, 10.0f / fmaxf(o->weight, 2.0f))); break; } float severity = clampf(0.7f * who->body.unsteadiness + 0.2f * unsure(who) + 0.12f * speed, 0.0f, 1.0f); if (dice(world) < trip * (0.04f + 0.35f * severity) * (0.4f + 0.6f * unsure(who))) Body_trip(&who->body, foot, facing, severity); return; /* one thing per step */ } } /* Stepping over something (the set move, see Physics): the first foot lifts and goes over (up first, over, down past it), then the other one, the knee up and out. The feet are clear of it all along. */ static void overFeet(Object *o) { const float s = o->look.height / 1.75f, t = o->over_t; const Vec3 dir = o->over_dir; const float clear = o->over_top + OVER_FEET_CLEAR * s; /* (over the feet's start height) */ for (int i = 0; i < 2; i++) { Foot *ft = &o->body.feet[i]; bool lead = i == o->over_lead; float u = lead ? smoothstepf(0.02f, 0.5f, t) : smoothstepf(0.5f, 0.98f, t); Vec3 from = o->over_foot0[i], to = o->over_land[i]; float go = smoothstepf(0.18f, 0.82f, u); /* over ... */ float up = smoothstepf(0.0f, 0.36f, u) * (1.0f - smoothstepf(0.72f, 1.0f, u)); /* ... once up, and down after */ Vec3 at = vec3_lerp(from, to, go); float base = lerpf(from.y, to.y, go); at.y = base + up * (clear + (o->over_from.y - base)) ; /* (the trailing leg swings out a little going over) */ Vec3 right = vec3(-dir.z, 0.0f, dir.x); float side = (i == 0 ? -1.0f : 1.0f) * (lead || o->over_up ? 0.03f * s : 0.09f * s + 0.5f * fmaxf(0.0f, o->over_top - 0.3f * s)) * up; at = vec3_add(at, vec3_scale(right, side)); ft->ankle = at; ft->dir = dir; Vec3 toe = vec3_add(at, vec3_scale(dir, 0.14f * s)); toe.y = at.y - (up > 0.05f ? 0.04f : 0.06f) * s; /* (lifted: toes up a little) */ ft->toe = toe; ft->stepping = false; ft->step = 1.0f; ft->step_from = ft->step_to = ft->spot_was = at; ft->over = 0.0f; ft->lift = 0.0f; } o->body.lean_goal = fmaxf(o->body.lean_goal, 0.3f); o->body.crossing = false; if (o->over_t >= 1.0f || o->over_t <= 0.0f) o->body.replant = false; } #define FAR_DETAIL 30.0f /* m from the player: beyond, feet and hands don't look out for things */ /* Smoking: puffs breathed out and off the tip, the butt when it's done: into a burning fire close by, the tin or a bin near (the tidy, and you), into a pocket ashtray; the tidy keep it in the fingers and go and find a place for it (ai.c binButt); the others drop it or flick it away. */ #define BUTT_REACH 4.0f /* m: the tin or a bin this near */ /* Where a butt goes: the tin or a bin within reach. */ static Object *buttPlace(const World *world, const Object *o, float reach) { Object *best = NULL; for (int i = 0; i < world->count; i++) { Object *t = world->objects[i]; if (!(t->type == OBJ_FURNITURE && t->variant == FURN_ASHTRAY) && t->type != OBJ_BIN) continue; float d = vec3_length(vec3_sub(t->position, o->position)); if (d < reach) { reach = d; best = t; } } return best; } #define CRAVE_TIME 300.0f /* s from nothing to a strong craving, for a chain smoker */ static void smoke(World *world, Object *o, Vec3 facing, float dt) { const Skeleton *sk = &o->skeleton; if (Body_isSmoking(&o->body)) o->craving = fmaxf(0.0f, o->craving - dt / 90.0f); else o->craving = fminf(1.0f, o->craving + dt * o->smoker / CRAVE_TIME); if (Body_takeButt(&o->body)) { Vec3 at = Figure_toWorld(o->position, facing, sk->cig_glow >= 0.0f ? sk->cig_tip : sk->joints[JOINT_HAND_R]); Object *fire = Camp_fireNear(world->objects, world->count, o->position, 3.0f, true); Object *tray = buttPlace(world, o, BUTT_REACH); bool tidy = o->type == OBJ_PLAYER || (o->type == OBJ_HUMAN && (o->ai.character.likes[ORDER_TRASH] > 0.2f || o->temperament.pride > 0.4f) && o->drunk < 0.6f); if (fire) { /* (flicked into the flames) */ } else if (tray && (tidy || worldDice(world) < 0.4f)) { if (tray->type == OBJ_FURNITURE) tray->stored++; /* (a bin: in it, gone) */ } else if (o->pocket_tray) { Pockets_keepButt(o); } else if (tidy && o->type == OBJ_HUMAN) { o->butt_held = true; /* (off to find a place for it, or ask) */ o->ai.butt_time = 0.0f; o->ai.butt_asked = false; } else { Object *butt = Object_create(OBJ_TRASH, PALETTE_WARM, at, vec3(-1.0f, 0.0f, 0.0f), 0.005f); if (butt) { snprintf(butt->name, sizeof(butt->name), "Cigarette butt"); Vec3 flick = vec3_add(vec3_scale(facing, 1.2f + 1.5f * worldDice(world)), vec3(0.0f, 1.0f, 0.0f)); butt->velocity = worldDice(world) < 0.5f ? flick : vec3_zero(); /* (flicked away, or dropped and stepped on) */ if (!World_add(world, butt)) Object_destroy(butt); } } } if (sk->cig_glow < 0.0f || o->far_off) return; if ((o->puff_timer -= dt) > 0.0f) return; if (o->body.exhale > 0.0f) { /* out of the mouth (and the nose) */ Vec3 mouth = vec3_add(sk->joints[JOINT_HEAD], vec3_add(vec3_scale(sk->head_forward, 0.11f * o->look.height / 1.75f), vec3_scale(sk->head_up, -0.06f * o->look.height / 1.75f))); Vec3 out = Figure_toWorld(vec3_zero(), facing, vec3_add(sk->head_forward, vec3(0.0f, -0.3f, 0.0f))); Chips_puff(&world->chips, Figure_toWorld(o->position, facing, mouth), out, false); o->puff_timer = 0.14f; } else if (sk->cig_glow > 0.0f) { /* a thin thread off the tip */ Chips_puff(&world->chips, Figure_toWorld(o->position, facing, sk->cig_tip), vec3(0.0f, 1.0f, 0.0f), true); o->puff_timer = 0.35f; } } /* Wet clothes: out in the rain they soak through; they dry by a fire quickest, in the day slower, at night or inside slowest (game seconds). */ #define SOAK_THROUGH (6.0f * 60.0f) /* hard rain: wet through */ #define DRY_BY_FIRE (8.0f * 60.0f) #define DRY_IN_DAY (40.0f * 60.0f) #define DRY_ELSE (120.0f * 60.0f) static void clothesWet(World *world, Object *o, bool by_fire, bool night) { float gdt = world->game_dt; if (o->rain_out) o->look.wet = fminf(1.0f, o->look.wet + world->rain * gdt / SOAK_THROUGH); else if (o->look.wet > 0.0f) { float dry = by_fire ? DRY_BY_FIRE : !night && o->indoors <= 0.0f && !o->in_tent ? DRY_IN_DAY : DRY_ELSE; o->look.wet = fmaxf(0.0f, o->look.wet - gdt / dry); } } /* Cold nights: away from a burning fire a chill builds up and the pullover goes on; by the fire, in the day (or warm with drink) it comes off again (round the waist). Put down somewhere: people go and get it, you are told where it is. You: put on for you once a cold spell (then it's up to you, B). */ /* An umbrella in hand: open while it rains (out in it), folded otherwise. Is one open over them? */ #define UMBRELLA_COVERS 0.9f /* of its radius: a head this near under the middle of an open umbrella stays dry */ static bool underUmbrella(World *world, Object *o, float dt) { bool over = false; if ((o->umbrella_guest_t -= dt) <= 0.0f) { o->umbrella_guest = NULL; o->umbrella_guest_t = 0.0f; } /* (nobody with them any more) */ for (int h = 0; h < 2; h++) { Object *u = o->hands.item[h]; if (!u || u->type != OBJ_UMBRELLA || o->hands.state[h] != HAND_HOLDING) continue; u->umbrella_open = world->rain > 0.05f && !o->in_tent && o->indoors <= 0.0f && !o->riding && !u->umbrella_ruined && !Weather_covered(world, vec3_add(o->position, vec3(0.0f, o->look.height + 0.3f, 0.0f))); over = over || (u->umbrella_open && u->umbrella_flip <= 0.0f); /* (blown inside out: no use a moment) */ } if (over || o->is_held) return over; /* under somebody else's (walking along with them) */ for (int i = 0; i < world->count && !over; i++) { const Object *p = world->objects[i]; if (p == o || !p->is_character) continue; for (int h = 0; h < 2 && !over; h++) { const Object *u = p->hands.item[h]; if (!u || u->type != OBJ_UMBRELLA || !u->umbrella_open || u->umbrella_flip > 0.0f || p->hands.state[h] != HAND_HOLDING) continue; Vec3 top = vec3_add(Object_colliderCenter(u, 0), vec3_scale(u->colliders[0].axes[1], UMBRELLA_LENGTH * 0.5f)); float dx = top.x - o->position.x, dz = top.z - o->position.z; over = dx * dx + dz * dz < (UMBRELLA_COVERS * UMBRELLA_RADIUS) * (UMBRELLA_COVERS * UMBRELLA_RADIUS) && top.y > o->position.y + o->look.height; } } return over; } static void dressForNight(World *world, Object *o, float dt) { if (o->type == OBJ_DOG) return; bool umbrella = underUmbrella(world, o, dt); bool night = world->hour >= 21.0f || world->hour < 7.0f; bool by_fire = Camp_fireNear(world->objects, world->count, o->position, 4.0f, true) != NULL; float cold = !night ? -2.0f : by_fire || (o->in_tent && Tent_hasBed(o->in_tent, o)) ? -1.0f : 1.0f - 0.8f * fminf(o->drunk, 1.0f); if (world->rain > 0.2f && !o->in_tent && o->indoors <= 0.0f && !o->riding && ((int)(world->hour * 3600.0f) + (int)(o->position.x * 7.0f)) % 30 == 0) o->rain_out = !Weather_covered(world, vec3_add(o->position, vec3(0.0f, o->look.height, 0.0f))); /* (out in it: now and then looked up) */ if (world->rain <= 0.2f || o->in_tent || o->indoors > 0.0f) o->rain_out = false; if (umbrella) o->rain_out = false; /* (dry under it) */ clothesWet(world, o, by_fire, night); if (o->rain_out) cold = fmaxf(cold, 0.0f) + 1.2f * world->rain; /* (out in it: cold, even in the day) */ else if (o->look.wet > 0.2f && !by_fire) cold = fmaxf(cold, 0.0f) + 0.8f * o->look.wet; /* (in wet clothes: cold too) */ o->chill = clampf(o->chill + cold * dt / 60.0f, 0.0f, 1.0f); /* (about a minute to feel it) */ bool player = o->type == OBJ_PLAYER; if (o->look.top != TOP_JACKET && o->chill > 0.6f && !(player && o->jacket_told)) { if (!o->pullover_down) { /* with them: on it goes */ Clothes_takeBack(o, true); if (player) o->jacket_told = true; else if (o->ai.speech_time <= 0.0f && worldDice(world) < 0.3f) Ai_say(o, "Brr, getting cold."); } else if (!player && !o->ai.want_jacket) { /* put down somewhere: they go and get it */ o->ai.want_jacket = true; o->ai.jacket_time = 0.0f; if (o->ai.speech_time <= 0.0f && worldDice(world) < 0.4f) Ai_say(o, "Brr - where did I leave my pullover?"); } } else if (o->look.top == TOP_JACKET && o->chill < 0.15f) { o->look.top = TOP_SHIRT; /* (off, and along with them: round the waist) */ } if (o->chill < 0.3f) o->jacket_told = false; /* out in the rain with a hood to it: the jacket on, the hood up (none under an umbrella) */ if (o->rain_out && o->look.hoodie && !umbrella) { if (o->look.top != TOP_JACKET && !o->pullover_down && o->look.top != TOP_BARE) { Clothes_takeBack(o, true); if (o->type != OBJ_PLAYER && o->ai.speech_time <= 0.0f && worldDice(world) < 0.3f) Ai_say(o, "Hood up."); } o->look.hood_up = o->look.top == TOP_JACKET; } else if (!o->rain_out) o->look.hood_up = false; o->look.jacket_tied = !o->pullover_down && o->look.top != TOP_JACKET; /* (off and with them) */ } /* A chair waits to be pushed in while somebody stands where it goes. */ static bool chairWaits(const World *world, const Object *chair) { if (chair->chair_goal >= chair->chair_out) return false; for (int i = 0; i < world->count; i++) { const Object *o = world->objects[i]; if (!o->is_character || o->seat == chair) continue; float dx = o->position.x - chair->tucked.x, dz = o->position.z - chair->tucked.z; if (dx * dx + dz * dz < 0.6f * 0.6f) return true; } return false; } /* A chair sliding takes whoever sits on it (or is getting up off it) along. */ static void slideChair(World *world, Object *chair) { Vec3 was = chair->position; Object_slideChair(chair, world->dt_props); Vec3 moved = vec3_sub(chair->position, was); moved.y = 0.0f; if (vec3_length(moved) < 1e-6f) return; for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (!o->is_character || o->seat != chair || o->sit_moving) continue; o->position = vec3_add(o->position, moved); o->sit_at = vec3_add(o->sit_at, moved); Object_syncColliders(o); } } /* Sat down on a chair: it's shuffled in towards the table (the table's edge SEAT_TO_TABLE from the hips; none in front: just pushed in where it stood). */ #define SEAT_TO_TABLE 0.30f #define SEAT_LOOK 1.2f /* m: a table this near in front counts */ static float scootTo(const World *world, const Object *chair) { Vec3 n = chair->colliders[0].axes[2]; n = vec3_normalize(vec3(n.x, 0.0f, n.z)); Vec3 from = vec3_add(chair->position, vec3(0.0f, SEAT_HEIGHT + 0.15f, 0.0f)); /* (just over the seat: a table top's edge) */ float t = World_rayCast(world, from, n, SEAT_LOOK, NULL, NULL, NULL, NULL); if (t < 0.0f) return 0.0f; float hips = 0.16f; /* (the hips this far in front of the chair's middle, see Object_sit) */ float closer = t - hips - SEAT_TO_TABLE; /* how much further in it can go from here */ return clampf(chair->chair_out - closer, -0.15f, chair->chair_out); } /* The tin shows its butts: its mesh is built again when that changes. */ static void showProps(World *world) { for (int i = 0; i < world->count; i++) { Object *f = world->objects[i]; if ((f->type == OBJ_FURNITURE && f->variant == FURN_GATE) || f->type == OBJ_CAR) { /* (opening or shutting: heard) */ bool open = f->type == OBJ_CAR ? f->boot_open : f->gate_open; if (open != f->heard_open) World_noise(world, f->type == OBJ_CAR ? NOISE_SLAM : NOISE_CREAK, f->position, 0.8f); f->heard_open = open; } if (f->type == OBJ_STAGE) { Stage_show(f); continue; } /* (after loading: its look and deck) */ if (f->type == OBJ_HAT && !f->broken && f->shown != 1) { /* (its look: the colour it had on the head) */ Mesh_destroy(&f->mesh); f->mesh = Shapes_hat(f->variant, f->look.hat_color); f->shown = 1; continue; } if (f->type == OBJ_UMBRELLA) { /* (folded once it's put down) */ if (!f->is_held) f->umbrella_open = false; Object_showUmbrella(f); continue; } if (f->type == OBJ_CRATE && f->shown != Crate_look(f)) { /* (a bottle taken or put back: it shows) */ Mesh_destroy(&f->mesh); f->mesh = Shapes_crate(f->variant, f->crate_slot, Crate_bottles(f->variant)); f->shown = Crate_look(f); } if (f->type != OBJ_FURNITURE) continue; if (f->variant == FURN_GATE) { if (f->gate_t != (f->gate_open ? 1.0f : 0.0f)) Object_swingGate(f, world->dt_props); continue; } if (f->variant == FURN_CHAIR) { if (f->chair_out != f->chair_goal && !chairWaits(world, f)) slideChair(world, f); continue; } if (f->variant == FURN_ASHTRAY) { int butts = f->stored < 12 ? f->stored : 12 + (f->stored >= 14); if (butts == f->shown) continue; Mesh_destroy(&f->mesh); f->mesh = Shapes_ashtray(f->size, f->stored); f->shown = butts; } } } #define VOID_BELOW -60.0f /* m: below everything there is (off the map the ground is far down) */ #define ROLL_ON 3.0f /* m/s on through a shoulder roll (scaled) */ /* Down from high up: into the knees, deeper, a roll or a crash (see Body_land). */ static void landFromHigh(World *world, Object *o) { float speed = o->air_fall; o->air_fall = 0.0f; if (o->type == OBJ_DOG || speed < 5.0f) return; float sporty = smoothstepf(0.6f, 0.95f, o->fitness) * (1.0f - fminf(o->drunk, 1.0f)) * smoothstepf(0.15f, 0.4f, o->needs.stamina); float sure = clampf(o->fitness * 1.3f, 0.0f, 1.0f) * sqrtf(o->needs.stamina) * (1.0f - fminf(o->drunk, 1.0f)) * (1.0f - 0.5f * Body_clumsiness(&o->body) * (1.0f - sporty)) + 0.15f * sporty; /* (the very sporty: next to sure) */ Vec3 dir = vec3(o->velocity.x, 0.0f, o->velocity.z); if (vec3_length(dir) < 0.3f) dir = Object_facing(o); float forward = hypotf(o->body.velocity.x, o->body.velocity.z), bad = 0.0f; int how = Body_land(&o->body, vec3_normalize(dir), speed, forward, sure, sporty, &bad); float jar = smoothstepf(5.0f, 10.0f, speed); o->needs.stamina = fmaxf(0.0f, o->needs.stamina - (how == LAND_CRASH ? 0.25f : 0.08f) * jar); if (how == LAND_CRASH) o->dirt = fminf(1.0f, o->dirt + 0.1f); else if (how == LAND_ROLL) o->dirt = fminf(1.0f, o->dirt + 0.05f); /* how it went for the body: the worse, the likelier something gives - a twisted ankle, or (bad) a broken bone */ world->rng = world->rng * 1664525u + 1013904223u; float d1 = (float)(world->rng >> 8) / 16777216.0f; world->rng = world->rng * 1664525u + 1013904223u; float d2 = (float)(world->rng >> 8) / 16777216.0f; if (bad > 0.0f && d1 < bad) { if (bad + 0.35f * d2 > 0.8f && Injury_break(world, o, how == LAND_CRASH && d2 < 0.3f ? INJ_ARM : INJ_LEG)) return; if (Injury_sprain(world, o)) return; } if (how == LAND_CRASH) Ai_say(o, "Ouch!"); } /* Up a steep bank (walking straight up it): bent forward, a free hand put down on the slope ahead, left there as the body passes, then put down again further up. */ #define SCRAMBLE_GRADE 0.8f /* rise per m ahead from which a hand goes down */ #define SCRAMBLE_AHEAD 0.6f /* m (scaled) ahead it goes down ... */ #define SCRAMBLE_LEAVE 0.15f /* ... and is taken up once the shoulders are this far past it */ static void scrambleHand(World *world, Object *o) { bool was = o->scrambling; o->scrambling = false; if (!o->is_character || !o->is_grounded || o->climbing || o->on_ladder || o->sitting || Body_isBusy(&o->body) || Hands_freeHand(o) < 0 || hypotf(o->body.velocity.x, o->body.velocity.z) < 0.3f) return; /* (walking up it: the body moving) */ const float s = o->look.height / 1.75f; Vec3 f = Object_facing(o); const Terrain *t = &world->terrain; float here = Terrain_height(t, o->position.x, o->position.z); if (fabsf(World_groundHeight(world, o->position.x, o->position.z, o->position.y + 0.3f) - here) > 0.1f) return; /* (on the bare ground) */ float grade = (Terrain_height(t, o->position.x + f.x * 0.5f * s, o->position.z + f.z * 0.5f * s) - here) / (0.5f * s); if (grade < SCRAMBLE_GRADE) return; Vec3 rel = vec3_sub(o->scramble_hand, o->position); if (!was || vec3_dot(vec3(rel.x, 0.0f, rel.z), f) < SCRAMBLE_LEAVE * s) { /* (down again further up) */ Vec3 at = vec3_add(o->position, vec3_scale(f, SCRAMBLE_AHEAD * s)); o->scramble_hand = vec3(at.x, Terrain_height(t, at.x, at.z) + 0.03f, at.z); } o->scrambling = true; Hands_touch(o, o->scramble_hand); } /* Hands, feet and skeleton, then carried items follow the hands. */ static void animateCharacter(World *world, Object *o, float dt) { scrambleHand(world, o); if (o->body.pose == POSE_DOWN && o->heard_pose == POSE_FALLING) World_noise(world, NOISE_THUD, o->position, clampf(o->weight / 80.0f, 0.4f, 1.0f)); /* (down on the ground) */ o->heard_pose = o->body.pose; if (o->heard_ride && !o->riding) World_noise(world, NOISE_SLAM, o->position, 0.7f); /* (out of the car: its door) */ o->heard_ride = o->riding != NULL; if (o->climbing || o->on_ladder || o->is_held) o->air_fall = 0.0f; else if (!o->is_grounded) o->air_fall = fmaxf(o->air_fall, -o->velocity.y); else if (o->air_fall > 0.0f) landFromHigh(world, o); bool sipped = o->hands.drink_hand >= 0 && o->hands.drink_sipped; /* (a sip or a bite just now: heard) */ const Object *drinking = o->hands.drink_hand >= 0 ? o->hands.item[o->hands.drink_hand] : NULL; Hands_update(o, dt); if (drinking && !sipped && o->hands.drink_sipped) World_noiseOn(world, NOISE_GULP, vec3_add(o->position, vec3(0.0f, o->look.height * 0.9f, 0.0f)), 0.5f, Object_isFood(drinking) ? 1 : 0); Tools_update(o, dt); Ladder_poseHands(o, dt); /* on a ladder: hands on the rungs */ bool settling = Object_settleOnSeat(o, dt); /* over to the seat first */ if (o->sitting && !settling && o->seat && o->seat->type == OBJ_FURNITURE && o->seat->variant == FURN_CHAIR && o->body.sit > 0.85f && !o->seat_scooted) { /* down: shuffle the chair in to the table */ o->seat_scooted = true; o->seat->chair_goal = scootTo(world, o->seat); } if (!o->sitting && o->seat_scooted && o->seat) { /* getting up: out again first */ o->seat_scooted = false; if (o->seat->type == OBJ_FURNITURE && o->seat->variant == FURN_CHAIR) o->seat->chair_goal = CHAIR_PULL; } Object_leaveSeat(o, dt); Object_scootOnSeat(o, dt); o->body.stand_ahead = o->sitting && o->seat ? 0.12f : 0.0f; /* (feet clear of it) */ const bool sit_climb = o->climbing && o->climb_move.sit_way; /* (the careful way up: sitting on it, not pulling up) */ o->body.on_ladder = o->on_ladder != NULL || (o->climbing && !sit_climb); /* (or pulling up over a ledge) */ o->body.pulling_up = o->climbing && o->on_ladder == NULL && !sit_climb; o->body.belly_over = o->climbing && o->climb_move.belly_way ? o->climb_move.belly : 0.0f; o->body.fitness = o->fitness; o->body.sulk = o->ai.annoyed; o->body.sit_goal = (o->sitting && !o->is_held && !settling) || (sit_climb && o->climb_move.sit_now) ? 1.0f : 0.0f; /* cross-legged, or on a seat */ o->body.squat_pee = o->female && !(o->urinella && o->urinellas > 0) && !o->toilet; o->body.urinella_use = o->female && !o->body.squat_pee && !o->toilet; o->body.sit_style = o->sit_style; { /* on something narrow (a log): balancing */ const Object *on = o->standing_on; float w = on && o->is_grounded && !o->on_ladder && !o->sitting ? Physics_footing(on) : 1e9f; o->body.balance_goal = 1.0f - smoothstepf(BALANCE_NARROW, BALANCE_WIDE, w); if (o->type == OBJ_HUMAN && !o->sitting && Object_upHigh(o, Terrain_height(&world->terrain, o->position.x, o->position.z))) o->body.balance_goal = fmaxf(o->body.balance_goal, 0.5f * Object_caution(o)); /* (up on a roof: arms out a little, the careful more) */ } BodyFrame frame = { .position = o->position, .facing = Object_facing(o), .height = o->look.height, .grounded = o->is_grounded, .dt = dt, .ground = groundQuery, .ground_ctx = world, .clutter = o->far_off ? NULL : clutterQuery, /* (far off nobody sees a foot on a log) */ .push_out = o->far_off ? NULL : pushOutQuery, }; Body_update(&o->body, &frame); Ladder_poseFeet(o, dt); /* ... and the feet */ if (o->climbing && o->climb_feet && !o->on_ladder) /* pulling up over a ledge: feet up the wall, then onto it */ for (int i = 0; i < 2; i++) { Foot *ft = &o->body.feet[i]; ft->ankle = o->climb_ankle[i]; ft->toe = o->climb_toe[i]; ft->dir = vec3_normalize(vec3(ft->toe.x - ft->ankle.x, 0.0f, ft->toe.z - ft->ankle.z)); ft->stepping = false; } if (!o->climbing) o->climb_feet = false; if (o->over_t > 0.0f) overFeet(o); int foot; Vec3 ankle; if (Body_takeLanding(&o->body, &foot, &ankle)) stepOn(world, o, foot, ankle); o->velocity = vec3_add(o->velocity, Body_takeStumblePush(&o->body)); /* misstep lurch */ o->position = vec3_add(o->position, Body_takeShift(&o->body)); /* got up elsewhere */ if (Body_takeVomit(&o->body)) { spawnSick(world, o); o->drunk = fmaxf(0.0f, o->drunk - SICK_RELIEF); } Forestry_finishJobs(world, o); if (Body_takePee(&o->body)) spawnPee(world, o); Body_solve(&o->body, o->position, frame.facing, o->look.height, dt, &o->skeleton); scraped(world, o, frame.facing, dt); Hands_placeHeld(o); if (o->toilet) o->skeleton.stream_kind = STREAM_NONE; /* (out of sight) */ if (o->skeleton.stream_kind == STREAM_PEE) { /* where it comes down: fires go out, people complain */ o->pee_hit = Figure_toWorld(o->position, frame.facing, o->skeleton.stream[3]); Camp_pee(world->objects, world->count, o, o->pee_hit, PEE_FLOW * (1.0f + 1.5f * o->body.pee_power) * dt, dt); Wetness_pour(&world->terrain, o->pee_hit.x, o->pee_hit.z, PEE_LITERS * (1.0f + 0.6f * o->body.pee_power) * dt, PEE_SPREAD, 1.0f); } if (o->spraying) water(world, o, dt); if (Body_isDown(&o->body)) o->dirt = fminf(1.0f, o->dirt + 0.003f * dt); /* lying in the dirt */ smoke(world, o, frame.facing, dt); dressForNight(world, o, dt); } /* ---------- What people carry bumps into things ---------- */ #define CARRY_SOLID 40.0f /* kg: lighter loose things are no wall for what you carry */ /* Is this something a carried item can't go through? */ #define CARRY_SHORT 0.62f /* carried things shorter than this (m) don't stop anyone walking */ /* Carrying the same thing (a log between two): no wall for each other. */ static bool carryTogether(const Object *a, const Object *b) { for (int i = 0; i < 2; i++) for (int k = 0; k < 2; k++) if (a->hands.item[i] && a->hands.item[i] == b->hands.item[k]) return true; return (a->hands.grabbed && a->hands.grabbed == b->hands.grabbed); } static bool solidFor(const Object *o, const Object *who) { if (o == who || o->is_held || o->collider_count == 0 || o->broken || o->type == OBJ_BUSH) return false; if (o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->type == OBJ_DECAL) return false; if (o->is_character || o->type == OBJ_DOG) return !carryTogether(o, who); /* (people and the dog: no walking a ladder through them) */ return o->is_static || o->weight >= CARRY_SOLID; } #define RAISE_DIP_DONE 0.2f /* rad: tipped down far enough to look whether it goes up again */ #define RAISE_UP_TEST 1.36f /* (how steep it goes back up) */ /* Long enough to be in the way (a ladder, a log, a branch; not a bag or a bottle). */ static bool carriedLong(const Object *item) { if (!item || item->collider_count == 0) return false; Vec3 e = item->colliders[0].half_extents; return item->collider_count > 1 || 2.0f * fmaxf(e.x, fmaxf(e.y, e.z)) >= CARRY_SHORT; } /* How deep what who carries is in solid things (the deepest). */ static float carriedOverlap(const World *world, const Object *who) { float most = 0.0f; for (int h = 0; h < 2; h++) { const Object *item = who->hands.item[h]; if (!item || who->hands.state[h] != HAND_HOLDING || (h == 1 && item == who->hands.item[0])) continue; if (!carriedLong(item)) continue; /* (a bag or a bottle just squeezes by) */ Vec3 c = Object_colliderCenter(item, 0); float reach = 0.0f; for (int k = 0; k < item->collider_count; k++) reach = fmaxf(reach, vec3_length(item->colliders[k].half_extents) + vec3_length(vec3_sub(Object_colliderCenter(item, k), c))); int near[NEAR_MAX], nn = nearby(world, c.x, c.z, reach + 1.0f, near); for (int q = 0; q < (nn < 0 ? world->count : nn); q++) { const Object *o = world->objects[nn < 0 ? q : near[q]]; if (!solidFor(o, who) || o == who->work_state.target || o == who->work_state.last_target) continue; /* (the axe goes into the wood) */ Vec3 d = vec3_sub(o->position, c); float r = reach + vec3_length(o->colliders[0].half_extents) + vec3_length(o->pivot) + 1.0f; if (vec3_dot(d, d) > r * r && !o->is_static) continue; most = fmaxf(most, Physics_penetration(item, o)); } } return most; } static void placeAt(Object *who, Vec3 pos, float yaw) { who->position.x = pos.x; who->position.z = pos.z; who->rotation.y = yaw; Object_syncColliders(who); Hands_placeHeld(who); } #define RAISE_DIP_TEST 0.12f /* (how flat it goes down) */ static float raisedAt(World *world, Object *who, Object *l, float angle) { float was = l->raised; l->raised = angle; Hands_placeHeld(who); float o = carriedOverlap(world, who); l->raised = was; Hands_placeHeld(who); return o; } /* A ladder held up that runs into something: under a beam or a doorway it is tipped down flat, carried under it, and up again once there's room up there. Against a wall (flat it would only go in deeper) it's stood up straight. */ static void dipLadder(World *world, Object *who, float overlap) { Object *l = NULL; for (int h = 0; h < 2; h++) if (who->hands.state[h] == HAND_HOLDING && Ladder_isRaised(who->hands.item[h])) l = who->hands.item[h]; if (!l) return; if (overlap > 0.002f) { if (l->dipped || l->steep) return; float flat = raisedAt(world, who, l, RAISE_DIP_TEST), up = raisedAt(world, who, l, RAISE_UP_TEST); if (flat + 0.002f < overlap && flat <= up) l->dipped = true; else l->steep = true; return; } if (l->steep) { /* clear: back to how it's held once that's clear too */ if (raisedAt(world, who, l, RAISE_UP_TEST) <= 0.002f) l->steep = false; return; } if (!l->dipped || l->raised > RAISE_DIP_DONE) return; /* (still going down: stays down) */ /* down and clear: up again if it would stand up there without going into anything */ if (raisedAt(world, who, l, RAISE_UP_TEST) <= 0.002f) l->dipped = false; } #define SLIDE_STEP 0.1f /* m: a ladder carried at the side slides back in the hand this much at a time ... */ #define SLIDE_KEEP 0.35f /* ... held at least this far from its end */ /* A ladder carried at the side that is in a wall (taken off it, walked into it): slid along in the hand till it's out (back to the middle once there's room). Returns how deep it's in now. */ static float slideLadder(World *world, Object *who, float now) { Object *l = NULL; for (int h = 0; h < 2; h++) { Object *item = who->hands.item[h]; if (who->hands.state[h] == HAND_HOLDING && item && item->type == OBJ_LADDER && !Ladder_isRaised(item) && item->leaning == false) l = item; } if (!l) return now; float most = fmaxf(0.0f, l->colliders[0].half_extents.y - SLIDE_KEEP), was = l->grip_shift; if (now > 0.002f) { float best = was, best_o = now; for (int k = 1; k * SLIDE_STEP <= 2.0f * most + SLIDE_STEP && best_o > 0.002f; k++) { for (int side = -1; side <= 1; side += 2) { float shift = clampf(was + side * k * SLIDE_STEP, -most, most); l->grip_shift = shift; Hands_placeHeld(who); float o = carriedOverlap(world, who); if (o < best_o - 0.002f) best = shift, best_o = o; if (best_o <= 0.002f) break; } } l->grip_shift = best; Hands_placeHeld(who); return best_o; } if (was != 0.0f) { l->grip_shift = was > 0.0f ? fmaxf(0.0f, was - 0.5f * SLIDE_STEP) : fminf(0.0f, was + 0.5f * SLIDE_STEP); Hands_placeHeld(who); if (carriedOverlap(world, who) > 0.002f) { l->grip_shift = was; Hands_placeHeld(who); } } return now; } /* A carried ladder, log or axe doesn't go through walls or trees: if turning (or walking) this frame pushed it into something, they don't turn (or move). */ static void keepCarriedClear(World *world, Object *who) { who->carry_blocked = false; bool carrying = who->hands.state[0] == HAND_HOLDING || who->hands.state[1] == HAND_HOLDING; if (carrying && who->carry_ok && !who->on_ladder && !Tools_isWorking(who)) { float now = slideLadder(world, who, carriedOverlap(world, who)); dipLadder(world, who, now); if (now > 0.002f) { Vec3 pos = who->position; float yaw = who->rotation.y; placeAt(who, pos, who->carry_yaw); /* without the turn */ float turned_back = carriedOverlap(world, who); placeAt(who, who->carry_pos, who->carry_yaw); /* without the step either */ float stayed = carriedOverlap(world, who); if (now <= fminf(turned_back, stayed) + 0.002f) { /* no deeper than before: fine (getting out) */ placeAt(who, pos, yaw); } else { who->carry_blocked = true; if (turned_back <= stayed + 0.002f) placeAt(who, pos, who->carry_yaw); } } } who->carry_pos = who->position; who->carry_yaw = who->rotation.y; who->carry_ok = true; } /* Others don't walk through what someone carries either: a person or the dog in it is moved out (away from it), light loose things are knocked aside. */ static void pushOffCarried(World *world, Object *who) { for (int h = 0; h < 2; h++) { Object *item = who->hands.item[h]; if (!item || who->hands.state[h] != HAND_HOLDING || (h == 1 && item == who->hands.item[0]) || !carriedLong(item)) continue; const Collider *col = &item->colliders[0]; Vec3 c = Object_colliderCenter(item, 0); int along = col->half_extents.x >= col->half_extents.y ? (col->half_extents.x >= col->half_extents.z ? 0 : 2) : (col->half_extents.y >= col->half_extents.z ? 1 : 2); Vec3 axis = col->axes[along]; float half = along == 0 ? col->half_extents.x : along == 1 ? col->half_extents.y : col->half_extents.z; int near[NEAR_MAX], nn = nearby(world, c.x, c.z, half + 1.0f, near); for (int q = 0; q < (nn < 0 ? world->count : nn); q++) { Object *o = world->objects[nn < 0 ? q : near[q]]; if (o == who || o == item || o->is_held || o->is_static || o->collider_count == 0 || o->broken) continue; bool living = o->is_character || o->type == OBJ_DOG; if (living ? carryTogether(o, who) : (o->weight >= CARRY_SOLID || o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->type == OBJ_DECAL)) continue; float depth = Physics_penetration(item, o); if (depth <= 0.005f) continue; /* away from the nearest point on its length */ Vec3 rel = vec3_sub(o->position, c); float t = clampf(vec3_dot(rel, axis), -half, half); Vec3 d = vec3_sub(o->position, vec3_add(c, vec3_scale(axis, t))); d.y = 0.0f; if (vec3_length(d) < 1e-3f) d = vec3_sub(o->position, who->position), d.y = 0.0f; if (vec3_length(d) < 1e-3f) continue; d = vec3_normalize(d); float push = fminf(depth + 0.01f, 0.25f); o->position = vec3_add(o->position, vec3_scale(d, push)); if (!living) o->velocity = vec3_add(o->velocity, vec3_scale(d, 1.5f)); Object_syncColliders(o); } } } void World_noiseOn(World *world, NoiseKind kind, Vec3 at, float loud, int surface) { if (world->noise_count >= WORLD_NOISES) return; world->noises[world->noise_count++] = (Noise){ kind, at, clampf(loud, 0.0f, 1.0f), surface }; } void World_noise(World *world, NoiseKind kind, Vec3 at, float loud) { World_noiseOn(world, kind, at, loud, 0); } void World_init(World *world) { memset(world, 0, sizeof(*world)); Object_seePeople(NULL, 0); /* (not the people of a world gone: loading a save builds a new one) */ world->rng = 0x2545F491u; world->hour = 12.0f; world->cash = WORLD_CASH; Weather_init(world); } void World_destroy(World *world) { Object_seePeople(NULL, 0); for (int i = 0; i < world->count; i++) Object_destroy(world->objects[i]); free(world->objects); if (world->has_terrain) Terrain_destroy(&world->terrain); Mesh_destroy(&world->flames); Chips_destroy(&world->chips); Grid_free(&world->grid); Mesh_destroy(&world->hose_piece); Mesh_destroy(&world->hose_tape); free(world->drops); memset(world, 0, sizeof(*world)); } bool World_add(World *world, Object *obj) { if (!obj) return false; if (world->count == world->capacity) { int cap = world->capacity ? world->capacity * 2 : 32; Object **grown = realloc(world->objects, (size_t)cap * sizeof(*grown)); if (!grown) { Object_destroy(obj); return false; } world->objects = grown; world->capacity = cap; } if (!obj->has_home && (obj->type == OBJ_AXE || obj->type == OBJ_SHEARS || obj->type == OBJ_CHAINSAW || obj->type == OBJ_UMBRELLA || obj->type == OBJ_SCYTHE)) { obj->has_home = true; /* (where it is at first is where it belongs) */ obj->home_spot = obj->position; obj->home_rot = obj->rotation; } world->objects[world->count++] = obj; return true; } bool World_buildTerrain(World *world) { world->flames = Shapes_flames(); Chips_init(&world->chips); world->hose_piece = Shapes_hosePiece(); world->hose_tape = Shapes_hoseTape(); world->has_terrain = Terrain_init(&world->terrain, world->objects, world->count); return world->has_terrain; } /* Lowest point of an object's main collider. */ static float bottomOf(const Object *o) { const Collider *c = &o->colliders[0]; float extent = fabsf(c->axes[0].y) * c->half_extents.x + fabsf(c->axes[1].y) * c->half_extents.y + fabsf(c->axes[2].y) * c->half_extents.z; return Object_colliderCenter(o, 0).y - extent; } /* What o just hit is hard: a stone, a wall, the road. Grass and people aren't. */ static bool hitHard(const Object *o) { if (o->touched) return Object_isHard(o->touched); return o->on_terrain && Site_hard(o->position.x, o->position.z); } /* Thrown things: bottles break when they hit something hard; anything lands eventually. */ static void landThrown(World *world, Object *o) { if (!o->thrown_by) return; float speed = vec3_length(o->velocity); bool hit = o->impact > BREAK_IMPACT || (o->is_grounded && speed > BREAK_SPEED); if (Object_isBottle(o) && hit && hitHard(o)) { Object_shatter(o, bottomOf(o)); World_noise(world, NOISE_SMASH, o->position, 0.9f); } else if (o->is_grounded) o->thrown_by = NULL; } /* Someone collecting trash got to the box of garbage bags: one for them. */ static void handOutBag(World *world, Object *o) { o->ai.wants_bag = false; int hand = Hands_freeHand(o); if (hand < 0) return; Object *bag = Object_create(OBJ_BAG, PALETTE_WARM, vec3_add(o->position, vec3(0.0f, 0.8f, 0.0f)), vec3_zero(), 0.05f); if (bag && World_add(world, bag)) Hands_pickUpObject(o, hand, bag); } /* A full ashtray tipped out into a scrap of paper in someone's hand (it goes to the bin then). */ static void emptyTray(World *world, Object *o) { Object *tin = o->ai.empty_tray; o->ai.empty_tray = NULL; int hand = Hands_freeHand(o); if (hand < 0 || !tin || tin->stored <= 0) return; Object *butts = Object_create(OBJ_TRASH, PALETTE_WARM, vec3_add(tin->position, vec3(0.0f, 0.16f, 0.0f)), vec3(0.07f, 0.03f, 0.06f), 0.04f); if (!butts) return; snprintf(butts->name, sizeof(butts->name), "Butts in paper"); if (!World_add(world, butts)) return; tin->stored = 0; if (!Hands_pickUpObject(o, hand, butts)) Hands_receive(o, hand, butts); /* (the tin a bit far: tipped into the hand all the same) */ } /* Light things lying about (paper and plastic litter, a pullover put down) blown along the ground by a strong wind, tumbling as they go. */ #define BLOW_PAPER 5.0f /* m/s: paper and plastic go from this wind ... */ #define BLOW_CLOTH 9.0f /* ... a pullover from this */ #define BLOW_SHOVE 0.9f /* m/s^2 for each m/s over it (against the ground's grip) */ static bool paperLike(const Object *o) { if (o->type == OBJ_HAT) return true; /* (light, and a cap catches the wind: it bowls along) */ return o->type == OBJ_TRASH && o->size.x > 0.0f && ((o->variant & 3) == PALETTE_WARM || (o->variant & 3) == PALETTE_STANDARD); } static void windBlows(World *world, Object *o, float dt) { if (o->is_static || o->is_held || o->broken || !o->is_grounded || o->lifted) return; bool paper = paperLike(o); bool cloth = o->type == OBJ_CLOTHES; if (!paper && !cloth) return; float speed = hypotf(world->wind.x, world->wind.z), from = paper ? BLOW_PAPER : BLOW_CLOTH; if (speed <= from) return; float shove = (speed - from) * BLOW_SHOVE * (cloth ? 0.6f : 1.0f) * dt / speed; o->velocity.x += world->wind.x * shove; o->velocity.z += world->wind.z * shove; if (paper) o->angular_velocity.y += (sinf(o->position.x * 3.1f) > 0.0f ? 1.0f : -1.0f) * 2.0f * shove * speed; } /* A gust tears paper or a pullover held in one hand out of it (the drunk and careless let go sooner); it flies off with the wind. */ #define SNATCH_PAPER 8.0f /* m/s: paper from this wind ... */ #define SNATCH_CLOTH 11.5f /* ... a pullover from this */ #define SNATCH_GUST 0.3f /* ... only in a gust this strong (0..1) */ #define SNATCH_RATE 0.35f /* chance a second for every 4 m/s over it, in a full gust */ static void windSnatches(World *world, Object *o, float dt) { if (!o->is_character || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < SNATCH_GUST || o->position.x > WORLD_BERLIN_FROM) return; float speed = hypotf(world->wind.x, world->wind.z); if (speed < SNATCH_PAPER) return; for (int h = 0; h < 2; h++) { Object *it = o->hands.item[h]; if (!it || o->hands.state[h] != HAND_HOLDING || o->hands.item[1 - h] == it) continue; /* (two hands on it: held tight) */ bool cloth = it->type == OBJ_CLOTHES; if (!cloth && !paperLike(it)) continue; float over = speed - (cloth ? SNATCH_CLOTH : SNATCH_PAPER); if (over <= 0.0f) continue; float rate = SNATCH_RATE * over / 4.0f * world->gust * (0.5f + fminf(o->drunk, 1.0f) + 0.5f * (1.0f - Object_caution(o))); if (dice(world) >= rate * dt) continue; Hands_drop(o, h); it->velocity = vec3(world->wind.x * 0.5f, 1.6f, world->wind.z * 0.5f); /* (up and away with it) */ it->is_grounded = false; if (o->type != OBJ_PLAYER) { o->ai.blown = it; o->ai.blown_t = 0.0f; } Ai_say(o, cloth ? "Hey - my pullover!" : "Whoops - gone with the wind!"); } } /* An open umbrella in a gust: blown inside out now and then (no use a moment, then pushed back right); the third time, or in a storm, the ribs bend and it's done for. */ #define FLIP_WIND 10.0f /* m/s in a gust: an open umbrella may blow inside out ... */ #define FLIP_GUST 0.4f /* ... a gust this strong (0..1) ... */ #define FLIP_RATE 0.3f /* ... chance a second for every 3 m/s over it, in a full gust */ #define FLIP_TIME 2.2f /* s till it's pushed back the right way */ #define FLIP_RUIN 3 /* blown inside out this often: broken */ #define RUIN_WIND 16.0f /* m/s: a gust like that breaks it at once */ static void windUmbrella(World *world, Object *o, float dt) { for (int h = 0; h < 2; h++) { Object *u = o->hands.item[h]; if (!u || u->type != OBJ_UMBRELLA || o->hands.state[h] != HAND_HOLDING || u->umbrella_ruined) continue; if (u->umbrella_flip > 0.0f) { if ((u->umbrella_flip -= dt) <= 0.0f) { u->umbrella_flip = 0.0f; if (o->type != OBJ_PLAYER && dice(world) < 0.4f) Ai_say(o, "There - right way round again."); } continue; } if (!u->umbrella_open || world->gust < FLIP_GUST) continue; float speed = hypotf(world->wind.x, world->wind.z), over = speed - FLIP_WIND; if (over <= 0.0f) continue; float careless = 0.6f + fminf(o->drunk, 1.0f) + 0.6f * (1.0f - Object_caution(o)); /* (the careful turn it into the wind) */ if (dice(world) >= FLIP_RATE * over / 3.0f * world->gust * careless * dt) continue; u->umbrella_flip = FLIP_TIME; u->umbrella_flips++; World_noiseOn(world, NOISE_RUSTLE, u->position, 0.6f, 0); /* (the canopy snapping over) */ if (u->umbrella_flips >= FLIP_RUIN || speed > RUIN_WIND) { u->umbrella_ruined = true; u->umbrella_open = false; if (o->type == OBJ_PLAYER) Ai_say(o, "The umbrella's had it - the ribs are bent."); else { /* (no use: down it goes, and in out of the rain) */ Ai_say(o, dice(world) < 0.5f ? "Oh no - my umbrella's had it!" : "Great. Broken."); Hands_drop(o, h); } } else { Ai_say(o, dice(world) < 0.5f ? "Argh - inside out!" : "Whoa - the umbrella!"); } } } /* A gust takes a cap off a head (a beanie hardly ever; not under a hood): it flies off with the wind and bowls along the ground; whoever's it is goes after it. Picked up by its owner, it goes back on. */ #define HAT_WIND 9.0f /* m/s in a gust: a cap may go from this ... */ #define HAT_GUST 0.4f #define HAT_RATE 0.3f /* ... chance a second for every 3 m/s over it, in a full gust (a beanie: a tenth) */ static void windHat(World *world, Object *o, float dt) { if (!o->look.hat || o->look.hood_up || o->indoors > 0.0f || o->in_tent || o->riding || world->gust < HAT_GUST || o->position.x > WORLD_BERLIN_FROM) return; /* (Berlin: indoors) */ float speed = hypotf(world->wind.x, world->wind.z), over = speed - HAT_WIND; if (over <= 0.0f) return; float rate = HAT_RATE * over / 3.0f * world->gust * (o->look.hat == HAT_BEANIE ? 0.1f : 1.0f) / (1.0f + 3.0f * (float)o->hats_lost); /* (lost it before: pulled down tight) */ if (dice(world) >= rate * dt) return; Vec3 head = vec3_add(o->position, vec3(0.0f, o->look.height + 0.05f, 0.0f)); Object *hat = Object_create(OBJ_HAT, PALETTE_STANDARD, head, vec3(0.2f, 0.1f, 0.2f), (float)o->look.hat); if (!hat) return; hat->look.hat_color = o->look.hat_color; hat->owner = o; hat->rotation.y = o->rotation.y; Object_syncColliders(hat); if (!World_add(world, hat)) return; hat->velocity = vec3(world->wind.x * 0.55f, 1.8f, world->wind.z * 0.55f); hat->angular_velocity = vec3(3.0f, 1.0f, 2.0f); hat->is_grounded = false; o->look.hat = HAT_NONE; o->hats_lost++; if (o->type != OBJ_PLAYER) { o->ai.blown = hat; o->ai.blown_t = 0.0f; } Ai_say(o, o->type == OBJ_PLAYER ? "My cap - the wind took it!" : dice(world) < 0.5f ? "Hey - my cap!" : "Oi! Come back here, cap!"); } /* Their own hat in hand: back on the head. */ static void hatBackOn(Object *o) { for (int h = 0; h < 2; h++) { Object *hat = o->hands.item[h]; if (!hat || hat->type != OBJ_HAT || hat->owner != o || o->look.hat) continue; Hands_drop(o, h); o->look.hat = hat->variant; o->look.hat_color = hat->look.hat_color; Object_useUp(hat); if (o->type == OBJ_PLAYER) Ai_say(o, "Cap back on."); } } void World_update(World *world, float dt, float game_dt) { Object_indexKinds(world->objects, world->count); world->dt_props = dt; world->game_dt = game_dt; Object_seePeople(world->objects, world->count); Clothes_update(world); Weather_update(world, dt, game_dt); Car_serve(world); Tent_update(world, dt); showProps(world); /* (who's a person, where the fires are: looked up again and again) */ for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->type == OBJ_DOG) Dog_update(o, world->objects, world->count, dt); if (o->type == OBJ_LADDER) Ladder_update(world, o, dt); if (!o->is_character) continue; /* How work goes: fresh and sober, and liking the job, is quickest */ o->work_rate = Object_vigor(o) * (1.0f - 0.45f * fminf(o->drunk, 1.0f)) * Ai_workRate(o) * (Injury_castKind(o) == INJ_ARM ? 0.6f : Injury_castKind(o) == INJ_LEG ? 0.85f : 1.0f); /* (a cast: slower at it) */ /* Every finished tool job is practice */ if (o->work_state.jobs_done > o->jobs_seen) { Ai_practice(o, o->work_state.last_job == JOB_FELL ? ORDER_TREES : ORDER_FIREWOOD, 0.015f * (float)(o->work_state.jobs_done - o->jobs_seen)); o->jobs_seen = o->work_state.jobs_done; } Condition_useGrid(&world->grid); Condition_update(o, world->objects, world->count, dt, game_dt); Injury_update(o, game_dt); if (o->indoors > 0.0f) { /* in the house: out of sight a while, then out at the door */ if (o->ai.sheltering && world->rain > 0.1f) o->indoors = fmaxf(o->indoors, 25.0f + 10.0f * (float)(i % 4)); /* (till the rain's over) */ o->indoors -= dt; if (world->fest.stage == FEST_STORM || world->fest.stage == FEST_WARNED) o->indoors = fmaxf(o->indoors, 20.0f); /* (the storm: in till it's over) */ Vec3 door = vec3_length(o->inside_door) > 0.1f ? o->inside_door : Site_houseDoor(); o->position = vec3(door.x, Terrain_height(&world->terrain, door.x, door.z), door.z); o->velocity = o->movement = vec3_zero(); Object_syncColliders(o); if (o->indoors <= 0.0f) { o->indoors = 0.0f; o->body.replant = true; o->ai.sheltering = false; o->inside_door = vec3_zero(); } continue; } if (o->type == OBJ_HUMAN && !o->riding) { Ai_useGrid(&world->grid); Ai_useWorld(world); Ai_update(o, world->objects, world->count, dt); } if (o->type == OBJ_HUMAN && o->ai.wants_bag) handOutBag(world, o); if (o->type == OBJ_HUMAN && o->ai.empty_tray) emptyTray(world, o); if (o->type == OBJ_HUMAN && o->body.stepped_off) { /* lost the balance up on something */ o->body.stepped_off = false; if (worldDice(world) < 0.5f) Ai_say(o, worldDice(world) < 0.5f ? "Whoa!" : "Oops - nearly..."); } } for (int i = 0; i < world->count; i++) { /* rolling over a shoulder: on the way it was going, whatever anyone wants */ Object *o = world->objects[i]; if (!o->is_character || !Body_isRolling(&o->body)) continue; float t = o->body.shoulder_roll; float v = ROLL_ON * (0.4f + 0.6f * smoothstepf(0.0f, 0.2f, t)) * (1.0f - 0.7f * smoothstepf(0.75f, 1.0f, t)); o->movement = vec3_scale(o->body.roll_dir, v * o->look.height / 1.75f); o->move_local = vec3(0.0f, 0.0f, 1.0f); } Physics_useGrid(&world->grid); for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; windBlows(world, o, dt); windSnatches(world, o, dt); windUmbrella(world, o, dt); windHat(world, o, dt); hatBackOn(o); Physics_step(o, world->objects, world->count, &world->terrain, dt); landThrown(world, o); if (o->is_character) { /* (a safety net: whoever got through the floor somehow is back where they last stood) */ if (o->is_grounded && !o->riding && o->indoors <= 0.0f) o->last_ground = o->position; else if (o->position.y < VOID_BELOW && vec3_length(o->last_ground) > 0.1f) { o->position = vec3_add(o->last_ground, vec3(0.0f, 0.05f, 0.0f)); o->velocity = o->movement = vec3_zero(); o->air_fall = 0.0f; o->body.replant = true; Object_syncColliders(o); } } } Grid_build(&world->grid, world->objects, world->count); /* (where things are now: for the questions below) */ Forestry_update(world, dt); Hands_solveGrabs(world->objects, world->count, dt); Hose_update(world, dt); for (int i = 0; i < world->count; i++) { /* chips off the axe, sawdust out of the chainsaw */ Object *o = world->objects[i]; if (!o->is_character) continue; Work *w = &o->work_state; Vec3 out = vec3_sub(w->point, o->position); out = vec3(out.x, 0.0f, out.z); out = vec3_length(out) > 1e-3f ? vec3_scale(vec3_normalize(out), -1.0f) : vec3(0.0f, 0.0f, 1.0f); /* (back towards the worker) */ if (w->struck > 0 && w->tool == TOOL_AXE) { World_noise(world, NOISE_CHOP, w->contact > 0.5f ? w->edge : w->point, 0.9f); Vec3 side = vec3(-out.z, 0.0f, out.x); float k = (world->rng & 1u) ? 1.0f : -1.0f; world->rng = world->rng * 1103515245u + 12345u; Chips_spray(&world->chips, w->contact > 0.5f ? w->edge : w->point, vec3_add(vec3_scale(side, 0.8f * k), vec3_add(vec3_scale(out, 0.5f), vec3(0.0f, 0.5f, 0.0f))), 10, false); } w->struck = 0; if (w->tool == TOOL_CHAINSAW && w->saw_in > 0.6f && w->job != JOB_NONE && Tools_isWorking(o)) { /* (sawdust: a steady stream, out of the cut towards the worker and down) */ float want = 70.0f * dt + (float)(world->rng % 1000u) / 1000.0f; world->rng = world->rng * 1103515245u + 12345u; Chips_spray(&world->chips, w->point, vec3_add(out, vec3(0.0f, -0.4f, 0.0f)), (int)want, true); } } Chips_update(&world->chips, &world->terrain, world->wind, dt); for (int i = 0; i < world->count; i++) { /* smoke off the fires (with the wind) */ Object *f = world->objects[i]; if (f->type != OBJ_FIREPLACE || !f->burning) continue; if ((f->smoke_t -= dt) > 0.0f) continue; f->smoke_t = 0.22f / fmaxf(0.4f, Camp_flameSize(f)); Chips_smoke(&world->chips, vec3_add(f->position, vec3(0.0f, 0.6f + 0.4f * Camp_flameSize(f), 0.0f))); } { /* woodpiles count what really lies on them; what fell off isn't on one */ Object *piles[16]; int np = 0; for (int i = 0; i < world->count && np < 16; i++) if (world->objects[i]->type == OBJ_WOODPILE) piles[np++] = world->objects[i]; for (int k = 0; k < np; k++) piles[k]->stored = Object_pileCount(piles[k], world->objects, world->count); for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (o->type != OBJ_FIREWOOD || !o->stored || o->is_held) continue; bool on = false; for (int k = 0; k < np && !on; k++) on = Object_onPile(piles[k], o); if (!on) o->stored = 0; } } if (world->has_terrain) Nav_update(world->objects, world->count, &world->terrain); /* Walking characters move their legs and trample the meadow */ const Object *player = NULL; for (int i = 0; i < world->count && !player; i++) if (world->objects[i]->type == OBJ_PLAYER) player = world->objects[i]; for (int i = 0; i < world->count; i++) { Object *o = world->objects[i]; if (!o->is_character) continue; if (player) { /* (far off: less detail; a bit of give either way, no flicking back and forth) */ float d = hypotf(o->position.x - player->position.x, o->position.z - player->position.z); o->far_off = (o->far_off ? d > FAR_DETAIL - 5.0f : d > FAR_DETAIL) || o->riding != NULL; /* (in the car: nothing to keep out of) */ } takeKnock(o); takeBump(world, o, dt); halfOver(o, dt); animateCharacter(world, o, dt); keepCarriedClear(world, o); pushOffCarried(world, o); if (o->on_terrain && o->ground_speed > TRAMPLE_MIN_SPEED) Terrain_trample(&world->terrain, o->position.x, o->position.z, dt); } Terrain_recover(&world->terrain, game_dt); Wetness_update(&world->terrain, dt, game_dt); dryPuddles(world, game_dt); } #define SHADOW_REACH 70.0f /* casters further than this from the shadowed area don't matter */ #define SMALL_REACH 90.0f /* bottles, tools, crates: too small to see further off */ #define TREE_REACH 220.0f /* the forest thins out into the distance */ #define BIG_SIZE 5.0f /* buildings are always drawn */ static bool flatOnGround(const Object *o) { return o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->type == OBJ_DECAL; } static float flatDistance2(Vec3 a, Vec3 b) { float dx = a.x - b.x, dz = a.z - b.z; return dx * dx + dz * dz; } /* Far enough away that it isn't worth drawing? */ static bool outOfSight(const Object *o, Vec3 eye) { if (o->is_character || o->type == OBJ_DECAL) return false; float d2 = flatDistance2(o->position, eye); if (o->type == OBJ_TREE) return d2 > TREE_REACH * TREE_REACH; if (fmaxf(o->size.x, fmaxf(o->size.y, o->size.z)) >= BIG_SIZE) return false; return d2 > SMALL_REACH * SMALL_REACH; } static Mat4 scaled(Vec3 s) { Mat4 m = mat4_identity(); m.m[0] = s.x; m.m[5] = s.y; m.m[10] = s.z; return m; } /* Flames over a fire: they flicker, and grow with the wood in it; the wind bends them over. */ static void drawFlames(Renderer *renderer, const Mesh *flames, Vec3 at, float width, float height, float spin, Vec3 wind) { float t = renderer->boil_time; float flicker = 0.85f + 0.3f * (0.5f + 0.5f * sinf(t * 23.0f + spin) * sinf(t * 7.3f + 1.0f)); Mat4 lean = mat4_identity(); /* (sheared downwind: the tips go furthest) */ lean.m[4] = clampf(wind.x * 0.07f, -0.8f, 0.8f); lean.m[6] = clampf(wind.z * 0.07f, -0.8f, 0.8f); Mat4 model = mat4_mul(mat4_translate(at), mat4_mul(lean, mat4_mul(mat4_rotate_y(t * 2.1f + spin), scaled(vec3(width, height * flicker, width))))); Renderer_drawMesh(renderer, flames, model); } /* Wood burning down: darker, glowing, smaller. */ static void drawCharred(Renderer *renderer, const Object *o) { float c = o->charred, shrink = 1.0f - 0.5f * c; Mat4 model = mat4_mul(mat4_translate(vec3_add(o->position, o->pivot)), mat4_mul(mat4_rotate_yxz(o->rotation), scaled(vec3(shrink, 0.6f + 0.4f * shrink, shrink)))); float dark = 1.0f - 0.8f * smoothstepf(0.0f, 0.45f, c); Renderer_drawMeshTinted(renderer, &o->mesh, model, vec3(dark, dark * 0.95f, dark * 0.9f), o->glow); } /* You're sitting in this car. */ static bool playerIn(const World *world, const Object *car) { for (int i = 0; i < world->count; i++) if (world->objects[i]->type == OBJ_PLAYER) return world->objects[i]->riding == car; return false; } void World_drawShadows(const World *world, Renderer *renderer) { for (int i = 0; i < world->count; i++) { const Object *obj = world->objects[i]; if (flatOnGround(obj) || (obj->is_character && (obj->toilet || obj->riding || obj->indoors > 0.0f)) || (!obj->is_character && !obj->mesh.vao)) continue; /* flat on the ground, hidden, nothing to see: no shadow */ bool big = fmaxf(obj->size.x, obj->size.z) >= BIG_SIZE * 3.0f; if (!big && flatDistance2(obj->position, renderer->shadow_focus) > SHADOW_REACH * SHADOW_REACH) continue; if (obj->is_character) { FigurePose pose = figurePose(obj); /* the player casts a shadow too */ Renderer_drawFigure(renderer, &pose); } else if (obj->type == OBJ_DOG) { Dog_draw(renderer, obj); /* (painted like the people: the shadow pass draws her the same way) */ } else { Renderer_drawShadowCaster(renderer, &obj->mesh, Object_modelMatrix(obj)); } } } /* Where a tree, log or stump is being cut: a pale band of cut wood round the bark on the side it's cut from, wider and taller the further along the cut is (in strips that follow the round of the wood). */ static void drawMark(Renderer *renderer, const Mesh *block, const Object *o) { float radius = o->type == OBJ_TREE ? (o->variant ? 0.16f : 0.2f) : o->size.x * 0.5f; float k = clampf(o->mark, 0.0f, 1.0f); float around = 0.5f + 0.9f * k, height = 0.05f + 0.07f * k; /* (half the angle it covers; how tall) */ const int strips = 5; float yaw0 = atan2f(o->mark_out.x, o->mark_out.z); Mat4 base = Object_modelMatrix(o); for (int i = 0; i < strips; i++) { float a = yaw0 + around * ((float)i + 0.5f - (float)strips * 0.5f) / (float)strips * 2.0f; Vec3 out = vec3(sinf(a), 0.0f, cosf(a)); Vec3 local = vec3_add(vec3(0.0f, o->mark_at.y, 0.0f), vec3_scale(out, radius - 0.003f)); float w = 2.0f * radius * sinf(around / (float)strips) + 0.004f; Mat4 m = mat4_mul(base, mat4_mul(mat4_translate(local), mat4_mul(mat4_rotate_y(a), scaled(vec3(w, height, 0.012f))))); Renderer_drawMeshTinted(renderer, block, m, vec3(0.93f, 0.8f, 0.55f), 0.0f); } } /* Trees and bushes bending with the wind (a lean growing with the height, swaying with the gusts; each its own beat). Their shadows stay put. */ #define SWAY_TREE 0.06f /* lean per m of height in the strongest wind */ #define SWAY_BUSH 0.10f static Mat4 windSway(const World *world, const Object *o, float t) { float k = o->type == OBJ_BUSH ? SWAY_BUSH : SWAY_TREE; float speed = hypotf(world->wind.x, world->wind.z), level = fminf(speed / WIND_MAX, 1.4f); float phase = o->position.x * 0.37f + o->position.z * 0.61f; float beat = 0.65f + 0.35f * sinf(t * (o->type == OBJ_BUSH ? 2.6f : 1.4f) + phase); Vec3 dir = speed > 0.01f ? vec3_scale(world->wind, 1.0f / speed) : vec3(1.0f, 0.0f, 0.0f); float lean = k * (level * beat + 0.04f * sinf(t * 0.8f + phase)); /* (a little even when it's calm) */ Mat4 shear = mat4_identity(); shear.m[4] = dir.x * lean; shear.m[6] = dir.z * lean; Vec3 base = o->position; return mat4_mul(mat4_translate(base), mat4_mul(shear, mat4_translate(vec3_scale(base, -1.0f)))); } void World_draw(World *world, Renderer *renderer, bool draw_player) { Terrain_syncTexture(&world->terrain); { float holes[TERRAIN_PITS][6]; for (int i = 0; i < world->terrain.pit_count; i++) { const TerrainPit *p = &world->terrain.pits[i]; holes[i][0] = p->x; holes[i][1] = p->z; holes[i][2] = p->cos_yaw; holes[i][3] = p->sin_yaw; holes[i][4] = p->half_x; holes[i][5] = p->half_z; } Renderer_setHoles(renderer, (const float (*)[6])holes, world->terrain.pit_count); } for (int i = 0; i < TERRAIN_TILES * TERRAIN_TILES; i++) Renderer_drawTerrain(renderer, &world->terrain.tiles[i], world->terrain.wear_texture, world->terrain.wet.texture, world->terrain.surface_texture, TERRAIN_SIZE, TERRAIN_GRASS_DEPTH); for (int i = 0; i < world->count; i++) { const Object *obj = world->objects[i]; if (obj->type == OBJ_PLAYER && !draw_player) continue; /* first person */ if (!obj->is_character && obj->type != OBJ_DOG && !obj->mesh.vao) continue; /* (invisible: a building's walls) */ if (outOfSight(obj, renderer->viewer_pos)) continue; if (obj->is_character && (obj->toilet || obj->indoors > 0.0f)) continue; /* in the toilet, in the house */ if (obj->is_character) { FigurePose pose = figurePose(obj); Renderer_drawFigure(renderer, &pose); if (obj->on_fire > 0.0f) { bool down = Body_isDown(&obj->body); drawFlames(renderer, &world->flames, vec3_add(obj->position, vec3(0.0f, down ? 0.0f : 0.3f, 0.0f)), down ? 1.3f : 0.8f, down ? 0.9f : 2.0f, (float)i, world->wind); } } else if (obj->type == OBJ_CAR && !draw_player && playerIn(world, obj)) { /* (from the driver's seat: inside it) */ Renderer_drawMesh(renderer, &obj->inside, Object_modelMatrix(obj)); } else if (obj->type == OBJ_DOG) { Dog_draw(renderer, obj); } else if (obj->type == OBJ_HOSE) { Renderer_drawMesh(renderer, &obj->mesh, Object_modelMatrix(obj)); Hose_draw(world, renderer, obj); } else if (obj->charred > 0.0f && obj->mesh.vao) { drawCharred(renderer, obj); } else if (flatOnGround(obj)) { Renderer_drawDecal(renderer, &obj->mesh, Object_modelMatrix(obj), TERRAIN_SIZE); } else if (Object_isFood(obj) && obj->grill > 0.0f) { /* browning, then black */ float brown = smoothstepf(0.0f, FOOD_DONE, obj->grill), black = smoothstepf(FOOD_DONE * 1.2f, FOOD_BURNT, obj->grill); Vec3 tint = vec3_lerp(vec3_lerp(vec3(1.0f, 1.0f, 1.0f), vec3(0.9f, 0.78f, 0.62f), obj->type == OBJ_SAUSAGE ? 0.0f : brown), vec3(0.2f, 0.17f, 0.15f), black); Renderer_drawMeshTinted(renderer, &obj->mesh, Object_modelMatrix(obj), tint, 0.0f); } else if ((obj->type == OBJ_TREE && !obj->toppled) || obj->type == OBJ_BUSH) { Renderer_drawMesh(renderer, &obj->mesh, mat4_mul(windSway(world, obj, renderer->boil_time), Object_modelMatrix(obj))); } else { Renderer_drawMesh(renderer, &obj->mesh, Object_modelMatrix(obj)); if (obj->type == OBJ_FIREPLACE && obj->grill_mesh.vao) { /* the grate swings a little on its chain */ float t = renderer->boil_time; const float sway = 1.35f / GRILL_DROP; /* (a long chain: slower, the grate goes as far) */ Mat4 swing = mat4_mul(mat4_rotate_x(0.035f * sway * sinf(t * 1.3f * sqrtf(sway))), mat4_rotate_z(0.03f * sway * sinf(t * 0.9f * sqrtf(sway) + 1.0f))); Renderer_drawMesh(renderer, &obj->grill_mesh, mat4_mul(mat4_translate(vec3_add(obj->position, vec3(0.0f, GRILL_TOP, 0.0f))), swing)); } if (obj->mark > 0.0f) drawMark(renderer, &world->chips.mesh, obj); if (obj->burning) { /* more wood: bigger, and taller still */ float size = Camp_flameSize(obj), width = size * obj->size.x / 1.4f; drawFlames(renderer, &obj->flames, vec3_add(obj->position, vec3(0.0f, 0.05f, 0.0f)), width, width * (0.85f + 0.2f * size), 0.0f, world->wind); } } } } void World_drawAir(World *world, Renderer *renderer) { Chips_draw(&world->chips, renderer); Weather_draw(world, renderer); }