Nimbin[12]?Games / Entropie / entropie/src/world/weather.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/weather.c 10.4 KB · 191 lines raw
#include "world/weather.h"
#include "world/wetness.h"
#include "render/shapes.h"

#include <stdlib.h>

#define FIRST_DRY     (3.0f * 3600.0f)  /* game s: dry at first (you arrive, the tents go up) */
#define DRY_MIN       (2.0f * 3600.0f)  /* a dry spell: this long ... */
#define DRY_MORE      (6.0f * 3600.0f)  /* ... up to this much longer */
#define SHOWER_MIN    (12.0f * 60.0f)   /* a shower: this long ... */
#define SHOWER_MORE   (40.0f * 60.0f)
#define RAIN_EASE     150.0f            /* game s: it sets in / eases off over about this */
#define SOAK_TIME     (25.0f * 60.0f)   /* game s of hard rain: everything soaked */
#define DRY_TIME      (3.0f * 3600.0f)  /* ... and dry again (in the day; slower at night) */
#define RAIN_MM_HOUR  9.0f              /* mm an hour in hard rain */
#define POUR_RADIUS   16.0f             /* m round you the ground gets the water (puddles, mud) */
#define POURS         8                 /* ... in this many splashes a frame */
#define PEOPLE_RADIUS 6.0f              /* m round anybody else out in it ... */
#define PEOPLE_POURS  2                 /* ... in this many */
#define DROP_RADIUS   9.0f              /* m round the camera the drops fall */
#define DROP_SPEED    8.5f
#define DROP_LEN      0.6f
#define FIRE_DOUSE    0.004f            /* kg of a fire's wood a game second, in hard rain */
#define STORM_RAIN    0.7f              /* rain this hard: a thunderstorm ... */
#define FLASH_EVERY   75.0f             /* ... a flash about every this many game seconds */
#define FLASH_FADE    4.0f              /* a flash gone in a quarter second (real) */
#define WIND_EASE     240.0f            /* game s: the wind picks up / dies down over about this */
#define WIND_SPELL    (20.0f * 60.0f)   /* game s: it changes after this ... */
#define WIND_SPELL_MORE (60.0f * 60.0f) /* ... up to this much later */
#define GUST_RATE     0.45f             /* gusts come and go (real s, a slow beat of two sines) */

static float dice(unsigned *rng) {
    *rng = *rng * 1103515245u + 12345u;
    return (float)((*rng >> 8) & 0xFFFFu) / 65536.0f;
}

void Weather_init(World *world) {
    world->rain = world->rain_goal = 0.0f;
    world->rain_left = FIRST_DRY;
    world->wet = 0.0f;
    world->weather = WEATHER_AUTO;
    world->wind_kind = WIND_AUTO;
    world->wind = vec3_zero();
    world->wind_goal = world->wind_level = 0.08f;
    world->wind_angle = 0.6f;
    world->wind_left = WIND_SPELL;
    world->gust = world->gust_t = 0.0f;
    world->flash = world->thunder_in = 0.0f;
    world->thunder = false;
    world->drops = calloc(1, sizeof(*world->drops));
    if (world->drops) world->drops->rng = 0x5eed1234u;
}

const char *Weather_windName(WindKind kind) {
    static const char *const NAME[WIND_KINDS] = { "on its own", "calm", "a breeze", "a storm" };
    return NAME[(unsigned)kind < WIND_KINDS ? kind : 0];
}

float Weather_windSpeed(const World *world) {
    return hypotf(world->wind.x, world->wind.z);
}

/* The wind: calm spells and windy ones (windier with rain coming), turning now
   and then; gusts on top of it. */
