#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; }