Nimbin[12]?Games / Entropie / commits / d457715

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (17 hours ago) · synced 8 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 2.29.0: soft smoke, voices

Christian Immanuel · 2026-10-26 15:22 · d4577151cfcc424cdbed02cc83383865472b4463

- Smoke is soft and see-through, fading at its edges
- Sounds: people talking (a murmur), Alice barking and crunching, sips
  and bites, the toilet
 entropie/Makefile                  |  2 +-
 entropie/README.md                 |  5 +++--
 entropie/STATE.md                  | 13 +++++++++----
 entropie/assets/shaders/scene.frag |  9 ++++++++-
 entropie/commit_message.txt        | 17 ++++-------------
 entropie/src/game/sound.c          | 32 ++++++++++++++++++++++++++++++++
 entropie/src/people/ai.c           | 14 ++++++++++++++
 entropie/src/render/renderer.c     | 16 ++++++++++++++++
 entropie/src/render/renderer.h     |  3 +++
 entropie/src/render/shapes.h       |  2 ++
 entropie/src/render/shapes_props.c | 18 ++++++++++++++++++
 entropie/src/world/chips.c         |  6 +++++-
 entropie/src/world/chips.h         |  3 ++-
 entropie/src/world/world.c         |  5 +++++
 entropie/src/world/world.h         |  5 +++++
 15 files changed, 127 insertions(+), 23 deletions(-)

diff --git a/entropie/Makefile b/entropie/Makefile
index d8c0ee3..061dc47 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.28.0
+VERSION   := 2.29.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index 3caa43e..bf3dbde 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -31,7 +31,8 @@ people living there.
 - **Drinks:** crates of beer, water, soda and mate. Empties go back into
   their crates; bottles in the wrong crate get sorted out
 - **Sound:** steps on every ground, wind, rain, thunder, the fire, axe and
-  chainsaw, glass, birds by day and crickets at night (all made up on the fly)
+  chainsaw, glass, voices, Alice barking, birds by day and crickets at night
+  (all made up on the fly)
 - **Drink:** too much and you sway, stumble, get sick or pass out
 - **Alice:** the dachshund. She fetches, begs and loves being petted
 - **Saves:** autosave, quick save and named saves. Old saves keep working
@@ -123,5 +124,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.28.0 (build e2f9b0f)`. The build id is
+`make version` prints e.g. `entropie 2.29.0 (build 58db6f4)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index a144987..540d40f 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -930,7 +930,9 @@ say no more often and don't come to you for a bit.
   stronger), rain (drumming and dull in your tent), thunder after the
   lightning, the fire crackling, axe strokes, the chainsaw, the car coming in,
   bottles clinking, glass breaking, somebody hitting the ground, a tree
-  coming down, twigs snapping, the hose, birds by day, crickets at night
+  coming down, twigs snapping, the hose, birds by day, crickets at night;
+  people talking (a murmur, deeper or higher by who it is, the words are
+  on screen), Alice barking and crunching, sips and bites, the toilet
 - Further away is quieter and duller, and comes from its side
 - M: sound off / on; the menus pause it
 
