Nimbin[12]?Games / Entropie / entropie/src/world/camp.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

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.gz
entropie/src/world/camp.c 20.8 KB · 510 lines 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?");
        }
    }
}