Nimbin[12]?Games / Entropie / entropie/src/world/hose.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/hose.c 15.1 KB · 349 lines raw
#include "world/hose.h"
#include "world/world.h"
#include "world/camp.h"
#include "world/wetness.h"

#define HOSE_RADIUS   0.018f
#define HOSE_SHORTEST 3.0f   /* cut no shorter than this */
#define MELT_TIME     15.0f  /* seconds right in the flames to burn through */
#define LEAK_MOST     0.9f   /* of the water lost through leaks, at most */
#define HOSE_GRAVITY  9.81f
#define HOSE_DAMPING  0.985f /* of the speed kept each step (air) */
#define HOSE_FRICTION 0.55f  /* of the sliding kept each step, lying on something */
#define HOSE_ITERATIONS 10
#define HOSE_NEAR     256    /* things near a hose checked at once */

/* Where the hose comes out of the tap, and where it ends at the nozzle. */
static Vec3 tapEnd(const Object *nozzle) {
    return vec3_add(nozzle->hose_tap->position, vec3(0.0f, TAP_HEIGHT, 0.0f));
}

static Vec3 nozzleEnd(const Object *nozzle) {
    return vec3_sub(vec3_add(nozzle->position, nozzle->pivot), vec3_scale(nozzle->colliders[0].axes[1], 0.1f));
}

static HoseRope *ropeOf(World *world, const Object *nozzle) {
    HoseRope *free_slot = NULL;
    for (int i = 0; i < HOSE_MAX; i++) {
        if (world->hoses[i].nozzle == nozzle) return &world->hoses[i];
        if (!world->hoses[i].nozzle && !free_slot) free_slot = &world->hoses[i];
    }
    if (!free_slot) return NULL;
    free_slot->nozzle = nozzle;
    free_slot->ready = false;
    free_slot->length = HOSE_LENGTH;
    for (int k = 0; k < HOSE_NODES; k++) {
        free_slot->melt[k] = free_slot->heat[k] = 0.0f;
        free_slot->taped[k] = false;
    }
    return free_slot;
}

static float segmentOf(const HoseRope *rope) { return rope->length / (float)(HOSE_NODES - 1); }
static bool brokenAt(const HoseRope *rope, int k) { return rope->melt[k] >= 1.0f; } /* (between k and k + 1) */

/* Lying in or right by the flames of a burning fire: it gets hot and melts. */
static void heatUp(World *world, HoseRope *rope, float dt) {
    float hot[HOSE_NODES] = { 0.0f };
    for (int i = 0; i < world->count; i++) {
        const Object *f = world->objects[i];
        if (f->type != OBJ_FIREPLACE || !f->burning || f->fuel <= 0.0f) continue;
        float flame = Camp_flameSize(f), reach = (0.25f * f->size.x + 0.1f) * fminf(flame, 1.5f) + 0.15f;
        for (int k = 1; k < HOSE_NODES - 1; k++) {
            Vec3 d = vec3_sub(rope->p[k], f->position);
            float across = sqrtf(d.x * d.x + d.z * d.z);
            if (across > reach + 0.3f) continue;
            float h = fminf(flame, 1.5f) * (1.0f - smoothstepf(0.6f * reach, reach + 0.3f, across)) * (1.0f - smoothstepf(0.4f, 1.0f + 0.3f * flame, d.y));
            hot[k] = fmaxf(hot[k], h);
        }
    }
    for (int k = 1; k < HOSE_NODES - 1; k++) {
        rope->heat[k] += (hot[k] - rope->heat[k]) * fminf(1.0f, 2.0f * dt); /* (warms up, cools down) */
        if (hot[k] > 0.0f) {
            rope->melt[k] = fminf(1.0f, rope->melt[k] + hot[k] * dt / MELT_TIME);
            if (rope->melt[k] >= HOSE_LEAKS) rope->taped[k] = false; /* (the tape melts too) */
        }
    }
}

