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/hose.c 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;
}