static void blow(World *world, float dt, float game_dt) {
    unsigned *rng = world->drops ? &world->drops->rng : &world->rng;
    if (world->wind_kind == WIND_CALM) world->wind_goal = 0.03f;
    else if (world->wind_kind == WIND_BREEZE) world->wind_goal = 0.3f;
    else if (world->wind_kind == WIND_STORM) world->wind_goal = 0.85f;
    else if ((world->wind_left -= game_dt) <= 0.0f) {
        world->wind_left = WIND_SPELL + WIND_SPELL_MORE * dice(rng);
        float d = dice(rng);
        world->wind_goal = 0.04f + 0.35f * d * d + 0.45f * world->rain_goal * dice(rng);
        world->wind_angle += (dice(rng) - 0.5f) * 1.6f; /* (turning a bit) */
    }
    if (world->wind_kind == WIND_AUTO && world->rain > STORM_RAIN) world->wind_goal = fmaxf(world->wind_goal, 0.55f); /* (a thunderstorm: windy) */
    world->wind_level += (world->wind_goal - world->wind_level) * fminf(1.0f, game_dt / WIND_EASE);
    world->gust_t += dt * GUST_RATE;
    float beat = 0.5f + 0.5f * sinf(world->gust_t * 2.3f) * sinf(world->gust_t * 0.71f + 1.3f);
    world->gust = clampf(beat * beat * 1.6f - 0.25f, 0.0f, 1.0f) * world->wind_level;
    float speed = WIND_MAX * world->wind_level * (0.7f + 0.3f * sinf(world->gust_t * 3.1f)) + 0.6f * WIND_MAX * world->gust;
    float a = world->wind_angle + 0.15f * sinf(world->gust_t * 1.7f);
    world->wind = vec3(cosf(a) * speed, 0.0f, sinf(a) * speed);
}

const char *Weather_name(WeatherKind kind) {
    static const char *const NAME[WEATHER_KINDS] = { "on its own", "dry", "a shower", "pouring" };
    return NAME[(unsigned)kind < WEATHER_KINDS ? kind : 0];
}

bool Weather_covered(const World *world, Vec3 p) {
    return World_rayCast(world, vec3_add(p, vec3(0.0f, 0.3f, 0.0f)), vec3(0.0f, 1.0f, 0.0f), 14.0f, NULL, NULL, NULL, NULL) >= 0.0f;
}

static void respawn(World *world, int k, bool anywhere) {
    RainDrops *d = world->drops;
    float a = dice(&d->rng) * 2.0f * PI_F, r = DROP_RADIUS * sqrtf(dice(&d->rng));
    Vec3 p = vec3_add(world->eye, vec3(cosf(a) * r, 0.0f, sinf(a) * r));
    float ground = world->has_terrain ? Terrain_height(&world->terrain, p.x, p.z) : 0.0f;
    p.y = anywhere ? ground + 7.0f * dice(&d->rng) : fmaxf(world->eye.y, ground) + 4.0f + 3.0f * dice(&d->rng);
    d->hidden[k] = Weather_covered(world, vec3(p.x, fminf(world->eye.y, ground + 1.0f), p.z));
    d->p[k] = p;
}