/* Laid out on the ground from the tap to the nozzle (the first time). */
static void layOut(World *world, HoseRope *rope) {
    Vec3 a = tapEnd(rope->nozzle), b = nozzleEnd(rope->nozzle);
    for (int k = 0; k < HOSE_NODES; k++) {
        float t = (float)k / (float)(HOSE_NODES - 1);
        Vec3 p = vec3_lerp(a, b, t);
        if (k > 0 && k < HOSE_NODES - 1) p.y = Terrain_height(&world->terrain, p.x, p.z) + HOSE_RADIUS;
        rope->p[k] = rope->prev[k] = p;
    }
    rope->ready = true;
}

/* Out of a box collider the shortest way (over the top if it came from above).
   True if it was in (and is now on it). */
static bool pushOut(const Object *o, int c, Vec3 *p, Vec3 prev) {
    const Collider *col = &o->colliders[c];
    Vec3 center = Object_colliderCenter(o, c);
    Vec3 rel = vec3_sub(*p, center);
    const float h[3] = { col->half_extents.x + HOSE_RADIUS, col->half_extents.y + HOSE_RADIUS, col->half_extents.z + HOSE_RADIUS };
    float local[3], best = 1e9f;
    int axis = -1;
    for (int k = 0; k < 3; k++) {
        local[k] = vec3_dot(rel, col->axes[k]);
        float in = h[k] - fabsf(local[k]);
        if (in <= 0.0f) return false;
        if (in < best) { best = in; axis = k; }
    }
    /* came down onto it from above: onto the top, not out the side */
    float was_up = vec3_dot(vec3_sub(prev, center), col->axes[1]);
    if (was_up >= h[1] - 0.01f) axis = 1;
    float sign = local[axis] >= 0.0f ? 1.0f : -1.0f;
    if (axis == 1 && was_up >= h[1] - 0.01f) sign = 1.0f;
    *p = vec3_add(*p, vec3_scale(col->axes[axis], sign * h[axis] - local[axis]));
    return true;
}