@@ -1167,6 +1169,10 @@ Round 2.28:
 - Hoods (`look.hoodie`: jackets 1, 3, 4 of the five; `hood_up`; `figure.frag` hoodDistance, in the jacket's colour): out in the rain the jacket goes on, the hood up. Umbrellas (OBJ_UMBRELLA, two: by the house door, in the garage kitchen; one-handed): open held in the rain (`world.c` underUmbrella: held up over the head, `hands.c`), dry under it (rain_out off); NPCs fetch one when the rain starts (`ai.c` shelter: idle, a hand free, UMBRELLA_FETCH 25 m) and stay out; ai_putAway takes it home once it's dry.
 - Landing: the very sporty (`sporty`: fitness 0.6..0.95, sober, fresh) roll off up to LAND_BREAK + LAND_SPORT (10.8 m/s, about 6 m), the risk mostly gone. Checked: fitness 0.95 from 3, 4.5, 5.5 m: no crash in 24; 0.6: crashes from 3 m on.
 
+Round 2.29:
+- Smoke soft and see-through (`Shapes_puff`: a ball; `Renderer_drawPuff`: blended, no depth, no outline; `scene.frag` `puff`: thickest facing you, fading at the rim, SMOKE_ALPHA 0.5, thinning out at the end). Checked: the fire's smoke against the sky and the house.
+- More sounds: voices (`Ai_say` -> NOISE_VOICE: a murmur of saw syllables, deep / man / woman, by the length of the line; nothing for "*...*"), Alice (Woof -> NOISE_BARK, crunch), a sip or a bite (`world.c`: the moment `drink_sipped` turns on -> NOISE_GULP), the toilet (NOISE_FLUSH). Checked: compiles, the game's sound stays clean (no NaN, no clipping); not listened to.
+
 ## Open tasks
 
 Most important first.
@@ -1190,8 +1196,7 @@ Most important first.
 - Clothing, still to do: more outfits for extra people (only the performance test has any: they all wear trousers)
 - Tents, still to do: NPCs taking theirs down on their own (leaving)
 - Weather, still to do: hats (nobody wears one yet) blown off; umbrellas while working (now only idle people fetch one); an umbrella blown inside out
-- Sound, still to do: voices (talking is text only), Alice barking, the toilet, eating and drinking
-- Smoke: drawn as pale boxes (no softness) - a softer look would be nicer
+- Sound, still to do: doors and gates, the car's doors and boot, rustling bushes walking through
 
 **Saves, still to do**
 - Things being done come back fresh: a person's part of a job half way (its progress on the thing is kept, and their order), carrying something together
@@ -1203,4 +1208,4 @@ Most important first.
 - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene)
 
 ---
-State of `entropie 2.28.0 (build e2f9b0f)`
+State of `entropie 2.29.0 (build 58db6f4)`
diff --git a/entropie/assets/shaders/scene.frag b/entropie/assets/shaders/scene.frag
index 83ff6c9..aa99155 100644
--- a/entropie/assets/shaders/scene.frag
+++ b/entropie/assets/shaders/scene.frag
@@ -29,6 +29,7 @@ uniform vec4  holes[8];    // holes in the ground (basements): middle xz, x axis
 uniform vec2  holeSize[8]; // ... half sizes
 uniform int   holeCount;
 uniform vec4  tint;   // rgb: darkens (charred wood), a: glows like embers
+uniform float puff;   // > 0: smoke - soft and see-through, this thick (edges fading out)
 
 const vec3  PATH_EARTH  = vec3(0.58, 0.44, 0.28);
 const vec3  MUD         = vec3(0.31, 0.23, 0.15);
@@ -149,6 +150,12 @@ void main() {
     }
     color = applyFog(color, length(viewPos - WorldPos), skyHorizon);
 
-    OutColor       = vec4(color, 1.0);
+    float alpha = 1.0;
+    if (puff > 0.0) { // smoke: thickest where it faces you, fading to nothing at the rim
+        float facing = abs(normalize(ViewNormal).z);
+        alpha = puff * smoothstep(0.05, 0.85, facing);
+        color = mix(color, skyHorizon, 0.15);
+    }
+    OutColor       = vec4(color, alpha);
     OutNormalDepth = vec4(normalize(ViewNormal), ViewDepth);
 }
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index ff4e5ee..bf15895 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,14 +1,5 @@
-entropie 2.28.0: sound, sorted crates, hoods and umbrellas
+entropie 2.29.0: soft smoke, voices
 
