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

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (15 hours ago) · synced 6 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

2.49.0: holding on, and people

Christian Immanuel · 2026-11-02 17:09 · bdcfee7b0eb61f21a2ec4c16a73c0967343d40ba

- E / Q with nothing in front: the hand reaches out and waits, then takes
  hold of whatever comes in reach (a loose thing, something fixed, somebody)
- Holding somebody by a body part; how they take it follows friendship and
  love (Object.like / love, saved): welcome, tolerated, unwelcome, harassment
- 1 - 5 while holding: pull closer, push away, walk with me (hand in hand),
  calm them, let go
- Witnesses: kind acts raise the place's awareness (World.aware, saved),
  harassment lowers it; somebody may step in (awareness team more likely)
- Festival: the odd very drunk guest bothers somebody dancing; the review
  counts it and who stepped in
- New: people/touch.c, people/ai_touch.c, game/game_touch.c

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018d6Y6HzDXzEhrHvYRZo3D8
 commit_message.txt                |  27 +++--
 entropie/ARCHITECTURE.md          |   4 +-
 entropie/Makefile                 |   2 +-
 entropie/README.md                |  10 +-
 entropie/STATE.md                 |  36 +++++-
 entropie/commit_message.txt       |  27 +++--
 entropie/src/game/game.c          |   1 +
 entropie/src/game/game.h          |   2 +
 entropie/src/game/game_actions.c  |   8 +-
 entropie/src/game/game_dev.c      |   6 +-
 entropie/src/game/game_festival.c |  13 +-
 entropie/src/game/game_internal.h |   6 +
 entropie/src/game/game_talk.c     |   5 +
 entropie/src/game/game_touch.c    | 245 ++++++++++++++++++++++++++++++++++++++
 entropie/src/game/save.c          |   9 +-
 entropie/src/people/ai.c          |   2 +
 entropie/src/people/ai.h          |   7 ++
 entropie/src/people/ai_fest.c     |  15 +++
 entropie/src/people/ai_internal.h |   2 +
 entropie/src/people/ai_touch.c    | 106 +++++++++++++++++
 entropie/src/people/hands.c       | 114 +++++++++++++++++-
 entropie/src/people/hands.h       |  18 +++
 entropie/src/people/touch.c       | 115 ++++++++++++++++++
 entropie/src/people/touch.h       |  36 ++++++
 entropie/src/world/cast.c         |   2 +
 entropie/src/world/guests.c       |   2 +
 entropie/src/world/object.h       |   4 +
 entropie/src/world/world.h        |   3 +
 28 files changed, 784 insertions(+), 43 deletions(-)

diff --git a/commit_message.txt b/commit_message.txt
index df92fd3..3701b46 100644
--- a/commit_message.txt
+++ b/commit_message.txt
@@ -1,13 +1,16 @@
-entropie 2.48.0: no falling through, landings, the old tree, any tree
+2.49.0: holding on, and people
 
-- Nobody goes down through a floor or a roof any more (the feet caught at
-  a top: back up onto it; never pushed down out of something you stand on),
-  and whoever drops into the void anyway is put back where they last stood
-- Landings: always into the knees first; moving along and not too high
-  (about 3.3 m, the very sporty 4 m) a roll; higher a crash. How hard it
-  was decides: a twisted ankle (a few hours of limping) or a broken bone
-- The big tree is old and half dead: gaps and dead brown layers in the
-  crown, the top snapped off. A storm can bring it down; felled without the
-  rope it goes the way it leans
-- Any tree can be felled - Kurti's apple trees (300 EUR, Kurti furious) and
-  the protected ones (a 1000 EUR fine) cost dearly; the hint warns first
+- E / Q with nothing in front: the hand reaches out and waits, then takes
+  hold of whatever comes in reach (a loose thing, something fixed, somebody)
+- Holding somebody by a body part; how they take it follows friendship and
+  love (Object.like / love, saved): welcome, tolerated, unwelcome, harassment
+- 1 - 5 while holding: pull closer, push away, walk with me (hand in hand),
+  calm them, let go
+- Witnesses: kind acts raise the place's awareness (World.aware, saved),
+  harassment lowers it; somebody may step in (awareness team more likely)
+- Festival: the odd very drunk guest bothers somebody dancing; the review
+  counts it and who stepped in
+- New: people/touch.c, people/ai_touch.c, game/game_touch.c
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
+Claude-Session: https://claude.ai/code/session_018d6Y6HzDXzEhrHvYRZo3D8
diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md
index 923f867..45bae2e 100644
--- a/entropie/ARCHITECTURE.md
+++ b/entropie/ARCHITECTURE.md
@@ -11,8 +11,8 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL
 | `src/math/` | vectors, matrices, frustum, yaw conventions (`linalg.h`) | math |
 | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render |
 | `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c` - also the Berlin rooms off the map, `course.c`), everybody in the story (`cast.c`), the floors' stages (`stage.c`), the side jobs' ground (`field.c`: the Kartoffelacker's grass, the snails, Kerry's art), the why questions' content (`why.c`), the festival's guests (`guests.c`), the big old tree's places (`bigtree.c`), broken bones and casts (`injury.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game |
-| `src/people/` | bodies (animation), hands, AI (the projects: `ai_project.c`, the side jobs and the big tree: `ai_jobs.c`, the festival weekend: `ai_fest.c`), talk, condition, dog, climbing | everything but game |
-| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, the story (`game_story.c`: the crew turning up, the crew overview), the shopping run (`game_shop.c`), the meetings in Berlin and the weekends (`game_meeting.c`), the why questions coming up and your notebook (`game_why.c`), the festival weekend and the storm (`game_festival.c`), the big tree's rope and fall (`game_bigtree.c`), the ambulance and the hospital (`game_injury.c`), saves (`save.c`), what you hear (`sound.c`) | all |
+| `src/people/` | bodies (animation), hands, AI (the projects: `ai_project.c`, the side jobs and the big tree: `ai_jobs.c`, the festival weekend: `ai_fest.c`, held, hand in hand, stepping in: `ai_touch.c`), touching somebody and what it means (`touch.c`), talk, condition, dog, climbing | everything but game |
+| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, the story (`game_story.c`: the crew turning up, the crew overview), the shopping run (`game_shop.c`), the meetings in Berlin and the weekends (`game_meeting.c`), the why questions coming up and your notebook (`game_why.c`), the festival weekend and the storm (`game_festival.c`), the big tree's rope and fall (`game_bigtree.c`), the ambulance and the hospital (`game_injury.c`), reaching out, holding people and who saw it (`game_touch.c`), saves (`save.c`), what you hear (`sound.c`) | all |
 
 - `render` knows nothing of the world: it gets meshes, matrices and plain numbers.
 - `world` and `people` are one layer: an `Object` owns its `Body`, `Ai` and `Hands`.
diff --git a/entropie/Makefile b/entropie/Makefile
index 3067d0d..c1ef305 100644
--- a/entropie/Makefile
+++ b/entropie/Makefile
@@ -1,5 +1,5 @@
 APP       := entropie
-VERSION   := 2.48.0
+VERSION   := 2.49.0
 
 PREFIX    ?= /usr
 BINDIR    ?= $(PREFIX)/bin
diff --git a/entropie/README.md b/entropie/README.md
index e617417..b905e33 100644
--- a/entropie/README.md
+++ b/entropie/README.md
@@ -47,6 +47,11 @@ people living there.
   an arm or a leg. Landing, you bounce into the knees and roll if you're
   moving; too high and something gives. Broken: the ambulance comes,
   they're back days later with a cast, and slower
+- **People and touch:** E / Q reaches out and holds on - a post, a thing, or
+  somebody. Where you hold them and how they feel about you decides how they
+  take it: walk hand in hand, calm them, or get told off. Harassment gets
+  noticed, and the more aware the place, the likelier someone steps in.
+  Intimacy stays off screen
 - **Any tree can be felled** - the protected ones and Kurti's apple trees
   cost dearly
 - **Money and shopping:** a cash box of 1200 EUR. Send Volkram to the
@@ -85,7 +90,8 @@ people living there.
 | 1 - 5         | Answer when talking / pick from the shout menu |
 | Right click   | Put down / let go (a bottle next to a crate: into it) |
 | Right click at a crate, hands empty | Take out a bottle that doesn't belong in it |
-| E / Q         | Take / drop with right / left hand (by a table or crate: set it on top) |
+| E / Q         | Take / drop with right / left hand (by a table or crate: set it on top); nothing in front: reach out and hold on |
+| 1 - 5 holding someone | Pull closer, push away, walk with me, calm them, let go |
 | F (hold)      | Push / pull (walk forward / back); look left / right to turn it |
 | Ctrl (hold)   | Look around; holding on: without turning it (the view swings back after) |
 | E / Q while holding on | Lift together / put down |