static void simulate(World *world, HoseRope *rope, float dt) {
    const Object *nozzle = rope->nozzle;
    /* what lies near it */
    Vec3 lo = rope->p[0], hi = rope->p[0];
    for (int k = 1; k < HOSE_NODES; k++) {
        lo = vec3(fminf(lo.x, rope->p[k].x), fminf(lo.y, rope->p[k].y), fminf(lo.z, rope->p[k].z));
        hi = vec3(fmaxf(hi.x, rope->p[k].x), fmaxf(hi.y, rope->p[k].y), fmaxf(hi.z, rope->p[k].z));
    }
    const Object *near[HOSE_NEAR];
    int n_near = 0;
    for (int i = 0; i < world->count && n_near < HOSE_NEAR; i++) {
        const Object *o = world->objects[i];
        if (o == nozzle || o == nozzle->hose_tap || o->is_character || o->is_held || o->collider_count == 0 ||
            o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->type == OBJ_DOG) continue;
        bool overlaps = false;
        for (int c = 0; c < o->collider_count && !overlaps; c++) {
            Vec3 cc = Object_colliderCenter(o, c);
            float r = vec3_length(o->colliders[c].half_extents) + 0.3f;
            overlaps = cc.x + r > lo.x && cc.x - r < hi.x && cc.y + r > lo.y && cc.y - r < hi.y &&
                       cc.z + r > lo.z && cc.z - r < hi.z;
        }
        if (overlaps) near[n_near++] = o;
    }

    /* fall */
    bool resting[HOSE_NODES] = { false };
    for (int k = 1; k < HOSE_NODES - 1; k++) {
        Vec3 v = vec3_scale(vec3_sub(rope->p[k], rope->prev[k]), HOSE_DAMPING);
        rope->prev[k] = rope->p[k];
        rope->p[k] = vec3_add(rope->p[k], vec3_add(v, vec3(0.0f, -HOSE_GRAVITY * dt * dt, 0.0f)));
    }
    rope->p[0] = tapEnd(nozzle);
    rope->p[HOSE_NODES - 1] = nozzleEnd(nozzle);

    /* keep its length, keep out of the ground and out of things */
    const float segment = segmentOf(rope);
    for (int it = 0; it < HOSE_ITERATIONS; it++) {
        for (int k = 0; k + 1 < HOSE_NODES; k++) {
            if (brokenAt(rope, k)) continue; /* (burned through: two loose ends) */
            Vec3 d = vec3_sub(rope->p[k + 1], rope->p[k]);
            float len = vec3_length(d);
            if (len <= segment || len < 1e-6f) continue; /* (slack: it may bunch up) */
            Vec3 fix = vec3_scale(d, (len - segment) / len);
            bool a_fixed = k == 0, b_fixed = k + 1 == HOSE_NODES - 1;
            if (a_fixed && b_fixed) continue;
            if (a_fixed) rope->p[k + 1] = vec3_sub(rope->p[k + 1], fix);
            else if (b_fixed) rope->p[k] = vec3_add(rope->p[k], fix);
            else {
                rope->p[k] = vec3_add(rope->p[k], vec3_scale(fix, 0.5f));
                rope->p[k + 1] = vec3_sub(rope->p[k + 1], vec3_scale(fix, 0.5f));
            }
        }
        for (int k = 1; k < HOSE_NODES - 1; k++) {
            Vec3 *p = &rope->p[k];
            float ground = Terrain_height(&world->terrain, p->x, p->z) + HOSE_RADIUS;
            if (p->y < ground) { p->y = ground; resting[k] = true; }
            for (int i = 0; i < n_near; i++)
                for (int c = 0; c < near[i]->collider_count; c++)
                    if (pushOut(near[i], c, p, rope->prev[k])) resting[k] = true;
        }
    }
    /* lying on something: it doesn't slide on by itself */
    for (int k = 1; k < HOSE_NODES - 1; k++) {
        if (!resting[k]) continue;
        Vec3 v = vec3_sub(rope->p[k], rope->prev[k]);
        rope->prev[k] = vec3_sub(rope->p[k], vec3(v.x * HOSE_FRICTION, fmaxf(v.y, 0.0f), v.z * HOSE_FRICTION));
    }
}

void Hose_update(World *world, float dt) {
    dt = fminf(dt, 1.0f / 30.0f);
    if (dt <= 0.0f) return;
    for (int i = 0; i < HOSE_MAX; i++) { /* (gone ones let go of their slot) */
        bool found = false;
        for (int k = 0; k < world->count && !found; k++) found = world->objects[k] == world->hoses[i].nozzle;
        if (!found) world->hoses[i].nozzle = NULL;
    }
    for (int i = 0; i < world->count; i++) {
        const Object *o = world->objects[i];
        if (o->type != OBJ_HOSE || !o->hose_tap) continue;
        HoseRope *rope = ropeOf(world, o);
        if (!rope) continue;
        if (!rope->ready) layOut(world, rope);
        simulate(world, rope, dt);
        heatUp(world, rope, dt);
    }
}

/* A matrix that takes the unit piece of hose (0..1 along +Y) from a to b. */
static Mat4 pieceFrom(Vec3 a, Vec3 b) {
    Vec3 y = vec3_sub(b, a);
    float len = vec3_length(y);
    if (len < 1e-4f) return mat4_translate(a);
    y = vec3_scale(y, 1.0f / len);
    Vec3 helper = fabsf(y.y) > 0.9f ? vec3(1, 0, 0) : vec3(0, 1, 0);
    Vec3 x = vec3_normalize(vec3_cross(helper, y)), z = vec3_cross(x, y);
    Mat4 m = mat4_identity();
    m.m[0] = x.x; m.m[1] = x.y; m.m[2] = x.z;
    m.m[4] = y.x * len; m.m[5] = y.y * len; m.m[6] = y.z * len;
    m.m[8] = z.x; m.m[9] = z.y; m.m[10] = z.z;
    m.m[12] = a.x; m.m[13] = a.y; m.m[14] = a.z;
    return m;
}

