#include "world/camp.h" #define FIRE_RADIUS(f) ((f)->size.x * 0.4f) /* wood this close to the middle is in the fire */ #define BURN_PER_KG 720.0f /* game seconds a kg of wood takes to burn down */ #define CATCH_RADIUS(f) ((f)->size.x * (0.36f + 0.1f * Camp_flameSize(f))) /* this close to the middle sets you alight */ #define BURN_HURT 0.06f /* stamina per second while burning */ #define ROLL_OUT 1.2f /* seconds rolling on the ground put it out */ #define BURN_OUT 25.0f /* ... or it goes out by itself */ #define GRILL_REACH 1.3f /* food this close over a fire gets grilled (FOOD_DONE etc. in object.h) */ #define GRILL_TAKE 1.9f /* food on the grill can be taken off from this far */ #define TRASH_REACH 1.3f #define BIN_REACH 1.2f #define FRONT_COS 0.35f #define BAG_FULL 24 #define FIRE_DRIES 0.08f /* liters a second a burning fire dries off again */ #define LITERS_PER_KG 0.25f /* water it takes per kg of wood burning */ #define WET_OUT 3.0f /* water on a burning person: seconds of rolling it's worth ... */ #define PEE_OUT 1.5f /* ... pee */ bool camp_doused = false; static float flatDist(Vec3 a, Vec3 b) { return sqrtf((a.x - b.x) * (a.x - b.x) + (a.z - b.z) * (a.z - b.z)); } static bool inFire(const Object *fire, const Object *o) { return Object_isFuel(o) && !o->is_held && !o->lifted && flatDist(o->position, fire->position) < FIRE_RADIUS(fire) + 0.15f && o->position.y < fire->position.y + 1.0f; } /* Wood left in the fire (kg). */ static float fuelIn(Object *fire, Object *const *objects, int count) { float kg = 0.0f; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (inFire(fire, o)) kg += o->weight * (1.0f - o->charred); } return kg; } /* Everything in a burning fire chars, glows, and falls to ashes; a piece burns about as long as it weighs. */ static void burn(Object *fire, Object *const *objects, int count, float game_dt) { for (int i = 0; i < count; i++) { Object *o = objects[i]; if (!inFire(fire, o)) continue; o->charred += game_dt / (BURN_PER_KG * fmaxf(o->weight, 0.3f)); o->glow = smoothstepf(0.25f, 0.7f, o->charred); if (o->charred >= 1.0f) Object_useUp(o); } } /* Someone walked into the fire, or is burning: rolling on the ground puts it out. */ static void scorch(Object *who, Object *const *objects, int count, float dt) { if (who->on_fire <= 0.0f) { objects = Object_kind(KIND_FIREPLACES, objects, count, &count); for (int i = 0; i < count; i++) { const Object *f = objects[i]; if (f->type == OBJ_FIREPLACE && f->burning && flatDist(who->position, f->position) < CATCH_RADIUS(f) && fabsf(who->position.y - f->position.y) < 0.6f) { who->on_fire = 0.001f; who->fire_roll = 0.0f; } } return; } who->on_fire += dt; who->needs.stamina = fmaxf(0.0f, who->needs.stamina - BURN_HURT * dt); if (Body_isDown(&who->body)) who->fire_roll += dt; if (who->fire_roll >= ROLL_OUT || who->on_fire >= BURN_OUT) { who->on_fire = 0.0f; who->fire_roll = 0.0f; } } static void grill(Object *food) { food->grilled = true; if (food->type == OBJ_SAUSAGE) { food->nourish = 0.12f; food->refresh = 0.05f; Mesh mesh = Shapes_sausage(food->size.y, true); if (mesh.vao) { Mesh_destroy(&food->mesh); food->mesh = mesh; } } else { food->nourish *= 1.6f; food->refresh *= 1.5f; } } /* Over the fire for dt more: done, then darker, then charcoal (hardly food any more). The food if it just got done, too dark or burnt, else NULL. */ static Object *cook(Object *food, float dt) { if (food->burnt) return NULL; bool light = food->grill < FOOD_DARK; food->grill += dt; if (light && food->grill >= FOOD_DARK) return food; /* (getting black: a warning) */ if (!food->grilled && food->grill >= FOOD_DONE) { grill(food); return food; } if (food->grill >= FOOD_BURNT) { food->burnt = true; food->nourish *= 0.25f; food->refresh = 0.0f; return food; } return NULL; } #define STOVE_REACH 0.9f /* food held this close over a stove cooks */ /* Food held over a stove cooks like over a fire. */ static Object *cookOnStove(const Object *stove, Object *const *objects, int count, float dt) { Object *done = NULL; for (int k = 0; k < count; k++) { Object *who = objects[k]; if (!who->is_character || flatDist(who->position, stove->position) > 1.6f) continue; for (int h = 0; h < 2; h++) { Object *food = who->hands.item[h]; if (!food || !Object_isFood(food) || flatDist(food->position, stove->position) > STOVE_REACH) continue; Object *changed = cook(food, dt); if (changed) done = changed; } } return done; } Object *Camp_update(Object *const *objects, int count, float dt, float game_dt) { Object *done = NULL; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (Object_isFuel(o)) o->glow = 0.0f; /* glowing only while in a burning fire */ if (o->is_character) scorch(o, objects, count, dt); if (o->type == OBJ_FURNITURE && o->variant == FURN_STOVE) { Object *cooked = cookOnStove(o, objects, count, dt); if (cooked) done = cooked; } if (o->on_grill) { /* on the grate: slower than held right over the flames */ if (o->is_held || !o->mesh.vao) o->on_grill = NULL; else if (o->on_grill->burning) { Object *changed = cook(o, dt * 0.5f); if (changed) done = changed; } } } for (int i = 0; i < count; i++) { Object *fire = objects[i]; if (fire->type != OBJ_FIREPLACE) continue; if (fire->burning) burn(fire, objects, count, game_dt); fire->water = fire->burning ? fmaxf(0.0f, fire->water - FIRE_DRIES * dt) : 0.0f; fire->fuel = fuelIn(fire, objects, count); if (fire->fuel <= 0.01f) fire->burning = false; /* burnt out */ if (!fire->burning) continue; /* Food in a hand close over the fire */ int people = 0; Object *const *who_all = Object_kind(KIND_CHARACTERS, objects, count, &people); for (int k = 0; k < people; k++) { Object *who = who_all[k]; if (!who->is_character) continue; for (int h = 0; h < 2; h++) { Object *food = who->hands.item[h]; if (!food || !Object_isFood(food)) continue; if (flatDist(food->position, fire->position) > GRILL_REACH || food->position.y > fire->position.y + 1.8f) continue; Object *changed = cook(food, dt); if (changed) done = changed; } } } return done; } Object *Camp_fireNear(Object *const *objects, int count, Vec3 pos, float range, bool burning_only) { Object *best = NULL; objects = Object_kind(KIND_FIREPLACES, objects, count, &count); for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->type != OBJ_FIREPLACE || (burning_only && !o->burning)) continue; float d = flatDist(o->position, pos); if (d < range) { range = d; best = o; } } return best; } static bool ahead(const Object *who, Vec3 at) { Vec3 to = vec3_sub(at, who->position); to.y = 0.0f; float d = vec3_length(to); return d < 1e-3f || vec3_dot(vec3_scale(to, 1.0f / d), Object_facing(who)) > FRONT_COS; } Object *Camp_inFront(const Object *who, Object *const *objects, int count, ObjectType type) { float reach = type == OBJ_FIREPLACE ? 1.0f : BIN_REACH; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o->type != type) continue; if (Hands_gapFrom(who->position, who->look.height, o) > reach || !ahead(who, o->position)) continue; return o; } return NULL; } /* A little random number from a position (the wood should not land in one heap). */ static float jitter(Vec3 p, float salt) { float s = sinf(p.x * 12.9898f + p.z * 78.233f + salt) * 43758.5453f; return s - floorf(s); } int Camp_feed(Object *who, Object *fire) { for (int h = 0; h < 2; h++) { Object *wood = who->hands.item[h]; if (!wood || !Object_isFuel(wood)) continue; Hands_drop(who, h); /* Into the middle, lying every which way */ float a = jitter(wood->position, 1.0f) * 2.0f * PI_F, r = jitter(wood->position, 2.0f) * FIRE_RADIUS(fire) * 0.5f; Vec3 at = vec3_add(fire->position, vec3(cosf(a) * r, 0.25f + 0.15f * fire->fuel / 6.0f, sinf(a) * r)); wood->rotation = vec3(PI_F / 2.0f, a * 1.7f, 0.15f * (jitter(at, 3.0f) - 0.5f)); wood->position = vec3_sub(at, wood->pivot); wood->velocity = vec3_zero(); wood->angular_velocity = vec3_zero(); Object_syncColliders(wood); return FED_WOOD; } for (int h = 0; h < 2; h++) { /* food goes on the grill */ Object *food = who->hands.item[h]; if (!food || !Object_isFood(food)) continue; Hands_drop(who, h); float a = jitter(food->position, 4.0f) * 2.0f * PI_F, r = 0.08f + 0.15f * jitter(food->position, 5.0f); Vec3 at = vec3_add(fire->position, vec3(cosf(a) * r, GRILL_TOP - GRILL_DROP + 0.03f, sinf(a) * r)); food->rotation = vec3(PI_F / 2.0f, a, 0.0f); food->position = vec3_sub(at, food->pivot); food->velocity = vec3_zero(); food->on_grill = fire; Object_syncColliders(food); return FED_GRILL; } return FED_NOTHING; } Object *Camp_onGrill(Object *const *objects, int count, const Object *fire) { Object *best = NULL; for (int i = 0; i < count; i++) { Object *food = objects[i]; if (food->on_grill == fire && (!best || food->grill > best->grill)) best = food; } return best; } Object *Camp_takeOff(Object *who, Object *fire, Object *const *objects, int count) { int hand = Hands_freeHand(who); if (hand < 0) return NULL; Object *best = Camp_onGrill(objects, count, fire); if (!best || flatDist(who->position, best->position) > GRILL_TAKE) return NULL; best->on_grill = NULL; return Hands_receive(who, hand, best) ? best : NULL; } #define LITERS_PER_SIP 0.1f /* (a beer: half a liter) */ int Camp_pourOnFire(Object *fire, Object *bottle) { if (!fire || !fire->burning || !bottle || !Object_isBottle(bottle) || bottle->sips <= 0 || bottle->broken) return POURED_NOTHING; float liters = LITERS_PER_SIP * (float)bottle->sips; bottle->sips = 0; if (bottle->type == OBJ_VODKA) { /* whoosh */ fire->water = 0.0f; return POURED_FLARE; } fire->water += liters; if (fire->water < Camp_waterNeeded(fire)) return POURED_HISS; fire->burning = false; fire->water = 0.0f; camp_doused = true; return POURED_OUT; } bool Camp_douse(Object *target, Object *bottle) { if (!bottle || !Object_isBottle(bottle) || bottle->sips <= 0 || bottle->broken || target->on_fire <= 0.0f) return false; bottle->sips = 0; target->on_fire = 0.0f; target->fire_roll = 0.0f; return true; } bool Camp_light(Object *fire) { if (fire->fuel <= 0.0f) return false; fire->burning = true; fire->water = 0.0f; return true; } float Camp_waterNeeded(const Object *fire) { return 0.2f + LITERS_PER_KG * fire->fuel; } float Camp_flameSize(const Object *fire) { float size = fminf(0.55f + 0.4f * sqrtf(fire->fuel / 3.0f), 2.0f); return size * (1.0f - 0.6f * clampf(fire->water / Camp_waterNeeded(fire), 0.0f, 1.0f)); /* hissing, smaller */ } Object *Camp_trashInFront(const Object *who, Object *const *objects, int count) { Object *best = NULL; float best_d = TRASH_REACH; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (!Object_isTrash(o) || o->is_held) continue; float d = flatDist(o->position, who->position); if (d < best_d && ahead(who, o->position)) { best_d = d; best = o; } } return best; } Object *Camp_bagInHand(const Object *who) { for (int h = 0; h < 2; h++) if (who->hands.item[h] && who->hands.item[h]->type == OBJ_BAG) return who->hands.item[h]; return NULL; } bool Camp_bag(Object *bag, Object *trash) { if (bag->stored >= BAG_FULL) return false; bag->stored++; Object_fillBag(bag, trash->weight); Object_useUp(trash); return true; } int Camp_throwAway(Object *who) { int gone = 0; for (int h = 0; h < 2; h++) { Object *o = who->hands.item[h]; if (!o || (o->type != OBJ_BAG && !Object_isTrash(o))) continue; gone += o->type == OBJ_BAG ? o->stored : 1; Hands_drop(who, h); Object_useUp(o); } return gone; } #define BOTTLE_PLACE_REACH 0.85f /* a table or crate this close: the bottle goes on it */ #define EMPTIES_AROUND 2.5f /* other empties standing this near ... */ #define EMPTIES_REACH 0.9f /* ... and one of them in reach: it joins them */ /* A spot on top of a table or crate for a bottle (not on another one). */ static bool spotOnTop(const Object *who, const Object *on, Object *const *objects, int count, Vec3 *spot) { Vec3 c = Object_colliderCenter(on, 0); const Collider *col = &on->colliders[0]; float top = c.y + fabsf(col->axes[1].y) * col->half_extents.y; Vec3 to = vec3_sub(who->position, c); to.y = 0.0f; /* the near edge, a little in */ for (int k = 0; k < 6; k++) { float in = 0.12f + 0.08f * (float)(k / 2), aside = (k & 1 ? 1.0f : -1.0f) * 0.1f * (float)(k / 2); Vec3 dir = vec3_length(to) > 1e-3f ? vec3_normalize(to) : vec3(1.0f, 0.0f, 0.0f); Vec3 side = vec3(-dir.z, 0.0f, dir.x); /* (how far the top reaches that way: to whichever side it meets first) */ float a0 = fabsf(vec3_dot(dir, col->axes[0])), a2 = fabsf(vec3_dot(dir, col->axes[2])); float hx = fminf(a0 > 1e-4f ? col->half_extents.x / a0 : 1e9f, a2 > 1e-4f ? col->half_extents.z / a2 : 1e9f); Vec3 p = vec3_add(c, vec3_add(vec3_scale(dir, fmaxf(hx - in, 0.0f)), vec3_scale(side, aside))); bool taken = false; for (int i = 0; i < count && !taken; i++) { const Object *o = objects[i]; if (o->is_held || o->is_character || o == on || o->collider_count == 0) continue; if (fabsf(o->position.y - top) < 0.3f && vec3_length(vec3(o->position.x - p.x, 0.0f, o->position.z - p.z)) < 0.09f) taken = true; } if (!taken) { *spot = vec3(p.x, top + 0.002f, p.z); return true; } } return false; } static void standItAt(Object *who, int hand, Object *bottle, Vec3 spot) { Hands_touch(who, spot); Hands_drop(who, hand); bottle->position = spot; bottle->rotation = vec3_zero(); /* standing */ bottle->velocity = vec3_zero(); bottle->angular_velocity = vec3_zero(); Object_syncColliders(bottle); } DoneWith Camp_doneWithBottle(Object *who, int hand, Object *const *objects, int count, float roll) { Object *bottle = who->hands.item[hand]; if (!bottle || !Object_isBottle(bottle) || bottle->sips > 0) return DONE_NONE; /* the container right in front */ if (Camp_inFront(who, objects, count, OBJ_BIN)) { Hands_drop(who, hand); Object_useUp(bottle); return DONE_BIN; } /* a table or crate in reach */ for (int i = 0; i < count; i++) { Object *o = objects[i]; bool surface = (o->type == OBJ_FURNITURE && o->variant == FURN_TABLE) || (o->type == OBJ_BOX && !o->is_held && o->size.y < 0.8f); if (!surface || Hands_gapFrom(who->position, who->look.height, o) > BOTTLE_PLACE_REACH) continue; Vec3 spot; if (spotOnTop(who, o, objects, count, &spot)) { standItAt(who, hand, bottle, spot); return DONE_PLACED; } } /* other empties standing about: with them */ int around = 0; Object *near = NULL; for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o == bottle || !Object_isBottle(o) || o->sips > 0 || o->is_held) continue; float d = vec3_length(vec3_sub(o->position, who->position)); if (d < EMPTIES_AROUND) around++; if (d < EMPTIES_REACH && (!near || d < vec3_length(vec3_sub(near->position, who->position)))) near = o; } if (around >= 2 && near) { Vec3 off = vec3_sub(who->position, near->position); off.y = 0.0f; off = vec3_length(off) > 1e-3f ? vec3_scale(vec3_normalize(off), 0.09f) : vec3(0.09f, 0.0f, 0.0f); standItAt(who, hand, bottle, vec3_add(near->position, off)); return DONE_WITH_OTHERS; } /* careless, drunk or wild: away with it; else down at the feet */ float toss = 0.15f + 0.5f * fminf(who->drunk, 1.0f) + 0.2f * who->temperament.energy - 0.2f * who->temperament.nerves - 0.4f * who->skill[ORDER_TRASH]; if (roll < toss) { Vec3 f = Object_facing(who); float turn = (roll / fmaxf(toss, 0.01f) - 0.5f) * 1.4f; Vec3 dir = vec3(f.x * cosf(turn) - f.z * sinf(turn), 0.0f, f.x * sinf(turn) + f.z * cosf(turn)); if (Hands_throw(who, dir)) return DONE_TOSSED; } Hands_drop(who, hand); return DONE_DOWN; } float Camp_firelight(Object *const *objects, int count, Vec3 *where) { float best = 0.0f; for (int i = 0; i < count; i++) { const Object *o = objects[i]; if (o->type != OBJ_FIREPLACE || !o->burning) continue; float strength = fminf(0.25f + 0.6f * Camp_flameSize(o), 1.5f) * o->size.x / 1.6f; if (strength > best) { best = strength; *where = vec3_add(o->position, vec3(0.0f, 0.6f, 0.0f)); } } return best; } #define WASH_RADIUS 1.2f /* water this close washes a puddle away ... */ #define WASH_RATE 0.6f /* ... this much of it per second */ static const char *const SPLASHED[] = { "Hey! Stop that!", "Brrr, that's cold!", "Watch it!", "Are you serious?!" }; static const char *const PEED_ON[] = { "Ewww! Are you peeing on me?!", "Hey! Gross!", "Seriously?!", "Stop that, you animal!" }; /* Liquid coming down at point: burning people and fires near it. */ static void quench(Object *const *objects, int count, const Object *by, Vec3 point, float liters, float dt, bool pee) { for (int i = 0; i < count; i++) { Object *o = objects[i]; float d = flatDist(o->position, point); if (o->is_character && d < (o == by ? 0.8f : 1.0f)) { if (o->on_fire > 0.0f) o->fire_roll += dt * (pee ? PEE_OUT : WET_OUT); /* counts like rolling */ } else if (o->type == OBJ_FIREPLACE && o->burning && d < o->size.x * 0.6f) { o->water += liters; if (o->water >= Camp_waterNeeded(o)) { /* pssshhh */ o->burning = false; o->water = 0.0f; camp_doused = true; } } } } void Camp_water(Object *const *objects, int count, const Object *by, Vec3 point, float liters, float dt) { quench(objects, count, by, point, liters, dt, false); for (int i = 0; i < count; i++) { Object *o = objects[i]; float d = flatDist(o->position, point); if (o->type == OBJ_PUDDLE && o->mesh.vao && o->size.x > 0.0f && d < WASH_RADIUS + o->size.x && o->work > 0.0f && o->variant == 0) { o->work -= WASH_RATE * dt; /* washed away (see the world's drying puddles) */ } else if (o->is_character && o != by && d < 1.0f && o->indoors <= 0.0f && !o->riding) { o->dirt = fmaxf(0.0f, o->dirt - 0.5f * dt); if (o->type == OBJ_HUMAN && o->ai.speech_time <= 0.0f) Ai_say(o, SPLASHED[(unsigned)(o->position.x * 10.0f + (float)i) % 4u]); } else if (o->type == OBJ_DOG && d < 1.0f && o->ai.speech_time <= 0.0f) { Ai_say(o, "Woof! Woof! *snaps at the water*"); o->dog.wag = 3.0f; } } if (by && flatDist(by->position, point) < 0.8f) { /* watering yourself */ Object *self = (Object *)by; self->dirt = fmaxf(0.0f, self->dirt - 0.5f * dt); } } void Camp_pee(Object *const *objects, int count, const Object *by, Vec3 point, float liters, float dt) { quench(objects, count, by, point, liters, dt, true); for (int i = 0; i < count; i++) { Object *o = objects[i]; if (o == by || flatDist(o->position, point) > 0.9f || o->indoors > 0.0f || o->riding) continue; /* (inside, gone home, in a car: not there) */ if (o->is_character) { o->dirt = fminf(1.0f, o->dirt + 0.08f * dt); if (o->type == OBJ_HUMAN && o->ai.speech_time <= 0.0f) Ai_say(o, o->on_fire > 0.0f ? "Thanks... I guess?!" : PEED_ON[(unsigned)(o->position.z * 10.0f + (float)i) % 4u]); } else if (o->type == OBJ_DOG && o->ai.speech_time <= 0.0f) { Ai_say(o, "*sniff sniff* ...woof?"); } } }