void Weather_update(World *world, float dt, float game_dt) {
    /* where it's going: forced (dev), or showers and dry spells taking turns */
    if (world->weather == WEATHER_DRY) world->rain_goal = 0.0f;
    else if (world->weather == WEATHER_SHOWER) world->rain_goal = 0.4f;
    else if (world->weather == WEATHER_RAIN) world->rain_goal = 1.0f;
    else if ((world->rain_left -= game_dt) <= 0.0f) {
        unsigned *rng = world->drops ? &world->drops->rng : &world->rng;
        if (world->rain_goal > 0.0f) {
            world->rain_goal = 0.0f;
            world->rain_left = DRY_MIN + DRY_MORE * dice(rng);
        } else {
            world->rain_goal = 0.25f + 0.75f * dice(rng);
            world->rain_left = SHOWER_MIN + SHOWER_MORE * dice(rng);
        }
    }
    world->rain += (world->rain_goal - world->rain) * fminf(1.0f, game_dt / RAIN_EASE);
    if (world->rain < 0.002f && world->rain_goal <= 0.0f) world->rain = 0.0f;
    bool day = world->hour >= 7.0f && world->hour < 20.0f;
    world->wet = clampf(world->wet + world->rain * game_dt / SOAK_TIME - (1.0f - world->rain) * game_dt / (DRY_TIME * (day ? 1.0f : 3.0f)), 0.0f, 1.0f);
    blow(world, dt, game_dt);
    /* a thunderstorm: lightning, the thunder a few seconds after it */
    world->thunder = false;
    world->flash = fmaxf(0.0f, world->flash - FLASH_FADE * dt);
    if (world->thunder_in > 0.0f && (world->thunder_in -= dt) <= 0.0f) { world->thunder_in = 0.0f; world->thunder = true; }
    if (world->rain > STORM_RAIN && world->drops && world->thunder_in <= 0.0f && dice(&world->drops->rng) < game_dt / FLASH_EVERY) {
        world->flash = 1.0f;
        world->thunder_in = 1.0f + 5.0f * dice(&world->drops->rng); /* (near: right after; further: later) */
    }
    if (world->rain <= 0.0f || !world->drops) return;

    /* the ground near you gets the water: puddles, mud (not under a roof) */
    RainDrops *d = world->drops;
    float liters = RAIN_MM_HOUR * world->rain / 3600.0f * game_dt * PI_F * POUR_RADIUS * POUR_RADIUS / (float)POURS;
    for (int k = 0; k < POURS && world->has_terrain; k++) {
        float a = dice(&d->rng) * 2.0f * PI_F, r = POUR_RADIUS * sqrtf(dice(&d->rng));
        Vec3 p = vec3_add(world->eye, vec3(cosf(a) * r, 0.0f, sinf(a) * r));
        p.y = Terrain_height(&world->terrain, p.x, p.z);
        if (!Weather_covered(world, p)) Wetness_pour(&world->terrain, p.x, p.z, liters, 2.2f, 0.0f);
    }
    /* ... and round everybody else out in it: where they walk (the paths) gets soaked too, and turns to mud */
    float near_liters = RAIN_MM_HOUR * world->rain / 3600.0f * game_dt * PI_F * PEOPLE_RADIUS * PEOPLE_RADIUS / (float)PEOPLE_POURS;
    for (int i = 0; i < world->count && world->has_terrain; i++) {
        const Object *o = world->objects[i];
        if (!o->is_character || o->indoors > 0.0f || o->in_tent || o->riding) continue;
        float ex = o->position.x - world->eye.x, ez = o->position.z - world->eye.z;
        if (ex * ex + ez * ez < (POUR_RADIUS - PEOPLE_RADIUS) * (POUR_RADIUS - PEOPLE_RADIUS)) continue; /* (in the camera's patch already) */
        for (int k = 0; k < PEOPLE_POURS; k++) {
            float a = dice(&d->rng) * 2.0f * PI_F, r = PEOPLE_RADIUS * sqrtf(dice(&d->rng));
            Vec3 p = vec3_add(o->position, vec3(cosf(a) * r, 0.0f, sinf(a) * r));
            p.y = Terrain_height(&world->terrain, p.x, p.z);
            if (!Weather_covered(world, p)) Wetness_pour(&world->terrain, p.x, p.z, near_liters, 2.2f, 0.0f);
        }
    }
    /* a fire burns down faster in it */
    for (int i = 0; i < world->count; i++) {
        Object *f = world->objects[i];
        if (f->type == OBJ_FIREPLACE && f->burning) f->fuel = fmaxf(0.0f, f->fuel - FIRE_DOUSE * world->rain * game_dt);
    }
    /* the drops round the camera */
    int n = (int)(RAIN_DROPS * world->rain);
    for (int k = 0; k < n; k++) {
        Vec3 *p = &d->p[k];
        float dx = p->x - world->eye.x, dz = p->z - world->eye.z;
        if (p->y == 0.0f && p->x == 0.0f) { respawn(world, k, true); continue; }
        p->y -= DROP_SPEED * dt;
        p->x += world->wind.x * dt; /* (blown along) */
        p->z += world->wind.z * dt;
        float ground = world->has_terrain ? Terrain_height(&world->terrain, p->x, p->z) : 0.0f;
        if (p->y < ground || dx * dx + dz * dz > DROP_RADIUS * DROP_RADIUS * 1.3f) respawn(world, k, p->y >= ground);
    }
}

void Weather_draw(World *world, Renderer *renderer) {
    if (world->rain <= 0.01f || !world->drops || world->eye.x > WORLD_BERLIN_FROM) return; /* (Berlin: indoors, another sky) */
    Vec3 shown[RAIN_DROPS];
    int n = 0, live = (int)(RAIN_DROPS * world->rain);
    for (int k = 0; k < live; k++)
        if (!world->drops->hidden[k]) { /* (not right in front of the eye: a fat bar) */
            Vec3 p = world->drops->p[k], e = vec3_sub(p, world->eye);
            if (e.x * e.x + e.z * e.z > 1.5f * 1.5f || fabsf(e.y) > 1.5f) shown[n++] = p;
        }
    if (n == 0) return;
    Vec3 lean = vec3(-world->wind.x / DROP_SPEED, 0.0f, -world->wind.z / DROP_SPEED); /* (slanting into where it comes from) */
    Mesh mesh = Shapes_rain(shown, n, DROP_LEN, lean);
    Renderer_drawSmoke(renderer, &mesh, mat4_identity(), vec3(1.0f, 1.0f, 1.0f));
    Mesh_destroy(&mesh);
}