void Hose_draw(World *world, Renderer *renderer, const Object *nozzle) {
    if (!nozzle->hose_tap || !world->hose_piece.vao) return;
    HoseRope *rope = ropeOf(world, nozzle);
    if (!rope) return;
    if (!rope->ready) layOut(world, rope);
    for (int k = 1; k < HOSE_NODES; k++) {
        if (brokenAt(rope, k - 1) || vec3_length(vec3_sub(rope->p[k], rope->p[k - 1])) < 1e-3f) continue;
        /* melted: darker, black where it's burned; hot: glowing */
        float m = fmaxf(rope->melt[k - 1], rope->melt[k]), heat = fmaxf(rope->heat[k - 1], rope->heat[k]);
        if (m > 0.02f || heat > 0.02f) {
            float dark = 1.0f - 0.85f * smoothstepf(0.0f, 0.9f, m);
            Renderer_drawMeshTinted(renderer, &world->hose_piece, pieceFrom(rope->p[k - 1], rope->p[k]),
                                    vec3(dark, dark * 0.9f, dark * 0.85f), 0.7f * clampf(heat, 0.0f, 1.0f));
        } else {
            Renderer_drawMesh(renderer, &world->hose_piece, pieceFrom(rope->p[k - 1], rope->p[k]));
        }
        if (rope->taped[k] && world->hose_tape.vao) { /* a band of tape round it */
            Vec3 a = vec3_lerp(rope->p[k - 1], rope->p[k], 0.6f), b = k + 1 < HOSE_NODES ? vec3_lerp(rope->p[k], rope->p[k + 1], 0.4f) : rope->p[k];
            Renderer_drawMesh(renderer, &world->hose_tape, pieceFrom(a, b));
        }
    }
}

HoseRope *Hose_of(World *world, const Object *nozzle) {
    if (!nozzle || nozzle->type != OBJ_HOSE || !nozzle->hose_tap) return NULL;
    HoseRope *rope = ropeOf(world, nozzle);
    if (rope && !rope->ready) layOut(world, rope);
    return rope;
}

float Hose_length(World *world, const Object *nozzle) {
    HoseRope *rope = Hose_of(world, nozzle);
    return rope ? rope->length : HOSE_LENGTH;
}

bool Hose_burnedThrough(World *world, const Object *nozzle, Vec3 *at) {
    HoseRope *rope = Hose_of(world, nozzle);
    if (!rope) return false;
    for (int k = 0; k < HOSE_NODES - 1; k++)
        if (brokenAt(rope, k)) {
            if (at) *at = rope->p[k];
            return true;
        }
    return false;
}

float Hose_flow(World *world, const Object *nozzle, float dt) {
    HoseRope *rope = Hose_of(world, nozzle);
    if (!rope) return 1.0f;
    const float liters = 0.3f * dt; /* (what the tap gives) */
    for (int k = 0; k < HOSE_NODES - 1; k++)
        if (brokenAt(rope, k)) { /* all of it runs out there */
            Wetness_pour(&world->terrain, rope->p[k].x, rope->p[k].z, liters, 0.2f, 0.0f);
            return 0.0f;
        }
    float lost = 0.0f;
    for (int k = 1; k < HOSE_NODES - 1; k++) {
        if (rope->melt[k] < HOSE_LEAKS || rope->taped[k]) continue;
        float leak = 0.12f + 0.6f * (rope->melt[k] - HOSE_LEAKS);
        lost += leak;
        Wetness_pour(&world->terrain, rope->p[k].x, rope->p[k].z, liters * leak, 0.12f, 0.0f); /* (it squirts out there) */
    }
    return 1.0f - fminf(lost, LEAK_MOST);
}