-- Sound: steps on every ground, wind, rain, thunder, fire, axe, chainsaw,
-  the car, bottles, glass, falls, a tree coming down, birds and crickets -
-  all made up on the fly (M: off / on)
-- Arrival: everybody gets a seat by the fire (people move up on the bench;
-  no room: they stand with the others)
-- Crates know each bottle: put a bottle down next to a crate and it goes in;
-  right click at a crate takes out one that does not belong; the tidy sort
-  the crates
-- Rain and smoke show against the sky; the sky clouds over with the weather
-- Rain soaks the ground wherever people walk: mud on trodden paths
-- Hoods go up in the rain; two umbrellas to fetch and carry
-- The very sporty roll off drops of 5 m and more
+- Smoke is soft and see-through, fading at its edges
+- Sounds: people talking (a murmur), Alice barking and crunching, sips
+  and bites, the toilet
diff --git a/entropie/src/game/sound.c b/entropie/src/game/sound.c
index e583035..36f00b2 100644
--- a/entropie/src/game/sound.c
+++ b/entropie/src/game/sound.c
@@ -94,6 +94,38 @@ static void noiseOut(const Noise *n, const Ear *ear) {
         play(WAVE_NOISE, 0.0f, 0.0f, 260.0f, 25.0f, 0.03f, 0.7f, 2.5f, 1.0f * g, pan, 0.05f);
         play(WAVE_SINE, 55.0f, 35.0f, 0.0f, 0.0f, 0.01f, 0.4f, 1.5f, 0.8f * g, pan, 0.05f);
         break;
+    case NOISE_VOICE: { /* words heard, not made out: a murmur, syllable after syllable, rising and falling */
+        static const float PITCH[3] = { 95.0f, 125.0f, 215.0f };
+        int voice = n->surface & 3, syllables = n->surface >> 2;
+        float f = PITCH[voice > 2 ? 1 : voice] * (0.92f + 0.16f * dice()), at = 0.0f;
+        for (int k = 0; k < syllables; k++) {
+            float lift = 1.0f + 0.12f * sinf(1.7f * (float)k + 6.0f * dice()) - (k == syllables - 1 ? 0.1f : 0.0f); /* (and down at the end) */
+            float len = 0.09f + 0.08f * dice();
+            play(WAVE_SAW, f * lift, f * lift * (0.95f + 0.1f * dice()), (700.0f + 1100.0f * dice()) * dull, 150.0f, 0.02f, 0.05f, len, 0.22f * g, pan, at);
+            at += len + 0.03f + (dice() < 0.15f ? 0.12f : 0.0f); /* (a pause now and then) */
+        }
+        break;
+    }
+    case NOISE_BARK: /* woof */
+        for (int k = 0; k < n->surface && k < 4; k++) {
+            float at = 0.3f * (float)k;
+            play(WAVE_SAW, 420.0f + 40.0f * dice(), 250.0f, 1900.0f * dull, 220.0f, 0.004f, 0.06f, 0.16f, 0.7f * g, pan, at);
+            play(WAVE_NOISE, 0.0f, 0.0f, 2500.0f * dull, 400.0f, 0.003f, 0.04f, 0.1f, 0.35f * g, pan, at);
+        }
+        break;
+    case NOISE_GULP:
+        if (n->surface == 1) /* a bite: crunch */
+            for (int k = 0; k < 3; k++) play(WAVE_CRACKLE, 900.0f, 0.0f, 4500.0f * dull, 500.0f, 0.003f, 0.04f, 0.1f, 0.5f * g, pan, 0.13f * (float)k);
+        else { /* a swallow */
+            play(WAVE_SINE, 320.0f, 150.0f, 0.0f, 0.0f, 0.01f, 0.05f, 0.12f, 0.35f * g, pan, 0.0f);
+            play(WAVE_NOISE, 0.0f, 0.0f, 900.0f * dull, 100.0f, 0.01f, 0.06f, 0.15f, 0.2f * g, pan, 0.02f);
+        }
+        break;
+    case NOISE_FLUSH: /* the cistern: a rush, then gurgling down */
+        play(WAVE_NOISE, 0.0f, 0.0f, 2600.0f * dull, 300.0f, 0.25f, 1.0f, 3.0f, 0.6f * g, pan, 0.0f);
+        play(WAVE_NOISE, 0.0f, 0.0f, 500.0f * dull, 60.0f, 0.4f, 1.4f, 3.5f, 0.5f * g, pan, 0.3f);
+        play(WAVE_CRACKLE, 60.0f, 0.0f, 1200.0f * dull, 100.0f, 0.5f, 1.2f, 3.0f, 0.3f * g, pan, 0.6f);
+        break;
     case NOISE_SNAP: /* a dry branch underfoot */
         play(WAVE_CRACKLE, 1500.0f, 0.0f, 7000.0f * dull, 900.0f, 0.001f, 0.03f, 0.08f, 0.8f * g, pan, 0.0f);
         play(WAVE_NOISE, 0.0f, 0.0f, 2500.0f * dull, 400.0f, 0.001f, 0.02f, 0.06f, 0.5f * g, pan, 0.0f);
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index abd9ad2..623edb0 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -1266,6 +1266,20 @@ void Ai_say(Object *self, const char *text) {
     Ai *ai = &self->ai;
     snprintf(ai->speech, sizeof(ai->speech), "%s", text);
     ai->speech_time = 2.0f + 0.06f * (float)strlen(ai->speech);
+    if (!g_world) return;
+    Vec3 head = vec3_add(self->position, vec3(0.0f, self->look.height * (self->type == OBJ_DOG ? 0.25f : 0.9f), 0.0f));
+    if (self->type == OBJ_DOG) { /* (her woofs, and a crunch) */
+        int woofs = 0;
+        for (const char *s = text; (s = strstr(s, "Woof")) != NULL; s += 4) woofs++;
+        if (woofs) World_noiseOn(g_world, NOISE_BARK, head, 0.9f, woofs);
+        else if (strstr(text, "crunch")) World_noiseOn(g_world, NOISE_GULP, head, 0.5f, 1);
+        return;
+    }
+    if (text[0] == '*') return; /* (a sound written out, not words) */
+    int syllables = (int)strlen(text) / 5;
+    syllables = syllables < 2 ? 2 : syllables > 10 ? 10 : syllables;
+    int voice = self->female ? VOICE_WOMAN : self->look.height > 1.82f || self->weight > 85.0f ? VOICE_DEEP : VOICE_MAN;
+    World_noiseOn(g_world, NOISE_VOICE, head, strchr(text, '!') ? 0.9f : 0.6f, voice | (syllables << 2));
 }
 
 void Ai_order(Object *self, Order order, Vec3 area) {
diff --git a/entropie/src/render/renderer.c b/entropie/src/render/renderer.c
index cdae0af..e8a86e4 100644
--- a/entropie/src/render/renderer.c
+++ b/entropie/src/render/renderer.c
@@ -36,6 +36,7 @@ static bool loadShaders(Renderer *r) {
     s->shadow_strength = glGetUniformLocation(s->program, "shadowStrength");
     s->fire_light      = glGetUniformLocation(s->program, "fireLight");
     s->tint            = glGetUniformLocation(s->program, "tint");
+    s->puff            = glGetUniformLocation(s->program, "puff");
     s->grass           = glGetUniformLocation(s->program, "grass");
     s->rain_wet        = glGetUniformLocation(s->program, "rainWet");
     s->shadow_map      = glGetUniformLocation(s->program, "shadowMap");
@@ -350,6 +351,7 @@ void Renderer_beginScenePass(Renderer *r, const Camera *camera, const Lighting *
     glUniform1f(s->decal_size, 0.0f);
     glUniform4f(s->tint, 1.0f, 1.0f, 1.0f, 0.0f);
     glUniform1f(s->grass, 0.0f);
+    glUniform1f(s->puff, 0.0f);
     glUniform1i(s->wear_map, UNIT_WEAR);
     glUniform1i(s->water_map, UNIT_WATER);
     glUniform1i(s->surface_map, UNIT_SURFACE);
@@ -409,6 +411,20 @@ void Renderer_drawSmoke(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb) {
     glColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
 }
 
+void Renderer_drawPuff(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float alpha) {
+    if (!meshInView(r, mesh, model) || alpha <= 0.01f) return;
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    glColorMaski(1, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* (no outline) */
+    glDepthMask(GL_FALSE);
+    glUniform1f(r->scene.puff, alpha);
+    Renderer_drawMeshTinted(r, mesh, model, rgb, 0.0f);
+    glUniform1f(r->scene.puff, 0.0f);
+    glDepthMask(GL_TRUE);
+    glColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+    glDisable(GL_BLEND);
+}
+
 void Renderer_drawGrass(Renderer *r, const Mesh *mesh, float terrain_size, float grass_depth) {
     if (!meshInView(r, mesh, mat4_identity())) return;
     glUniform1f(r->scene.grass, terrain_size);
diff --git a/entropie/src/render/renderer.h b/entropie/src/render/renderer.h
index ac9b9eb..5f62c53 100644
--- a/entropie/src/render/renderer.h
+++ b/entropie/src/render/renderer.h
@@ -49,6 +49,7 @@ typedef struct {
     GLint  terrain_size, grass_depth, wear_map, water_map, surface_map;  /* terrain only */
     GLint  decal_size;                           /* things lying on the ground: sink with it */
     GLint  tint;                                 /* color multiplier and ember glow */
+    GLint  puff;                                 /* > 0: soft and see-through (smoke), this thick */
     GLint  grass;                                /* grass blades: terrain size while drawing them */
     GLint  rain_wet;                             /* everything wet from rain (0..1) */
     GLint  holes, hole_size, hole_count;         /* holes in the ground (basements): not drawn there */
@@ -138,6 +139,8 @@ void Renderer_beginAirPass(Renderer *r);
 /* Like Renderer_drawMesh, darkened by rgb and glowing like embers by glow (0..1). */
 void Renderer_drawMeshTinted(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float glow);
 /* Like Renderer_drawMeshTinted, but no ink outline round it and nothing hides behind it (smoke). */
+/* Smoke: a soft, see-through ball (edges fading out), this thick (0..1). */
+void Renderer_drawPuff(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float alpha);
 void Renderer_drawSmoke(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb);
 /* Grass blades (see world/meadow.h): trampled with the terrain, swaying. */
 void Renderer_drawGrass(Renderer *r, const Mesh *mesh, float terrain_size, float grass_depth);
diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h
index e355756..f2cb342 100644
--- a/entropie/src/render/shapes.h
+++ b/entropie/src/render/shapes.h
@@ -23,6 +23,8 @@ typedef enum {
 Mesh Shapes_box(Vec3 size, Palette palette);
 /* A white unit block (tinted when drawn): wood chips, sawdust. */
 Mesh Shapes_chip(void);
+/* A ball of smoke (1 across, white: tinted), drawn soft and see-through (Renderer_drawPuff). */
+Mesh Shapes_puff(void);
 
 /* Upright cylinder centered on the origin (single palette color). */
 Mesh Shapes_cylinder(float diameter, float height, int segments, Palette palette);
diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c
index 03bea0a..6ff81e7 100644
--- a/entropie/src/render/shapes_props.c
+++ b/entropie/src/render/shapes_props.c
@@ -661,6 +661,24 @@ Mesh Shapes_chip(void) {
     return Mesh_fromBuilder(&b);
 }
 
+Mesh Shapes_puff(void) {
+    MeshBuilder b;
+    MeshBuilder_init(&b);
+    const int around = 10, up = 7;
+    GLuint ring[8][11];
+    for (int j = 0; j <= up; j++) {
+        float v = PI_F * (float)j / (float)up;
+        for (int i = 0; i <= around; i++) {
+            float u = 2.0f * PI_F * (float)i / (float)around;
+            Vec3 n = vec3(sinf(v) * cosf(u), cosf(v), sinf(v) * sinf(u));
+            ring[j][i] = MeshBuilder_vertex(&b, vec3_scale(n, 0.5f), n, vec3(1.0f, 1.0f, 1.0f));
+        }
+    }
+    for (int j = 0; j < up; j++)
+        for (int i = 0; i < around; i++) MeshBuilder_quad(&b, ring[j][i], ring[j][i + 1], ring[j + 1][i + 1], ring[j + 1][i]);
+    return Mesh_fromBuilder(&b);
+}
+
 Mesh Shapes_bag(float fullness) {
     const Vec3 size = Shapes_bagSize(fullness);
     const float r = size.x * 0.5f, h = size.y;
diff --git a/entropie/src/world/chips.c b/entropie/src/world/chips.c
index 19b87f4..82ad2c6 100644
--- a/entropie/src/world/chips.c
+++ b/entropie/src/world/chips.c
@@ -21,10 +21,12 @@ void Chips_init(Chips *c) {
     memset(c, 0, sizeof(*c));
     c->rng = 0x51f15eedu;
     c->mesh = Shapes_chip();
+    c->puff = Shapes_puff();
 }
 
 void Chips_destroy(Chips *c) {
     Mesh_destroy(&c->mesh);
+    Mesh_destroy(&c->puff);
     memset(c, 0, sizeof(*c));
 }
 
@@ -116,6 +118,8 @@ void Chips_update(Chips *c, const Terrain *terrain, Vec3 wind, float dt) {
     }
 }
 
+#define SMOKE_ALPHA 0.5f /* how thick a puff of smoke is (see-through) */
+
 void Chips_draw(const Chips *c, Renderer *renderer) {
     for (int i = 0; i < CHIP_MAX; i++) {
         const Chip *ch = &c->chips[i];
@@ -124,7 +128,7 @@ void Chips_draw(const Chips *c, Renderer *renderer) {
         float s = ch->size * (ch->smoke && ch->size > 0.05f ? 1.0f : shrink); /* (a fire's smoke: thinning out instead) */
         Mat4 m = mat4_mul(mat4_translate(ch->p), mat4_mul(mat4_rotate_yxz(vec3(ch->down ? 0.0f : ch->angle, ch->angle * 0.7f, 0.0f)),
                                                           mat4_scale(vec3(s, s * (ch->dust || ch->smoke ? 1.0f : 0.35f), s * (ch->dust || ch->smoke ? 1.0f : 0.7f)))));
-        if (ch->smoke) Renderer_drawSmoke(renderer, &c->mesh, m, ch->color);
+        if (ch->smoke) Renderer_drawPuff(renderer, &c->puff, m, ch->color, SMOKE_ALPHA * fminf(1.0f, ch->life / 0.8f)); /* (thinning out at the end) */
         else Renderer_drawMeshTinted(renderer, &c->mesh, m, ch->color, 0.0f);
     }
 }
diff --git a/entropie/src/world/chips.h b/entropie/src/world/chips.h
index 1800e7c..a4c2a46 100644
--- a/entropie/src/world/chips.h
+++ b/entropie/src/world/chips.h
@@ -31,7 +31,8 @@ typedef struct {
     Chip     chips[CHIP_MAX];
     int      next;
     unsigned rng;
-    Mesh     mesh;  /* a small white block (tinted per chip) */
+    Mesh     mesh;  /* a small white block (tinted per chip) ... */
+    Mesh     puff;  /* ... and a ball for smoke */
 } Chips;
 
 void Chips_init(Chips *c);
diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c
index 437e0e6..0e7ef2c 100644
--- a/entropie/src/world/world.c
+++ b/entropie/src/world/world.c
@@ -298,6 +298,7 @@ static void spawnPee(World *world, Object *o) {
         o->rotation.y = atan2f(front.z, front.x) + PI_F / 2.0f;
         Object_syncColliders(o);
         o->toilet->dog.mode = 0;
+        World_noise(world, NOISE_FLUSH, o->toilet->position, 0.8f);
         o->toilet = NULL;
         return;
     }
@@ -949,7 +950,11 @@ static void animateCharacter(World *world, Object *o, float dt) {
     if (o->climbing || o->on_ladder || o->is_held) o->air_fall = 0.0f;
     else if (!o->is_grounded) o->air_fall = fmaxf(o->air_fall, -o->velocity.y);
     else if (o->air_fall > 0.0f) landFromHigh(world, o);
+    bool sipped = o->hands.drink_hand >= 0 && o->hands.drink_sipped; /* (a sip or a bite just now: heard) */
+    const Object *drinking = o->hands.drink_hand >= 0 ? o->hands.item[o->hands.drink_hand] : NULL;
     Hands_update(o, dt);
+    if (drinking && !sipped && o->hands.drink_sipped)
+        World_noiseOn(world, NOISE_GULP, vec3_add(o->position, vec3(0.0f, o->look.height * 0.9f, 0.0f)), 0.5f, Object_isFood(drinking) ? 1 : 0);
     Tools_update(o, dt);
     Ladder_poseHands(o, dt); /* on a ladder: hands on the rungs */
     bool settling = Object_settleOnSeat(o, dt); /* over to the seat first */
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index 5f0cbef..3efb320 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -23,9 +23,14 @@ typedef enum {
     NOISE_THUD,   /* somebody (or something heavy) hitting the ground */
     NOISE_CRASH,  /* a tree coming down */
     NOISE_SNAP,   /* a dry branch breaking underfoot */
+    NOISE_VOICE,  /* somebody saying something (surface: VOICE_* | syllables << 2) */
+    NOISE_BARK,   /* Alice (surface: how many woofs) */
+    NOISE_GULP,   /* a sip (surface 0), a bite (1) */
+    NOISE_FLUSH,  /* the toilet */
     NOISE_COUNT
 } NoiseKind;
 enum { STEP_GRASS, STEP_GRAVEL, STEP_HARD, STEP_EARTH, STEP_MUD, STEP_WOOD, STEP_WATER };
+enum { VOICE_DEEP, VOICE_MAN, VOICE_WOMAN };
 typedef struct { NoiseKind kind; Vec3 at; float loud; int surface; } Noise;
 #define WORLD_NOISES 96