@@ -159,5 +165,5 @@ Dependencies point downward: `main → game → world/people → render → core
 
 ## Version
 
-`make version` prints e.g. `entropie 2.48.0 (build 4f0a82a)`. The build id is
+`make version` prints e.g. `entropie 2.49.0 (build 7776609)`. The build id is
 an md5 of all sources and assets.
diff --git a/entropie/STATE.md b/entropie/STATE.md
index c92eb86..76c228c 100644
--- a/entropie/STATE.md
+++ b/entropie/STATE.md
@@ -439,6 +439,31 @@ Painted in the rotoscope style of *Waking Life* (2001):
 - You: the ambulance takes you; six hours later you're back with the cast
 - A guest: that's their festival over
 
+### Holding on, and people
+
+- E / Q with nothing right in front: that hand reaches out where you look
+  and stays out till something comes in reach. A loose thing is taken (or
+  held on to, if heavy); a fixed thing (post, tree, railing) is held - you
+  can't walk off further than your arm. E / Q again: let go
+- Somebody: you hold them where your hand got them - hand, arm, shoulder,
+  back, head, waist, leg, chest, bottom, crotch. How they take it goes with
+  how they feel about you: friendship (like) and love. A shoulder is fine
+  for most; a hand needs friends or a bit of love; the waist real closeness;
+  the intimate places only real love. Unwelcome: they pull away and say so.
+  On the intimate places it's harassment
+- Holding somebody, 1 - 5: pull closer, push away, come with me (by the
+  hand: hand in hand; again: stop here), calm them, let go
+- Who sees it: kind touches raise the place's awareness and how people see
+  you. Harassment sinks both, annoys everyone near, and somebody may step
+  in - likelier the more aware the place is, the braver they are, and with
+  the awareness team on shift
+- The place's awareness drifts with everybody's mood and, at the festival,
+  how good the floors are (F1: shown on the like row)
+- At the festival a very drunk party guest now and then goes for somebody
+  dancing; she leaves the floor, the people round may step in. The review
+  says how often it happened and how often somebody stepped in
+- Love and anything sexual stay off screen
+
 ### The festival (the sixth weekend)
 
 - Weekend 6 is the festival. The Herberge before weekend 4 says two more
@@ -1646,6 +1671,13 @@ Round 2.48 (no falling through, landings, the old tree, any tree):
 - The big tree: `TreeSize.old` (variant 2): every seventh layer gone, some dead brown and thinner, the top three missing, a bare snag with splinters, three dead branches. A storm (wind over STORM_WIND 11 m/s) brings it down the way it leans (STORM_FALLS 0.3 a game hour) while nobody's cutting it. Felled by you (no rope): away from you plus BIG_LEAN 1.4 x the lean. However it came down, the lines and the review follow (its angle off from the fall itself).
 - Any tree: `tools.c` lets you (only you) fell kept trees; `forestry.c` counts a fine (`World.tree_fine`: FINE_APPLE 300 for an apple tree, FINE_PROTECTED 1000); `game_bigtree.c` takes it from the cash box and says so (Kurti -0.3 for an apple tree). The felling hint warns first.
 
+Round 2.49 (holding on, and people):
+- `hands.c`: HAND_READY (out along the view, READY_OUT 0.55 m: waiting) and HAND_HOLD (`hold`, `hold_local`, `hold_joint`, `hold_part`). `Hands_catch` takes the nearest thing within CATCH_NEAR 0.22 m of the palm; on a person the nearest joint (their hands a bit preferred). `game_touch.c`: E / Q reach out (unless something's right in front: taken as ever), a fixed thing holds you within ARM_REACH 0.72 m (HOLD_BREAK 0.45 m past: the grip's gone; hand in hand 1 m more).
+- `people/touch.c`: Touch_part (joint + where the hand is: front / back), Touch_react (welcome, tolerated, unwelcome, harassment; what it does to like / love / annoyed / fun), Touch_calm. `Object.like`, `love`, `harass_t` (saved); couples (Volkram, Jana) for touches between others.
+- `people/ai_touch.c`: held (they stay and look at you), led (hand in hand: straight along beside you, the pace of yours plus catching up), stepIn (Ai.step_in: off to tell them), bother (Ai.harass: a drunk guest after somebody dancing, HARASS_REACH 0.95 m; again after HARASS_AGAIN 2.5 game h).
+- `World.aware` (saved): eases to the place's mood; +0.01 a kind act seen, -0.03 harassment. Witnesses within 16 m; the nearest one who dares steps in. Festival `harassed` / `stepped` (saved) in the review.
+- Checked (harness): a shoulder taken and calmed (Toni), led 7.8 m hand in hand (Jana kept 0.6 m beside), a waist unwelcome (Stella pulls away), Kilian after Doro (she says so, he gets the 2.5 h pause), save / load of like, love, aware.
+
 ## Open tasks
 
 Most important first.
@@ -1663,7 +1695,7 @@ Most important first.
 - Crowds: lots of "Ouch! My shin" where many people mill about (the shin scrapes)
 - The big tree, still to do: the trunk cut up for the stages' frames (it's huge); the phone line mended (now it stays down); Enrico's geo-dome at the Kartoffelacker
 - Injuries, still to do: heavy things (logs, crates) falling on somebody; a limp with a leg cast; crutches; somebody sitting with the hurt till the ambulance comes
-- Next (from the latest notes): E / Q reach for whatever's in front and hold on to it (staying ready to grab till something's in reach); grabbing people by a body part (shoulder, hip, arm, hand) with what it means between the two (calm someone, show them a place, walk hand in hand, pull closer, push away); relationships (friendship, love - kept off screen); harassment and how the people round notice it, with a group awareness that rises and falls with the mood, kind acts and the place itself; drugs and their effects
+- People, still to do: NPCs taking your hand (or each other's) on their own; being held shown on them (a hand on the shoulder: now only your arm shows it); love going somewhere (off screen); drugs and their effects; the awareness team stepping in for guests too
 
 **Bugs**
 - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm). Note (2.35): the audit is chaotic - each part starts from where the one before left you, so one small change anywhere reshuffles the later parts (a fall in the drunk walk or not, the ladder test not getting on the ladder); compare parts run on their own (the harness: LADDERONLY / JUMPSONLY / JOGONLY). Tried in 2.35: easing the running swing's ends (sprint worse), a swing not hurrying when stopping (no better on its own)
@@ -1696,4 +1728,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.48.0 (build 4f0a82a)`
+State of `entropie 2.49.0 (build 7776609)`
diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt
index df92fd3..3701b46 100644
--- a/entropie/commit_message.txt
+++ b/entropie/commit_message.txt
@@ -1,13 +1,16 @@
-entropie 2.48.0: no falling through, landings, the old tree, any tree
+2.49.0: holding on, and people
 