HoseRope *Hose_aimed(World *world, Vec3 eye, Vec3 dir, float reach, int *node) {
    HoseRope *best = NULL;
    float best_d = 0.2f;
    for (int i = 0; i < HOSE_MAX; i++) {
        HoseRope *rope = &world->hoses[i];
        if (!rope->nozzle || !rope->ready) continue;
        for (int k = 1; k < HOSE_NODES - 1; k++) {
            Vec3 to = vec3_sub(rope->p[k], eye);
            float along = vec3_dot(to, dir);
            if (along < 0.0f || along > reach) continue;
            float d = vec3_length(vec3_sub(to, vec3_scale(dir, along)));
            if (d < best_d) { best_d = d; best = rope; *node = k; }
        }
    }
    return best;
}

int Hose_damageNear(const HoseRope *rope, int k) {
    int best = -1;
    for (int j = k - 2; j <= k + 2; j++) {
        if (j < 1 || j >= HOSE_NODES - 1) continue;
        bool damaged = rope->melt[j] >= HOSE_LEAKS && !rope->taped[j];
        if (damaged && (best < 0 || rope->melt[j] > rope->melt[best])) best = j;
    }
    return best;
}

/* The run of melted (untaped) bits round k: [*from, *to]. */
static void damagedRun(const HoseRope *rope, int k, int *from, int *to) {
    *from = *to = k;
    while (*from > 1 && rope->melt[*from - 1] >= HOSE_LEAKS && !rope->taped[*from - 1]) (*from)--;
    while (*to < HOSE_NODES - 2 && rope->melt[*to + 1] >= HOSE_LEAKS && !rope->taped[*to + 1]) (*to)++;
}

bool Hose_tape(HoseRope *rope, int k) {
    int at = Hose_damageNear(rope, k), from, to;
    if (at < 0) return false;
    damagedRun(rope, at, &from, &to);
    for (int j = from; j <= to; j++)
        if (brokenAt(rope, j)) return false;
    for (int j = from; j <= to; j++) rope->taped[j] = true;
    return true;
}

float Hose_cutOut(HoseRope *rope, int k) {
    int at = Hose_damageNear(rope, k), from, to;
    if (at < 0) return 0.0f;
    damagedRun(rope, at, &from, &to);
    const float segment = segmentOf(rope);
    float cut = (float)(to - from + 1) * segment;
    if (rope->length - cut < HOSE_SHORTEST) return 0.0f;
    /* what's left, the ends joined (the bits from..to gone), laid out again along it at the new spacing */
    Vec3 keep[HOSE_NODES];
    bool keep_tape[HOSE_NODES];
    float keep_melt[HOSE_NODES];
    int n = 0;
    for (int j = 0; j < HOSE_NODES; j++) {
        if (j >= from && j <= to) continue;
        keep_tape[n] = rope->taped[j];
        keep_melt[n] = rope->melt[j];
        keep[n++] = rope->p[j];
    }
    const float length = rope->length - cut, step = length / (float)(HOSE_NODES - 1);
    for (int j = 0; j < HOSE_NODES; j++) {
        float u = (float)j * step / segment; /* (in kept bits, each a segment long) */
        int a = (int)u;
        if (a >= n - 1) a = n - 2;
        float t = clampf(u - (float)a, 0.0f, 1.0f);
        rope->p[j] = rope->prev[j] = vec3_lerp(keep[a], keep[a + 1], t);
        rope->taped[j] = keep_tape[t < 0.5f ? a : a + 1]; /* (other mended or melted bits stay as they were) */
        rope->melt[j] = keep_melt[t < 0.5f ? a : a + 1];
        rope->heat[j] = 0.0f;
    }
    rope->length = length;
    return cut;
}