Entropie git · main
Entropie festival game; first steps
C 88.5% Markdown 9.1% GLSL 2.3%git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gzentropie/src/world/camp.c raw
#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?");
}
}
}