-- Nobody goes down through a floor or a roof any more (the feet caught at
-  a top: back up onto it; never pushed down out of something you stand on),
-  and whoever drops into the void anyway is put back where they last stood
-- Landings: always into the knees first; moving along and not too high
-  (about 3.3 m, the very sporty 4 m) a roll; higher a crash. How hard it
-  was decides: a twisted ankle (a few hours of limping) or a broken bone
-- The big tree is old and half dead: gaps and dead brown layers in the
-  crown, the top snapped off. A storm can bring it down; felled without the
-  rope it goes the way it leans
-- Any tree can be felled - Kurti's apple trees (300 EUR, Kurti furious) and
-  the protected ones (a 1000 EUR fine) cost dearly; the hint warns first
+- E / Q with nothing in front: the hand reaches out and waits, then takes
+  hold of whatever comes in reach (a loose thing, something fixed, somebody)
+- Holding somebody by a body part; how they take it follows friendship and
+  love (Object.like / love, saved): welcome, tolerated, unwelcome, harassment
+- 1 - 5 while holding: pull closer, push away, walk with me (hand in hand),
+  calm them, let go
+- Witnesses: kind acts raise the place's awareness (World.aware, saved),
+  harassment lowers it; somebody may step in (awareness team more likely)
+- Festival: the odd very drunk guest bothers somebody dancing; the review
+  counts it and who stepped in
+- New: people/touch.c, people/ai_touch.c, game/game_touch.c
+
+Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
+Claude-Session: https://claude.ai/code/session_018d6Y6HzDXzEhrHvYRZo3D8
diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c
index 2bbc3fe..3f8f66a 100644
--- a/entropie/src/game/game.c
+++ b/entropie/src/game/game.c
@@ -345,6 +345,7 @@ void Game_update(Game *game, const Input *input, float dt, float aspect) {
     puttingUp(game);
     game_climbLadder(game, input, dt);
     game_climb(game, input, dt);
+    game_touch(game, dt); /* (after your move: holding on to something keeps you near it) */
     game->world.eye = game->camera.position;
     if (game->world.rain > 0.3f && !game->rain_told) {
         game->rain_told = true;
diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h
index 4ba49a2..4d8a8bf 100644
--- a/entropie/src/game/game.h
+++ b/entropie/src/game/game.h
@@ -88,6 +88,8 @@ typedef struct {
     float    review_t;  /* ... till the next */
     int      result;    /* how good it was, 0..100 (-1: not over yet) */
     int      helped;    /* the awareness team saw to people (kept here: the world's counter is set every frame) */
+    int      harassed;  /* somebody bothered (harassment) ... */
+    int      stepped;   /* ... and somebody stepped in */
 } Festival;
 
 typedef struct {
diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c
index 9fede07..993fe18 100644
--- a/entropie/src/game/game_actions.c
+++ b/entropie/src/game/game_actions.c
@@ -1560,9 +1560,11 @@ void game_handleActions(Game *game, const Input *input) {
     } else if ((input->use_left || input->use_right) && takeLadder(game)) {
         /* (taken down) */
     } else {
-        if (input->use_left && !bottleIntoCrate(game, HAND_LEFT))   Hands_pickUpOrDrop(p, HAND_LEFT, objects, count);
-        if (input->use_right && !bottleIntoCrate(game, HAND_RIGHT)) Hands_pickUpOrDrop(p, HAND_RIGHT, objects, count);
-        if (input->use_left || input->use_right) tell(game, reachingFor(p));
+        bool used[2];
+        game_touchKeys(game, input, used); /* (reaching out, letting go) */
+        if (input->use_left && !used[HAND_LEFT] && !bottleIntoCrate(game, HAND_LEFT))    Hands_pickUpOrDrop(p, HAND_LEFT, objects, count);
+        if (input->use_right && !used[HAND_RIGHT] && !bottleIntoCrate(game, HAND_RIGHT)) Hands_pickUpOrDrop(p, HAND_RIGHT, objects, count);
+        if ((input->use_left && !used[HAND_LEFT]) || (input->use_right && !used[HAND_RIGHT])) tell(game, reachingFor(p));
     }
 
     /* Left click (held: again and again), right click puts down */
diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c
index acfe07c..a015fae 100644
--- a/entropie/src/game/game_dev.c
+++ b/entropie/src/game/game_dev.c
@@ -12,7 +12,7 @@
 typedef enum {
     DEV_WHO, DEV_TELEPORT, DEV_TIME, DEV_SPEED, DEV_WEATHER, DEV_WIND, DEV_START, DEV_FESTIVAL, DEV_BIGTREE, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY,
     DEV_BLADDER, DEV_HUNGER, DEV_THIRST, DEV_SLEEPY, DEV_STRENGTH, DEV_ANNOYED,
-    DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING, DEV_COMMIT,
+    DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING, DEV_COMMIT, DEV_LIKE, DEV_LOVE,
     DEV_LIKES,                       /* likes for each job ... */
     DEV_SKILLS = DEV_LIKES + 4,      /* ... skill at each job */
     DEV_ROWS = DEV_SKILLS + 4
@@ -164,6 +164,8 @@ void game_dev(Game *game, const Input *input) {
     case DEV_HEIGHTS:  o->height_fear = step01(o->height_fear, dir, 0.05f); break;
     case DEV_WILLING:  c->willing = step01(c->willing, dir, 0.1f); break;
     case DEV_COMMIT:   o->commitment = step01(o->commitment, dir, 0.1f); break;
+    case DEV_LIKE:     o->like = step01(o->like, dir, 0.1f); break;
+    case DEV_LOVE:     o->love = step01(o->love, dir, 0.1f); break;
     default: break;
     }
 }
@@ -214,6 +216,8 @@ static void devLine(const Game *game, const Object *o, int row, char *out) {
     case DEV_HEIGHTS:  snprintf(out, 96, "Afraid of heights:  %.2f", o->height_fear); break;
     case DEV_WILLING:  snprintf(out, 96, "Willing:  %.0f%%", c->willing * 100.0f); break;
     case DEV_COMMIT:   snprintf(out, 96, "Into the festival (commitment):  %.0f%%", (double)(o->commitment * 100.0f)); break;
+    case DEV_LIKE:     snprintf(out, 96, "Likes you:  %.0f%%   (the place's awareness %.0f%%)", (double)(o->like * 100.0f), (double)(game->world.aware * 100.0f)); break;
+    case DEV_LOVE:     snprintf(out, 96, "Drawn to you (love):  %.0f%%", (double)(o->love * 100.0f)); break;
     default:           out[0] = '\0'; break;
     }
 }
diff --git a/entropie/src/game/game_festival.c b/entropie/src/game/game_festival.c
index 2e9dbd8..b1db91d 100644
--- a/entropie/src/game/game_festival.c
+++ b/entropie/src/game/game_festival.c
@@ -298,7 +298,12 @@ static bool review(Game *game, int step) {
         else snprintf(text, sizeof text, "People lay drunk in the grass and nobody looked. Next time: an awareness team.");
         line(game, w->fest.awareness ? "Stella" : "Julia", text);
         return true;
-    case 4: {
+    case 4:
+        if (fe->harassed == 0) { snprintf(text, sizeof text, "And nobody got bothered or groped all weekend. That's the festival I want."); line(game, "Jana", text); return true; }
+        snprintf(text, sizeof text, "%d times somebody got groped or bothered. %d times somebody stepped in.", fe->harassed, fe->stepped);
+        line(game, "Jana", text);
+        return true;
+    case 5: {
         int n;
         float fun = guestFun(game, &n);
         fe->result = score(game);
@@ -307,7 +312,7 @@ static bool review(Game *game, int step) {
         line(game, "Toni", text);
         return true;
     }
-    case 5: line(game, "Volkram", "Tomorrow we clean up. But tonight - to us!"); return true;
+    case 6: line(game, "Volkram", "Tomorrow we clean up. But tonight - to us!"); return true;
     default: return false;
     }
 }
@@ -338,7 +343,7 @@ void game_festival(Game *game, float dt) {
     if (fe->stage >= FEST_OPEN && fe->stage <= FEST_LEAVING) guestsIn(game, h);
     if (fe->stage == FEST_OVER && fe->review >= 0 && (fe->review_t -= dt) <= 0.0f) {
         if (review(game, fe->review)) { fe->review_t = REVIEW_GAP; fe->review++; }
-        else if (fe->review < 6) fe->review++;
+        else if (fe->review < 7) fe->review++;
         else {
             fe->review = -1;
             snprintf(game->story_line, sizeof game->story_line, "%s is over - %d%%. The clean-up and the drive home are still to come",
@@ -421,6 +426,6 @@ int game_festivalNotes(const Game *game, char lines[][128], int max) {
         if (w->fest.good[k] <= 0.0f) snprintf(lines[n++], 128, "-  %c%s doesn't play: its stage hasn't got its deck", FLOOR_NAME[k][0] - 32, FLOOR_NAME[k] + 1);
     if (fe->counted && n < max) snprintf(lines[n++], 128, "-  The storm: %d inside, %d out in it", fe->inside, fe->caught);
     if (w->fest.awareness && n < max) snprintf(lines[n++], 128, "-  The awareness team helped %d people", fe->helped);
-    if (fe->stage == FEST_OVER && (fe->review < 0 || fe->review > 4) && n < max) snprintf(lines[n++], 128, "-  How it went: %d%%", fe->result);
+    if (fe->stage == FEST_OVER && (fe->review < 0 || fe->review > 5) && n < max) snprintf(lines[n++], 128, "-  How it went: %d%%", fe->result);
     return n;
 }
diff --git a/entropie/src/game/game_internal.h b/entropie/src/game/game_internal.h
index d58c919..d489ab9 100644
--- a/entropie/src/game/game_internal.h
+++ b/entropie/src/game/game_internal.h
@@ -59,6 +59,12 @@ void game_bigTreeJump(Game *game);
 /* game_injury.c: somebody hurt - the ambulance, the hospital, back with a cast, the cast off; the ambulance here (no saving). */
 void game_injury(Game *game, float dt);
 bool game_ambulanceHere(const Game *game);
+/* game_touch.c: your hands and other people - E / Q (used: handled there), holding somebody (the answers 1 - 5),
+   every frame (catching, holding on, who saw what, the place's awareness). */
+void game_touchKeys(Game *game, const Input *input, bool used[2]);
+int  game_touchChoices(const Game *game, const char *out[TALK_CHOICES]);
+void game_touchAnswer(Game *game, int choice);
+void game_touch(Game *game, float dt);
 #define FESTIVAL_WEEKEND 6
 void game_festival(Game *game, float dt);
 bool game_festivalWeekend(const Game *game);
diff --git a/entropie/src/game/game_talk.c b/entropie/src/game/game_talk.c
index 5ac87aa..11330fa 100644
--- a/entropie/src/game/game_talk.c
+++ b/entropie/src/game/game_talk.c
@@ -157,6 +157,7 @@ static void shout(Game *game, int choice) {
 int Game_choices(const Game *game, const char *out[TALK_CHOICES]) {
     int meeting = Game_meetingChoices(game, out); /* (a question at a meeting: that first) */
     if (meeting) return meeting;
+    if (!game->shouting && !Talk_active(&game->talk)) { int touch = game_touchChoices(game, out); if (touch) return touch; } /* (holding somebody) */
     if (!game->shouting) return Talk_choices(&game->talk, out);
     Game *g = (Game *)game; /* (the lines live in the game) */
     int n = 0;
@@ -198,6 +199,10 @@ void game_talk(Game *game, const Input *input, float dt) {
         }
         return;
     }
+    if (!Talk_active(t) && input->choice >= 1) { /* holding somebody: what you do */
+        const char *lines[TALK_CHOICES];
+        if (game_touchChoices(game, lines) >= input->choice) { game_touchAnswer(game, input->choice); return; }
+    }
     if (!Talk_active(t)) {
         for (int i = 0; i < game->world.count; i++) {
             Object *o = game->world.objects[i];
diff --git a/entropie/src/game/game_touch.c b/entropie/src/game/game_touch.c
new file mode 100644
index 0000000..9d68307
--- /dev/null
+++ b/entropie/src/game/game_touch.c
@@ -0,0 +1,245 @@
+/*
+ * Your hands and other people (people/touch.h, hands.h):
+ *
+ * - E / Q: that hand reaches out where you look and takes hold of whatever
+ *   comes in reach - something to carry, something heavy (push, pull, lift
+ *   together), something fixed (a post, a tree, a railing: you can't walk
+ *   off further than your arm), or somebody, at the part of them you got.
+ *   Again: let go (or put down what it carries).
+ * - Holding somebody: answers 1 - 5 - pull them closer, push them away, take
+ *   them along (by the hand: hand in hand), calm them, let go. How they take
+ *   it goes with how they feel about you (Object.like, love).
+ * - Who sees it: kind acts lift how the place looks out for each other
+ *   (World.aware) and how they see you; harassment sinks it - and the more
+ *   aware the place, the likelier somebody steps in (the awareness team: all
+ *   the more). The place's awareness drifts with the mood of everybody and,
+ *   at the festival, how good the floors are.
+ */
+
+#include <math.h>
+
+#include "game/game_internal.h"
+#include "people/touch.h"
+#include "world/why.h"
+
+#define SEE_NEAR     16.0f  /* m: who notices a touch */
+#define ARM_REACH    0.72f  /* m (scaled) from the shoulder: holding on to something fixed, no further */
+#define HOLD_BREAK   0.45f  /* m past that: you lose your grip (somebody pulled away, you were pushed) */
+#define AWARE_EASE   0.004f /* a real second: the place's awareness drifting to how it is */
+#define AWARE_KIND   0.01f  /* a kind act seen */
+#define AWARE_HARASS 0.03f  /* harassment seen */
+
+static Object *heldPerson(const Game *game, int *hand) {
+    const Object *p = game->player;
+    for (int i = 0; i < 2; i++)
+        if (p->hands.state[i] == HAND_HOLD && p->hands.hold[i] && p->hands.hold[i]->is_character) { if (hand) *hand = i; return p->hands.hold[i]; }
+    return NULL;
+}
+
+static Vec3 shoulderOf(const Object *o, int hand) {
+    return Figure_toWorld(o->position, Object_facing(o), o->skeleton.joints[hand == HAND_LEFT ? JOINT_SHOULDER_L : JOINT_SHOULDER_R]);
+}
+
+static void letGo(Game *game, int hand) {
+    Object *p = game->player, *o = p->hands.hold[hand];
+    if (o && o->is_character && o->led_by == p) o->led_by = NULL;
+    Hands_letGo(p, hand);
+}
+
+/* E / Q: used[hand] - handled here (else the old way: picked up in front, or put down: hands.c). */
+void game_touchKeys(Game *game, const Input *input, bool used[2]) {
+    Object *p = game->player;
+    bool keys[2] = { input->use_left, input->use_right };
+    for (int hand = 0; hand < 2; hand++) {
+        used[hand] = false;
+        if (!keys[hand]) continue;
+        int state = p->hands.state[hand];
+        if (state == HAND_READY || state == HAND_HOLD) { letGo(game, hand); say(game, "You let go"); used[hand] = true; continue; }
+        if (state != HAND_FREE || p->hands.grabbed) continue;
+        if (Hands_inFront(p, game->world.objects, game->world.count)) continue; /* (something right there to take: taken, as ever) */
+        Hands_reachOut(p, hand);
+        say(game, hand == HAND_LEFT ? "Left hand out - it takes hold of whatever comes in reach (Q again: never mind)"
+                                    : "Right hand out - it takes hold of whatever comes in reach (E again: never mind)");
+        used[hand] = true;
+    }
+}
+
+/* ---------- Who saw it ---------- */
+
+static void noticed(Game *game, Object *by, Object *whom, int what, int part) {
+    World *w = &game->world;
+    bool you = by->type == OBJ_PLAYER;
+    bool kind = what == TOUCHED_WELCOME && (part < 0 || part == TOUCH_HAND || part == TOUCH_SHOULDER || part == TOUCH_BACK || part == TOUCH_ARM);
+    if (kind) w->aware = fminf(1.0f, w->aware + AWARE_KIND);
+    if (what == TOUCHED_HARASSED) w->aware = fmaxf(0.0f, w->aware - AWARE_HARASS);
+    Object *stepper = NULL;
+    float best = 1e9f;
+    for (int i = 0; i < w->count; i++) {
+        Object *o = w->objects[i];
+        if (o == by || o == whom || o->type != OBJ_HUMAN || o->body.asleep || o->in_tent || o->indoors > 0.0f || Game_inBerlin(o) != Game_inBerlin(whom)) continue;
+        float d = hypotf(o->position.x - whom->position.x, o->position.z - whom->position.z);
+        if (d > SEE_NEAR) continue;
+        if (you && kind) o->like = fminf(1.0f, o->like + 0.01f);           /* (seen being kind) */
+        if (you && what == TOUCHED_UNWELCOME) o->like = fmaxf(0.0f, o->like - 0.02f);
+        if (what != TOUCHED_HARASSED) continue;
+        if (you) o->like = fmaxf(0.0f, o->like - 0.12f);
+        o->ai.annoyed = fminf(1.0f, o->ai.annoyed + 0.3f);
+        /* stepping in: the place's awareness, their own nerve, the awareness team on it */
+        bool team = w->fest.awareness && Why_knows(o, WHY_AWARENESS);
+        float chance = w->aware * (0.7f - 0.3f * o->temperament.nerves + 0.2f * o->temperament.pride) + (team ? 0.4f : 0.0f) - 0.1f * fminf(o->drunk, 1.0f);
+        w->rng = w->rng * 1664525u + 1013904223u;
+        if ((float)(w->rng >> 8) / 16777216.0f < chance && d < best && !o->ai.step_in) { best = d; stepper = o; }
+    }
+    if (what != TOUCHED_HARASSED) return;
+    if (game->festival.stage) game->festival.harassed++;
+    if (stepper) {
+        stepper->ai.step_in = by;
+        stepper->ai.step_for = whom;
+        stepper->ai.step_t = 0.0f;
+        if (game->festival.stage) game->festival.stepped++;
+        if (you) {
+            static char line[128];
+            snprintf(line, sizeof line, "%s saw that - and is coming over", stepper->name);
+            say(game, line);
+        }
+    } else if (you) say(game, "Nobody said anything. But they saw.");
+}
+
+/* ---------- Holding somebody: what you do ---------- */
+
+int game_touchChoices(const Game *game, const char *out[TALK_CHOICES]) {
+    int hand;
+    const Object *o = heldPerson(game, &hand);
+    if (!o) return 0;
+    static char take[48];
+    int part = game->player->hands.hold_part[hand];
+    snprintf(take, sizeof take, part == TOUCH_HAND ? "Walk with me (hand in hand)" : "Come with me");
+    out[0] = "Pull them closer";
+    out[1] = "Push them away";
+    out[2] = o->led_by == game->player ? "Stop here" : take;
+    out[3] = "Easy now... (calm them)";
+    out[4] = "Let go";
+    return 5;
+}
+
+static void answer(Game *game, int choice) {
+    int hand;
+    Object *o = heldPerson(game, &hand), *p = game->player;
+    if (!o) return;
+    World *w = &game->world;
+    static char line[128];
+    int part = p->hands.hold_part[hand];
+    switch (choice) {
+    case 1: { /* closer: that's closeness */
+        int got = Touch_react(w, o, p, part == TOUCH_HAND || part == TOUCH_ARM || part == TOUCH_SHOULDER ? TOUCH_WAIST : part);
+        if (got <= TOUCHED_TOLERATED) {
+            Vec3 to = vec3_sub(p->position, o->position);
+            to.y = 0.0f;
+            float d = vec3_length(to);
+            if (d > 0.55f) { o->ai.dodge_to = vec3_add(o->position, vec3_scale(to, (d - 0.45f) / d)); o->ai.dodge_t = 1.2f; }
+            o->love = fminf(1.0f, o->love + 0.02f);
+        } else letGo(game, hand);
+        break;
+    }
+    case 2: { /* away: a push */
+        Vec3 away = vec3_sub(o->position, p->position);
+        away.y = 0.0f;
+        Body_stagger(&o->body, vec3_scale(vec3_length(away) > 1e-3f ? vec3_normalize(away) : Object_facing(p), 1.6f));
+        o->like = fmaxf(0.0f, o->like - 0.1f);
+        o->ai.annoyed = fminf(1.0f, o->ai.annoyed + 0.5f);
+        if (o->type == OBJ_HUMAN) Ai_say(o, "Hey! What's that for?");
+        letGo(game, hand);
+        noticed(game, p, o, TOUCHED_UNWELCOME, part);
+        break;
+    }
+    case 3:
+        if (o->led_by == p) { o->led_by = NULL; if (o->type == OBJ_HUMAN) Ai_say(o, "Here? Okay."); break; }
+        if (Touch_react(w, o, p, part == TOUCH_HAND ? TOUCH_HAND : TOUCH_ARM) == TOUCHED_WELCOME) {
+            o->led_by = p;
+            o->ai.mode = AI_IDLE;
+            o->ai.on_project = false;
+            snprintf(line, sizeof line, part == TOUCH_HAND ? "%s walks along with you, hand in hand" : "%s comes along with you", o->name);
+            say(game, line);
+        } else {
+            if (o->type == OBJ_HUMAN) Ai_say(o, "I'd rather not, thanks.");
+            letGo(game, hand);
+        }
+        break;
+    case 4:
+        if (!Touch_calm(w, o, p, part)) letGo(game, hand);
+        break;
+    default:
+        letGo(game, hand);
+        break;
+    }
+}
+
+void game_touchAnswer(Game *game, int choice) { answer(game, choice); }
+
+/* ---------- Every frame ---------- */
+
+static float placeMood(const Game *game) {
+    const World *w = &game->world;
+    float sum = 0.0f;
+    int n = 0;
+    for (int i = 0; i < w->count; i++) {
+        const Object *o = w->objects[i];
+        if (o->type != OBJ_HUMAN || o->gone_home || Game_inBerlin(o)) continue;
+        sum += (o->guest ? o->fun : o->commitment) - 0.3f * o->ai.annoyed;
+        n++;
+    }
+    float mood = n ? sum / (float)n : 0.5f, place = 0.5f;
+    if (w->fest.stage >= FEST_OPEN) { /* (the festival: how good the floors are) */
+        place = 0.0f;
+        for (int k = 0; k < FEST_FLOORS; k++) place += w->fest.good[k] / (float)FEST_FLOORS;
+    }
+    return clampf(0.2f + 0.45f * mood + 0.15f * place + (w->fest.awareness ? 0.15f : 0.0f), 0.0f, 1.0f);
+}
+
+void game_touch(Game *game, float dt) {
+    World *w = &game->world;
+    Object *p = game->player;
+    if (w->aware <= 0.0f) w->aware = 0.5f;
+    w->aware += (placeMood(game) - w->aware) * fminf(1.0f, AWARE_EASE * dt);
+    for (int hand = 0; hand < 2; hand++) {
+        int state = p->hands.state[hand];
+        if (state == HAND_READY) { /* in reach now? */
+            bool person = false;
+            Object *got = Hands_catch(p, hand, w->objects, w->count, &person);
+            if (!got) continue;
+            if (person) {
+                int part = Touch_part(got, p->hands.hold_joint[hand], p->body.hand_target[hand]);
+                p->hands.hold_part[hand] = part;
+                int how = Touch_react(w, got, p, part);
+                static char line[128];
+                snprintf(line, sizeof line, "You take hold of %s's %s  -  1 - 5: what you do", got->name, Touch_partName(part));
+                say(game, line);
+                if (how >= TOUCHED_UNWELCOME) { /* (they pull away) */
+                    letGo(game, hand);
+                    Vec3 away = vec3_sub(got->position, p->position);
+                    away.y = 0.0f;
+                    if (vec3_length(away) > 1e-3f) { got->ai.dodge_to = vec3_add(got->position, vec3_scale(vec3_normalize(away), 1.2f)); got->ai.dodge_t = 1.5f; }
+                }
+            } else if (p->hands.state[hand] == HAND_HOLD) say(game, "You hold on to it (E / Q: let go)");
+            else tell(game, got);
+        } else if (state == HAND_HOLD) { /* held: no further than the arm; too far, the grip's gone */
+            Object *o = p->hands.hold[hand];
+            float s = p->look.height / 1.75f;
+            Vec3 at = Hands_holdPoint(p, hand), sh = shoulderOf(p, hand);
+            float d = vec3_length(vec3_sub(at, sh)), reach = ARM_REACH * s;
+            if (o && o->led_by == p) reach += 1.0f; /* (hand in hand: the arm swings out and back, they catch up) */
+            if (d > reach + HOLD_BREAK) { letGo(game, hand); say(game, o && o->is_character ? "They're out of your reach - you let go" : "You lose your grip"); continue; }
+            if (o && !o->is_character && d > reach) { /* (walking off: held back) */
+                Vec3 out = vec3_sub(sh, at);
+                out.y = 0.0f;
+                if (vec3_length(out) > 1e-3f) {
+                    out = vec3_normalize(out);
+                    float away = vec3_dot(p->movement, out);
+                    if (away > 0.0f) p->movement = vec3_sub(p->movement, vec3_scale(out, away));
+                }
+            }
+        }
+    }
+    for (int k = 0; k < w->touched_n; k++) noticed(game, w->touched[k].by, w->touched[k].whom, w->touched[k].what, w->touched[k].part);
+    w->touched_n = 0;
+}
diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c
index a9d1cf0..8fa604c 100644
--- a/entropie/src/game/save.c
+++ b/entropie/src/game/save.c
@@ -79,7 +79,7 @@ static const Field PERSON[] = {
     RE("ai_woodpile", ai.woodpile), BY("ai_banned", ai.banned), BY("ai_defies", ai.defies), BO("ai_unload", ai.unload),
     BO("ai_greet", ai.greet), FL("willing", ai.character.willing), IN("snails", snails),
     IN("injured", injured), IN("injury_side", injury_side), FL("hospital_until", hospital_until), IN("cast_until", cast_until),
-    IN("cast_limb", look.cast), FL("sprain", sprain), VE("last_ground", last_ground), IN("guest", guest), FL("fun", fun), IN("camp_spot", camp_spot), BO("gone_home", gone_home), BO("storm_told", storm_told),
+    IN("cast_limb", look.cast), FL("sprain", sprain), FL("like", like), FL("love", love), FL("harass_t", harass_t), VE("last_ground", last_ground), IN("guest", guest), FL("fun", fun), IN("camp_spot", camp_spot), BO("gone_home", gone_home), BO("storm_told", storm_told),
     VE("inside_door", inside_door),
 };
 
@@ -196,8 +196,9 @@ bool Save_write(const Game *game, const char *dir, const char *name, char *err,
     fprintf(f, "bigtree %d %.9g %.9g %.9g %d %.9g %d %d\n", bt->step, (double)bt->work, (double)bt->rope_h, (double)bt->rope_wait, bt->pullers,
             (double)bt->off, bt->lines_down, bt->told);
     const Festival *fe = &game->festival;
-    fprintf(f, "festival %d %d %d %d %d %u %d %.9g %d %d\n", fe->stage, fe->came, fe->caught, fe->inside, fe->counted, fe->played,
-            fe->review, (double)fe->review_t, fe->result, fe->helped);
+    fprintf(f, "festival %d %d %d %d %d %u %d %.9g %d %d %d %d\n", fe->stage, fe->came, fe->caught, fe->inside, fe->counted, fe->played,
+            fe->review, (double)fe->review_t, fe->result, fe->helped, fe->harassed, fe->stepped);
+    fprintf(f, "aware %.9g\n", (double)w->aware);
     fprintf(f, "told %d %d\n", game->rain_told, game->wet_told);
     fprintf(f, "story %.9g\n", (double)game->story_hours);
     fprintf(f, "cash %.9g\n", (double)w->cash);
@@ -467,6 +468,7 @@ static void applyGame(Game *game, const Parsed *p) {
             a->welcome = numI(&s); a->welcome_t = numF(&s); a->chat = numI(&s); a->chat_wait = numF(&s); a->greeted = numI(&s) != 0;
             a->car = byId(p, numI(&s));
         } else if (!strcmp(key, "told")) { game->rain_told = numI(&s) != 0; game->wet_told = numI(&s) != 0; }
+        else if (!strcmp(key, "aware")) w->aware = numF(&s);
         else if (!strcmp(key, "bigtree")) {
             BigTree *bt = &w->bigtree;
             bt->step = numI(&s); bt->work = numF(&s); bt->rope_h = numF(&s); bt->rope_wait = numF(&s); bt->pullers = numI(&s);
@@ -477,6 +479,7 @@ static void applyGame(Game *game, const Parsed *p) {
             Festival *fe = &game->festival;
             fe->stage = numI(&s); fe->came = numI(&s); fe->caught = numI(&s); fe->inside = numI(&s); fe->counted = numI(&s) != 0;
             fe->played = (unsigned)numI(&s); fe->review = numI(&s); fe->review_t = numF(&s); fe->result = numI(&s); fe->helped = numI(&s);
+            fe->harassed = numI(&s); fe->stepped = numI(&s);
             w->fest.helped = fe->helped;
         }
         else if (!strcmp(key, "story")) game->story_hours = numF(&s);
diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c
index 4cfe250..6374ba3 100644
--- a/entropie/src/people/ai.c
+++ b/entropie/src/people/ai.c
@@ -2586,6 +2586,8 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) {
     /* The wind took something out of their hand: after it */
     if ((ai->mode == AI_IDLE || ai->mode == AI_WANDER || ai->mode == AI_TABLE || ai->mode == AI_WORK || ai->mode == AI_FETCH) &&
         fetchBlown(self, objects, count, dt)) return;
+    /* People and people: hand in hand, stepping in, bothering somebody */
+    if (ai->mode != AI_FOLLOW && ai_touch(self, objects, count, dt)) return;
     /* The big tree's rope: out to their place on it, pulling when it goes */
     if (ai->mode != AI_FOLLOW && ai_onRope(self, objects, count, dt)) return;
     /* The festival: the storm, guests camping and leaving, dancing, the awareness team */
diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h
index 5d57384..c25c738 100644
--- a/entropie/src/people/ai.h
+++ b/entropie/src/people/ai.h
@@ -278,6 +278,13 @@ typedef struct {
     float          fest_time;   /* ... real s at it so far (negative: till they feel like dancing again) */
     struct Object *fest_who;    /* the storm: off to warn this one; the awareness team: off to see to this one */
     float          fest_walk;   /* ... on the way this long */
+
+    /* People and people (ai_touch.c) */
+    struct Object *step_in;     /* off to tell this one to stop (they bothered step_for) ... */
+    struct Object *step_for;
+    float          step_t;      /* ... on the way this long */
+    struct Object *harass;      /* a drunk guest after this one (the festival) ... */
+    float          harass_walk; /* ... on the way this long */
 } Ai;
 
 /* Starts helping the leader; drops a bottle if needed. */
diff --git a/entropie/src/people/ai_fest.c b/entropie/src/people/ai_fest.c
index 6488211..1246686 100644
--- a/entropie/src/people/ai_fest.c
+++ b/entropie/src/people/ai_fest.c
@@ -37,6 +37,7 @@
 #define TELL_LOOK_TEAM 140.0f /* ... the awareness team goes round everywhere */
 #define HELP_LOOK      45.0f  /* m: the awareness team sees somebody lying drunk this near */
 #define GATE_REACH     1.5f
+#define HARASS_EVERY   900.0f /* real s: a drunk party guest dancing gets too close to somebody about this often */
 
 static bool crewMember(const Object *o) { return o->type == OBJ_HUMAN && o->cast_id > 0; }
 
@@ -267,6 +268,20 @@ static bool dance(Object *self, Object *const *objects, int count, float dt) {
     stand(self);
     turnTowards(self, vec3_scale(f->front[k], -1.0f), dt);
     self->body.dance_goal = 1.0f;
+    if (guest && !self->female && self->drunk > 0.6f && self->harass_t <= 0.0f && !self->ai.harass) { /* (a drunk one, now and then: too close to somebody) */
+        float party = (float)((self->guest * 2654435761u) >> 24 & 255u) / 255.0f;
+        if (party > 0.75f && random01(ai) < dt / HARASS_EVERY) {
+            Object *best = NULL;
+            float best_d = 6.0f;
+            for (int i = 0; i < count; i++) {
+                Object *o = objects[i];
+                if (o == self || o->type != OBJ_HUMAN || !o->female || o->guest == Guest_mate(self->guest) || Body_isDown(&o->body)) continue;
+                float d = flatDistance(o->position, self->position);
+                if (d < best_d) { best_d = d; best = o; }
+            }
+            if (best) { ai->harass = best; ai->harass_walk = 0.0f; stopDancing(self); return false; }
+        }
+    }
     self->body.dance_beat = f->clock * f->bpm[k] / 60.0f + 0.03f * (float)(self->guest % 3);
     self->body.dance_style = guest ? self->guest % 4 : self->cast_id % 4;
     if (guest) self->fun = fminf(1.0f, self->fun + FUN_DANCING * f->good[k] * dt);
diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h
index 4fa7a2b..9bb170e 100644
--- a/entropie/src/people/ai_internal.h
+++ b/entropie/src/people/ai_internal.h
@@ -115,6 +115,8 @@ int  ai_job(Object *self, int project, Object *const *objects, int count, float
 bool ai_jobDone(int project);
 /* On the big tree's rope (Object.puller): out to the place, hands on, pulling when it goes. True while on it. */
 bool ai_onRope(Object *self, Object *const *objects, int count, float dt);
+/* ai_touch.c: hand in hand with whoever leads them, stepping in when somebody's bothered, a drunk guest bothering somebody. */
+bool ai_touch(Object *self, Object *const *objects, int count, float dt);
 const char *ai_jobStart(int project);
 
 static inline float random01(Ai *ai) {
diff --git a/entropie/src/people/ai_touch.c b/entropie/src/people/ai_touch.c
new file mode 100644
index 0000000..3533217
--- /dev/null
+++ b/entropie/src/people/ai_touch.c
@@ -0,0 +1,106 @@
+/*
+ * People and people (people/touch.h): walking along hand in hand with whoever
+ * leads them (they hold your hand: Object.led_by); stepping in when somebody
+ * bothers somebody (game_touch.c sends them: Ai.step_in); and, at the
+ * festival, the odd drunk guest who gets too close to somebody dancing.
+ */
+
+#include <math.h>
+
+#include "people/ai_internal.h"
+#include "people/touch.h"
+
+#define LED_SIDE     0.5f   /* m beside the one leading them */
+#define LED_LOST     3.0f   /* m apart: the hand's gone */
+#define STEP_REACH   1.3f   /* m: near enough to say it */
+#define STEP_GIVE_UP 45.0f  /* real s on the way: they've gone */
+#define HARASS_REACH 0.95f  /* m: (people keep a step apart walking up: an arm reaches) */
+#define HARASS_AGAIN (2.5f * 3600.0f) /* game s before a told-off one tries it again */
+
+/* Hand in hand: beside them, the hand on theirs, the way they go. */
+static bool led(Object *self, float dt) {
+    Object *by = self->led_by;
+    if (!by) return false;
+    int hand = Hands_holding(by, self);
+    if (hand < 0 || Body_isBusy(&self->body) || flatDistance(by->position, self->position) > LED_LOST) { self->led_by = NULL; return false; }
+    Vec3 f = Object_facing(by), right = vec3(-f.z, 0.0f, f.x);
+    float side = hand == HAND_LEFT ? -1.0f : 1.0f; /* (their left hand holds mine: I'm on their left) */
+    Vec3 spot = vec3_add(by->position, vec3_scale(right, side * LED_SIDE));
+    self->sitting = false;
+    Vec3 gap = vec3_sub(spot, self->position);
+    gap.y = 0.0f;
+    float d = vec3_length(gap), pace = hypotf(by->velocity.x, by->velocity.z);
+    if (d > 0.12f || pace > 0.3f) { /* (straight along with them: no way round to find, they're right there) */
+        Vec3 dir = d > 0.12f ? vec3_scale(gap, 1.0f / d) : f;
+        if (d > 0.12f && pace > 0.3f) dir = vec3_normalize(vec3_add(vec3_scale(f, pace), vec3_scale(dir, 2.0f * d))); /* (on, and back beside them) */
+        ai_walk(self, dir, fminf(runSpeed(self), pace + 2.0f * d), dt);
+    } else { stand(self); turnTowards(self, f, dt); }
+    int joint = hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R;
+    Hands_touch(self, Figure_toWorld(by->position, f, by->skeleton.joints[joint]));
+    return true;
+}
+
+/* Off to tell somebody to stop. */
+static bool stepIn(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    Object *who = ai->step_in;
+    if (!who) return false;
+    if ((ai->step_t += dt) > STEP_GIVE_UP || who->indoors > 0.0f || who->gone_home) { ai->step_in = ai->step_for = NULL; return false; }
+    self->sitting = false;
+    if (!ai_goToPoint(self, who->position, STEP_REACH, objects, count, dt)) return true;
+    stand(self);
+    turnTowards(self, vec3_sub(who->position, self->position), dt);
+    static const char *const SAY[] = { "Hey! Leave them alone - that's not okay.", "Stop that. Now. Nobody wants that here.",
+                                       "Back off - you heard them. Not here, not anywhere." };
+    Ai_say(self, SAY[(int)(random01(ai) * 3.0f) % 3]);
+    if (who->type == OBJ_HUMAN) { /* (one of us: they back off) */
+        who->ai.harass = NULL;
+        who->harass_t = HARASS_AGAIN;
+        if (who->ai.speech_time <= 0.0f) Ai_say(who, random01(ai) < 0.5f ? "Alright, alright... sorry." : "Whoa - okay. I'm going.");
+        who->ai.mode = AI_WANDER;
+    }
+    if (ai->step_for && ai->step_for->type == OBJ_HUMAN && ai->step_for->ai.speech_time <= 0.0f) Ai_say(ai->step_for, "Thank you.");
+    if (ai->step_for && ai->step_for->guest) ai->step_for->fun = fminf(1.0f, ai->step_for->fun + 0.12f); /* (somebody had her back) */
+    ai->step_in = ai->step_for = NULL;
+    return true;
+}
+
+/* The festival: a drunk guest after somebody dancing. */
+static bool bother(Object *self, Object *const *objects, int count, float dt) {
+    Ai *ai = &self->ai;
+    self->harass_t = fmaxf(0.0f, self->harass_t - (g_world ? g_world->game_dt : dt));
+    Object *who = ai->harass;
+    if (!who) return false;
+    if ((ai->harass_walk += dt) > 60.0f || who->indoors > 0.0f || who->gone_home || Body_isDown(&who->body)) { ai->harass = NULL; return false; }
+    self->sitting = false;
+    self->body.dance_goal = 0.0f;
+    if (!ai_goToPoint(self, who->position, HARASS_REACH, objects, count, dt)) return true;
+    Vec3 f = Object_facing(who);
+    Vec3 at = vec3_add(who->position, vec3_add(vec3_scale(f, -0.12f), vec3(0.0f, 0.85f * who->look.height / 1.75f, 0.0f))); /* (from behind: the bottom) */
+    Hands_touch(self, at);
+    Touch_react(g_world, who, self, TOUCH_BUTT);
+    who->ai.fest_floor = 0; /* (she's off out of it) */
+    who->ai.fest_time = -120.0f;
+    self->harass_t = HARASS_AGAIN;
+    ai->harass = NULL;
+    return true;
+}
+
+/* You've got hold of them (and they let you): they stay, and look at you. */
+static bool held(Object *self, Object *const *objects, int count, float dt) {
+    if (self->led_by) return false;
+    Object *you = findPlayer(objects, count);
+    if (!you || Hands_holding(you, self) < 0) return false;
+    if (self->ai.dodge_t > 0.0f) return false; /* (pulled closer: let them come) */
+    self->sitting = false;
+    stand(self);
+    turnTowards(self, vec3_sub(you->position, self->position), dt);
+    return true;
+}
+
+bool ai_touch(Object *self, Object *const *objects, int count, float dt) {
+    if (stepIn(self, objects, count, dt)) return true;
+    if (bother(self, objects, count, dt)) return true;
+    if (held(self, objects, count, dt)) return true;
+    return led(self, dt);
+}
diff --git a/entropie/src/people/hands.c b/entropie/src/people/hands.c
index a2bcf1b..8cc8dfa 100644
--- a/entropie/src/people/hands.c
+++ b/entropie/src/people/hands.c
@@ -29,6 +29,8 @@
 #define THROW_TIME           0.35f  /* arm swings forward */
 #define THROW_SPEED         11.0f   /* m/s for something light */
 #define FULL_WIND            0.8f   /* seconds winding up for the hardest throw */
+#define READY_OUT            0.55f  /* m (scaled) out from the shoulder: a hand reaching out (E / Q) ... */
+#define CATCH_NEAR           0.22f  /* ... takes hold of what's this near to it */
 #define OVERHAND_AFTER       0.3f   /* wound up this long (about), the arm goes back over the shoulder */
 #define OVERHAND_WEIGHT      3.0f   /* kg: heavier things go underhand */
 #define SETTLE_TIME          0.45f  /* a taken item turns into the carry pose */
@@ -161,7 +163,7 @@ static bool twoHanded(const Hands *h) {
 void Hands_drop(Object *who, int hand) {
     Hands *h = &who->hands;
     Object *item = h->item[hand];
-    if (!item) return;
+    if (!item) { if (h->state[hand] == HAND_READY || h->state[hand] == HAND_HOLD) Hands_letGo(who, hand); return; }
     if (h->drink_hand == hand) h->drink_hand = -1;
 
     for (int i = 0; i < 2; i++) {
@@ -1042,6 +1044,27 @@ void Hands_update(Object *who, float dt) {
             break;
         }
 
+        case HAND_READY: { /* out in front, where you look, open */
+            Vec3 aim = who->work_state.aim;
+            if (vec3_length(aim) < 0.1f) aim = facing;
+            aim = vec3_normalize(vec3(aim.x, clampf(aim.y, -0.8f, 0.8f), aim.z));
+            Vec3 shoulder = Figure_toWorld(who->position, facing, vec3(sign * 0.19f * s, 1.42f * s, 0.0f));
+            b->hand_active[i] = true;
+            b->hand_target[i] = vec3_add(shoulder, vec3_scale(aim, READY_OUT * s));
+            b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, b->hand_target[i].y) * 0.6f);
+            break;
+        }
+
+        case HAND_HOLD: {
+            Object *o = h->hold[i];
+            if (!o || o->broken || o->indoors > 0.0f || o->riding || o->gone_home) { Hands_letGo(who, i); break; }
+            Vec3 target = Hands_holdPoint(who, i);
+            b->hand_active[i] = true;
+            b->hand_target[i] = target;
+            b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, target.y) * 0.6f);
+            break;
+        }
+
         case HAND_GRABBING: {
             Vec3 target = fromObject(h->grabbed, h->grab_point[i]);
             b->hand_active[i] = true;
@@ -1262,3 +1285,92 @@ void Hands_placeHeld(Object *who) {
         if (h->drink_hand != i && !(item->type == OBJ_UMBRELLA && item->umbrella_open) && item->type != OBJ_SCYTHE) keepOffBody(who, item);
     }
 }
+
+/* ---------- Reaching out, holding on (E / Q) ---------- */
+
+void Hands_reachOut(Object *who, int hand) {
+    Hands *h = &who->hands;
+    if (h->state[hand] != HAND_FREE || h->grabbed) return;
+    h->state[hand] = HAND_READY;
+    h->hold[hand] = NULL;
+}
+
+void Hands_letGo(Object *who, int hand) {
+    Hands *h = &who->hands;
+    if (h->state[hand] != HAND_READY && h->state[hand] != HAND_HOLD) return;
+    h->state[hand] = HAND_FREE;
+    h->hold[hand] = NULL;
+}
+
+int Hands_holding(const Object *who, const Object *whom) {
+    for (int i = 0; i < 2; i++) if (who->hands.state[i] == HAND_HOLD && who->hands.hold[i] == whom) return i;
+    return -1;
+}
+
+Vec3 Hands_holdPoint(const Object *who, int hand) {
+    const Hands *h = &who->hands;
+    const Object *o = h->hold[hand];
+    if (!o) return who->position;
+    if (o->is_character) return Figure_toWorld(o->position, Object_facing(o), o->skeleton.joints[h->hold_joint[hand]]);
+    return fromObject(o, h->hold_local[hand]);
+}
+
+/* Where on a person a point is: their nearest joint (the hand, the wrists into it). */
+static int nearestJoint(const Object *o, Vec3 p, float *dist) {
+    Vec3 facing = Object_facing(o);
+    int best = -1;
+    float best_d = 1e9f;
+    for (int j = 0; j < JOINT_COUNT; j++) {
+        if (j == JOINT_TOE_L || j == JOINT_TOE_R) continue;
+        float d = vec3_length(vec3_sub(Figure_toWorld(o->position, facing, o->skeleton.joints[j]), p));
+        if (j == JOINT_HAND_L || j == JOINT_HAND_R) d -= 0.07f; /* (a hand reaching for theirs takes it) */
+        if (d < best_d) { best_d = d; best = j; }
+    }
+    *dist = fmaxf(best_d, 0.0f);
+    return best;
+}
+
+Object *Hands_catch(Object *who, int hand, Object *const *objects, int count, bool *person) {
+    Hands *h = &who->hands;
+    *person = false;
+    if (h->state[hand] != HAND_READY) return NULL;
+    const Body *b = &who->body;
+    Vec3 palm = b->hand_target[hand];
+    Object *best = NULL;
+    float best_d = CATCH_NEAR;
+    int best_joint = -1;
+    for (int i = 0; i < count; i++) {
+        Object *o = objects[i];
+        if (o == who || o->is_held || o->collider_count == 0 || o->broken || o->type == OBJ_DECAL || o->type == OBJ_STRUCTURE) continue;
+        if (fabsf(o->position.x - palm.x) > 6.0f || fabsf(o->position.z - palm.z) > 6.0f) continue;
+        if (o->is_character) { /* somebody: at a joint of theirs (the body round it) */
+            if (o->type == OBJ_DOG || o->riding || o->indoors > 0.0f) continue;
+            float d;
+            int j = nearestJoint(o, palm, &d);
+            if (j >= 0 && d - 0.08f < best_d) { best_d = d - 0.08f; best = o; best_joint = j; }
+            continue;
+        }
+        float d = vec3_length(vec3_sub(closestPoint(o, palm), palm));
+        if (d < best_d) { best_d = d; best = o; best_joint = -1; }
+    }
+    if (!best) return NULL;
+    if (best->is_character) {
+        h->state[hand] = HAND_HOLD;
+        h->hold[hand] = best;
+        h->hold_joint[hand] = best_joint;
+        *person = true;
+        return best;
+    }
+    if (!best->is_static) { /* loose: carried if it can be, else held on to (push, pull, lift together) */
+        h->state[hand] = HAND_FREE;
+        if (liftableOneHand(best) || liftableTwoHands(best)) { if (startPickUp(who, hand, best)) return best; }
+        else if (Hands_grabObject(who, best)) return best;
+        h->state[hand] = HAND_READY;
+        return NULL;
+    }
+    h->state[hand] = HAND_HOLD; /* fixed: the hand holds on where it touched */
+    h->hold[hand] = best;
+    h->hold_local[hand] = toObject(best, closestPoint(best, palm));
+    h->hold_joint[hand] = -1;
+    return best;
+}
diff --git a/entropie/src/people/hands.h b/entropie/src/people/hands.h
index 9fe7e71..a8742bb 100644
--- a/entropie/src/people/hands.h
+++ b/entropie/src/people/hands.h
@@ -31,6 +31,8 @@ typedef enum {
     HAND_REACHING,  /* bending down / reaching for an item */
     HAND_HOLDING,   /* carrying an item */
     HAND_GRABBING,  /* holding on to an object to push or pull it */
+    HAND_READY,     /* reaching out in front (E / Q): takes hold of whatever comes in reach */
+    HAND_HOLD,      /* holding on to something fixed (a post, a wall, a tree) or somebody (a body part): hold, hold_local */
 } HandState;
 
 typedef struct {
@@ -65,8 +67,24 @@ typedef struct {
     int            drink_hand;     /* hand bringing a bottle to the mouth, -1 = none */
     float          drink_time;
     bool           drink_sipped;
+    struct Object *hold[2];        /* HAND_HOLD: what (or who) this hand holds on to ... */
+    Vec3           hold_local[2];  /* ... where on it (its own space; a person: unused) ... */
+    int            hold_joint[2];  /* ... a person: the joint (Joint) held at, its part (people/touch.h) */
+    int            hold_part[2];
 } Hands;
 
+/* E / Q: this hand reaches out in front and takes hold of whatever comes in reach - a thing to
+   carry, something heavy to push, something fixed to hold on to, somebody. Again: lets go. */
+void  Hands_reachOut(struct Object *who, int hand);
+/* Lets go with this hand (whatever it holds on to, reaching out). */
+void  Hands_letGo(struct Object *who, int hand);
+/* A hand reaching out: what's in reach of it now taken hold of. Returns what (or NULL); *person: it's somebody. */
+struct Object *Hands_catch(struct Object *who, int hand, struct Object *const *objects, int count, bool *person);
+/* Holding on to this one (a hand on them), or -1. */
+int   Hands_holding(const struct Object *who, const struct Object *whom);
+/* Where a hand is held (world): the fixed point, or the person's joint now. */
+Vec3  Hands_holdPoint(const struct Object *who, int hand);
+
 /* Picks up the item in front with this hand (or both), or drops what it holds. */
 bool  Hands_pickUpOrDrop(struct Object *who, int hand, struct Object *const *objects, int count);
 void  Hands_drop(struct Object *who, int hand);
diff --git a/entropie/src/people/touch.c b/entropie/src/people/touch.c
new file mode 100644
index 0000000..2df7e13
--- /dev/null
+++ b/entropie/src/people/touch.c
@@ -0,0 +1,115 @@
+#include "people/touch.h"
+#include "render/figure.h"
+
+#include <math.h>
+#include <string.h>
+
+static const char *const NAMES[TOUCH_PARTS] = { "hand", "arm", "shoulder", "back", "head", "waist", "leg", "chest", "bottom", "crotch" };
+
+const char *Touch_partName(int part) { return NAMES[part < 0 || part >= TOUCH_PARTS ? 0 : part]; }
+
+bool Touch_intimate(int part) { return part == TOUCH_CHEST || part == TOUCH_BUTT || part == TOUCH_GROIN || part == TOUCH_LEG; }
+
+int Touch_part(const Object *whom, int joint, Vec3 hand) {
+    Vec3 local = Figure_toLocal(whom->position, Object_facing(whom), hand); /* (z: in front of them, x: their right) */
+    switch (joint) {
+    case JOINT_HAND_L: case JOINT_HAND_R: case JOINT_WRIST_L: case JOINT_WRIST_R: return TOUCH_HAND;
+    case JOINT_ELBOW_L: case JOINT_ELBOW_R: return TOUCH_ARM;
+    case JOINT_SHOULDER_L: case JOINT_SHOULDER_R: return TOUCH_SHOULDER;
+    case JOINT_NECK: case JOINT_HEAD: return TOUCH_HEAD;
+    case JOINT_CHEST: return local.z > 0.04f ? TOUCH_CHEST : local.z < -0.04f ? TOUCH_BACK : TOUCH_WAIST;
+    case JOINT_PELVIS: case JOINT_HIP_L: case JOINT_HIP_R:
+        if (local.z > 0.06f && fabsf(local.x) < 0.12f) return TOUCH_GROIN;
+        if (local.z < -0.04f) return TOUCH_BUTT;
+        return TOUCH_WAIST;
+    default: return TOUCH_LEG; /* (a knee, an ankle) */
+    }
+}
+
+/* Two who belong together (they share a tent). */
+static bool couple(const Object *a, const Object *b) {
+    return (!strcmp(a->name, "Volkram") && !strcmp(b->name, "Jana")) || (!strcmp(a->name, "Jana") && !strcmp(b->name, "Volkram"));
+}
+
+void Touch_feelings(const Object *whom, const Object *by, float *like, float *love) {
+    if (by->type == OBJ_PLAYER) { *like = whom->like; *love = whom->love; return; }
+    if (couple(whom, by)) { *like = 0.9f; *love = 0.9f; return; }
+    bool known = whom->cast_id > 0 && by->cast_id > 0; /* (the crew know each other) */
+    *like = known ? 0.55f : 0.25f;
+    *love = 0.0f;
+}
+
+/* How much of each it takes for a touch there to be welcome (and its being just too much). */
+static void needs(int part, float *like, float *love) {
+    *like = 2.0f; *love = 2.0f; /* (out of reach) */
+    switch (part) {
+    case TOUCH_ARM: case TOUCH_SHOULDER: case TOUCH_BACK: *like = 0.25f; break;
+    case TOUCH_HAND:  *like = 0.5f; *love = 0.35f; break;
+    case TOUCH_HEAD:  *like = 0.6f; *love = 0.45f; break;
+    case TOUCH_WAIST: *like = 0.85f; *love = 0.5f; break;
+    case TOUCH_LEG:   *love = 0.6f; break;
+    default:          *love = 0.8f; break; /* (the chest, the bottom, between the legs) */
+    }
+}
+
+static void note(World *world, Object *by, Object *whom, int what, int part) {
+    if (world->touched_n >= 8) return;
+    world->touched[world->touched_n].by = by;
+    world->touched[world->touched_n].whom = whom;
+    world->touched[world->touched_n].what = what;
+    world->touched[world->touched_n].part = part;
+    world->touched_n++;
+}
+
+static void sayIt(Object *whom, const char *line) { if (whom->type == OBJ_HUMAN) Ai_say(whom, line); }
+
+int Touch_react(World *world, Object *whom, Object *by, int part) {
+    float like, love, need_like, need_love;
+    Touch_feelings(whom, by, &like, &love);
+    needs(part, &need_like, &need_love);
+    float mood = -0.15f * whom->ai.annoyed - 0.1f * fmaxf(whom->temperament.nerves, 0.0f) + 0.1f * fminf(whom->drunk, 1.0f) * (love > 0.2f ? 1.0f : 0.0f);
+    float margin = fmaxf(like + mood - need_like, love + mood - need_love); /* (either will do) */
+    bool you = by->type == OBJ_PLAYER;
+    int what;
+    if (margin >= 0.0f) {
+        what = TOUCHED_WELCOME;
+        if (you) { /* closeness that's welcome brings you closer */
+            whom->like = fminf(1.0f, whom->like + 0.01f);
+            if (part == TOUCH_HAND || part == TOUCH_WAIST || Touch_intimate(part)) whom->love = fminf(1.0f, whom->love + 0.025f);
+        }
+        static const char *const OK[TOUCH_PARTS] = { "Hey, you.", "Mm?", "What's up?", "Hm?", "Hey...", "Come here, you.", "Oh - hi.", "Mm...", "Cheeky.", "Not here - later." };
+        if (whom->ai.speech_time <= 0.0f) sayIt(whom, OK[part]);
+    } else if (margin > -0.15f && !Touch_intimate(part) && part != TOUCH_WAIST) {
+        what = TOUCHED_TOLERATED;
+        if (you) whom->like = fmaxf(0.0f, whom->like - 0.01f);
+        sayIt(whom, part == TOUCH_HAND ? "Uh... okay?" : "Can I help you?");
+    } else if (!Touch_intimate(part)) {
+        what = TOUCHED_UNWELCOME;
+        if (you) whom->like = fmaxf(0.0f, whom->like - 0.06f);
+        whom->ai.annoyed = fminf(1.0f, whom->ai.annoyed + 0.4f);
+        sayIt(whom, "Don't touch me, please.");
+    } else {
+        what = TOUCHED_HARASSED;
+        if (you) { whom->like = fmaxf(0.0f, whom->like - 0.3f); whom->love = fmaxf(0.0f, whom->love - 0.4f); }
+        whom->ai.annoyed = 1.0f;
+        whom->commitment = fmaxf(0.0f, whom->commitment - 0.1f);
+        if (whom->guest) whom->fun = fmaxf(0.0f, whom->fun - 0.2f);
+        static const char *const NO[] = { "Hey! Get your hands off me!", "What the hell?! Don't touch me!", "Back off! Seriously!" };
+        sayIt(whom, NO[(int)(fabsf(whom->position.x * 7.0f + whom->position.z)) % 3]);
+    }
+    note(world, by, whom, what, part);
+    return what;
+}
+
+bool Touch_calm(World *world, Object *whom, Object *by, int part) {
+    float like, love;
+    Touch_feelings(whom, by, &like, &love);
+    if (Touch_intimate(part) || like < 0.3f) return Touch_react(world, whom, by, part) == TOUCHED_WELCOME;
+    whom->ai.annoyed = fmaxf(0.0f, whom->ai.annoyed - 0.5f);
+    whom->temperament.nerves = fmaxf(-0.5f, whom->temperament.nerves - 0.05f);
+    if (by->type == OBJ_PLAYER) whom->like = fminf(1.0f, whom->like + 0.03f);
+    if (whom->guest) whom->fun = fminf(1.0f, whom->fun + 0.03f);
+    sayIt(whom, like > 0.6f ? "Thanks. I needed that." : "...Thanks. Better.");
+    note(world, by, whom, TOUCHED_WELCOME, -1); /* (a kind act: seen) */
+    return true;
+}
diff --git a/entropie/src/people/touch.h b/entropie/src/people/touch.h
new file mode 100644
index 0000000..eeb90dc
--- /dev/null
+++ b/entropie/src/people/touch.h
@@ -0,0 +1,36 @@
+#ifndef PEOPLE_TOUCH_H
+#define PEOPLE_TOUCH_H
+
+/*
+ * Taking hold of somebody (a hand reaching out: E / Q, see hands.h) - where,
+ * and what it means between the two. A hand, an arm, a shoulder, the back:
+ * friendly, if they like you well enough; the head: closer; the waist, a
+ * leg: closeness; the chest, the bottom, between the legs: only if there's
+ * real love between you. Unwelcome they pull away and say so - on the
+ * intimate places it's harassment, and the people round may step in (how
+ * likely: the whole place's awareness, World.aware: game_touch.c).
+ *
+ * How somebody feels about you: Object.like (friendship) and Object.love
+ * (drawn to you); between two others it's strangers, but for couples.
+ */
+
+#include "world/world.h"
+
+enum { TOUCH_HAND, TOUCH_ARM, TOUCH_SHOULDER, TOUCH_BACK, TOUCH_HEAD, TOUCH_WAIST, TOUCH_LEG,
+       TOUCH_CHEST, TOUCH_BUTT, TOUCH_GROIN, TOUCH_PARTS };
+/* What a touch did: welcome, put up with (a bit much, said so), unwelcome (pulled away), harassment. */
+enum { TOUCHED_WELCOME, TOUCHED_TOLERATED, TOUCHED_UNWELCOME, TOUCHED_HARASSED };
+
+/* Where on whom a joint (and the hand at it) is: TOUCH_*. Intimate: a leg, the chest, the bottom, between the legs
+   (unwelcome there: harassment; the waist is closeness, unwelcome is all). */
+int         Touch_part(const Object *whom, int joint, Vec3 hand);
+const char *Touch_partName(int part);
+bool        Touch_intimate(int part);
+/* How they feel about by: friendship and love (you: like, love; others: strangers, couples). */
+void        Touch_feelings(const Object *whom, const Object *by, float *like, float *love);
+/* Taken hold of there by by: their answer (TOUCHED_*), said, and what it does to how they feel. */
+int         Touch_react(World *world, Object *whom, Object *by, int part);
+/* A kind touch (calming, a hand on the shoulder): how they take it - true if it helps. */
+bool        Touch_calm(World *world, Object *whom, Object *by, int part);
+
+#endif /* PEOPLE_TOUCH_H */
diff --git a/entropie/src/world/cast.c b/entropie/src/world/cast.c
index 3510e17..ace6f2b 100644
--- a/entropie/src/world/cast.c
+++ b/entropie/src/world/cast.c
@@ -176,6 +176,8 @@ Object *Cast_create(int id, Vec3 at) {
     obj->work_trousers = id == 0; /* (you: work trousers, plenty of pockets) */
     obj->craving      = 0.3f + 0.15f * (float)(id % 3);
     obj->commitment   = c->commitment;
+    obj->like         = c->lives_here ? 0.45f : 0.5f + 0.25f * c->commitment; /* (you: the crew are friends; the hosts friendly) */
+    obj->love         = 0.05f + 0.2f * (float)((id * 2654435761u) >> 24 & 255u) / 255.0f; /* (a little, here and there) */
     obj->leads        = (int)c->leads;
     obj->helps        = (int)c->helps;
     for (int k = 0; k < ORDER_COUNT; k++) obj->skill[k] = c->character.skill[k];
diff --git a/entropie/src/world/guests.c b/entropie/src/world/guests.c
index e4885a9..4abca85 100644
--- a/entropie/src/world/guests.c
+++ b/entropie/src/world/guests.c
@@ -72,6 +72,8 @@ Object *Guest_create(int k, Vec3 at) {
     o->craving = 0.3f;
     o->commitment = 0.0f;
     o->fun = 0.55f + 0.15f * dice(k, 28);
+    o->like = 0.2f + 0.15f * dice(k, 40); /* (strangers to you) */
+    o->love = 0.1f * dice(k, 41);
     o->ai.character.willing = 0.2f + 0.3f * dice(k, 29);
     o->ai.character.tempo = 1.0f;
     o->ai.character.ponder = 0.5f;
diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h
index d86ed5b..45abcad 100644
--- a/entropie/src/world/object.h
+++ b/entropie/src/world/object.h
@@ -396,6 +396,10 @@ typedef struct Object {
     float      air_fall;      /* characters: in the air, falling this fast at most (m/s; for the landing) */
     Vec3       last_ground;   /* characters: where they last stood on something (fell through anyway: back there) */
     float      sprain;        /* characters: game s left limping on a twisted ankle (world/injury.c) */
+    float      like;          /* people: 0..1 how much they like you (friendship; people/touch.c) ... */
+    float      love;          /* ... 0..1 how drawn to you they are (closeness, more) ... */
+    struct Object *led_by;    /* ... walking along with this one, hand in hand (or arm in arm) */
+    float      harass_t;      /* people: game s till they might bother somebody again (the festival's drunk ones) */
     float      ladder_skill;  /* characters: practice climbing ladders one-handed (0..1) ... */
     float      ladder_wobble; /* ... nearly slipped: the next slip is a fall (seconds this counts) */
     Vec3       lean_normal;   /* ladders: away from what it leans on */
diff --git a/entropie/src/world/world.h b/entropie/src/world/world.h
index bea5b2c..ee9bb33 100644
--- a/entropie/src/world/world.h
+++ b/entropie/src/world/world.h
@@ -125,6 +125,9 @@ typedef struct World {
     int      why_heard;    /* ... bit k: came up at the Herberge (not seen on the ground) */
     Fest     fest;         /* the festival weekend (set by the game every frame) */
     BigTree  bigtree;      /* the big old tree's felling */
+    float    aware;        /* 0..1 how much the people here look out for each other (game_touch.c: the mood, kind acts, harassment seen) */
+    struct { struct Object *by, *whom; int what, part; } touched[8]; /* touches this frame worth noticing (people/touch.c) ... */
+    int      touched_n;    /* ... how many (the game looks at them: who saw it) */
     int      tree_fine;    /* EUR just fined for a tree that wasn't to be felled (the game says so, then 0) ... */
     int      tree_fine_kind; /* ... 1 one of Kurti's apple trees, 2 a protected one */
 } World;