Nimbin[12]?Games / Entropie / entropie/src/people/hands.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/src/people/hands.c 63.8 KB · 1376 lines raw
#include "people/hands.h"
#include "world/object.h"
#include "world/ladder.h"
#include "world/physics.h"

#define UMBRELLA_HAND_OUT   0.16f /* an open umbrella: the hand this far out to its side ... */
#define UMBRELLA_HAND_UP    1.16f /* ... this high ... */
#define UMBRELLA_HAND_AHEAD 0.17f /* ... and in front (all at 1.75 m tall) */
#define UMBRELLA_TOP        2.05f /* the shaft points at this high over the head's middle */

#define ONE_HAND_MAX_WEIGHT  10.0f
#define ONE_HAND_MAX_SIZE    0.6f
#define TWO_HAND_MAX_WEIGHT  35.0f
#define TWO_HAND_MAX_SIZE    1.1f
#define REACH_TIME           0.45f  /* seconds to bend down and take an item */
#define PICKUP_GAP           0.9f   /* max gap between body and item */
#define GRAB_GAP             0.6f
#define FRONT_CONE           0.35f  /* cos of the half angle in front of the character */
#define BODY_HALF_DEPTH      0.15f
#define GRIP_BREAK_DISTANCE  1.0f   /* pushed/pulled object got stuck too far away */
#define GRAB_HAND_SPACING    0.17f
#define CARRY_PALM_GAP       0.04f  /* palms just outside the item's sides */
#define CARRY_CHEST_GAP      0.18f  /* item distance from the chest (scaled) */
#define CARRY_GRIP_DEPTH     0.12f  /* palms this far behind the item's back face */
#define DRINK_TIME           1.6f   /* raise, tip, swallow, lower */
#define SIP_AT               0.85f
#define REFERENCE_WEIGHT     70.0f  /* bottle strength is given for this body weight */
#define DRINK_TILT           1.9f   /* radians the bottle tips back */
#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 */
#define BODY_HALF_WIDTH      0.17f  /* carried things stay outside this box around the spine */
#define BODY_CLEARANCE       0.11f  /* ... half its depth */
#define LIFT_CAPACITY       40.0f   /* kg an average person lifts with both hands */
#define LIFT_HEIGHT          0.6f   /* a team carries things this high off the ground */
#define LIFT_SPEED           1.5f   /* m/s up or down */
#define LEVEL_RATE           3.0f   /* a carried log tilts towards the carriers' heights */
#define GRIP_MISS            0.08f  /* a hand this far from its grip doesn't hold */
#define REGRAB_TIME          0.4f   /* ... for this long: take hold again */

static const Vec3 UP = { 0.0f, 1.0f, 0.0f };

/* ---------- Geometry helpers (main collider) ---------- */

static float extentAlong(const Object *o, Vec3 dir) {
    const Collider *c = &o->colliders[0];
    return fabsf(vec3_dot(c->axes[0], dir)) * c->half_extents.x +
           fabsf(vec3_dot(c->axes[1], dir)) * c->half_extents.y +
           fabsf(vec3_dot(c->axes[2], dir)) * c->half_extents.z;
}

static float largestSize(const Object *o) {
    Vec3 h = o->colliders[0].half_extents;
    return 2.0f * fmaxf(h.x, fmaxf(h.y, h.z));
}

static Vec3 itemCenter(const Object *o) { return Object_colliderCenter(o, 0); }

/* Point of the main collider closest to p (on its surface when p is outside). */
static Vec3 closestPoint(const Object *o, Vec3 p) {
    const Collider *c = &o->colliders[0];
    Vec3 center = itemCenter(o);
    Vec3 rel = vec3_sub(p, center);
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    Vec3 q = center;
    for (int k = 0; k < 3; k++)
        q = vec3_add(q, vec3_scale(c->axes[k], clampf(vec3_dot(rel, c->axes[k]), -half[k], half[k])));
    return q;
}

/* Horizontal distance from a body to the object's surface. */
static float gapAt(Vec3 position, float height, const Object *o, Vec3 *contact) {
    Vec3 chest = vec3_add(position, vec3(0.0f, height * 0.55f, 0.0f));
    Vec3 q = closestPoint(o, chest);
    if (contact) *contact = q;
    return vec3_length(vec3(q.x - position.x, 0.0f, q.z - position.z)) - BODY_HALF_DEPTH;
}

static float gapTo(const Object *who, const Object *o, Vec3 *contact) {
    return gapAt(who->position, who->look.height, o, contact);
}

float Hands_gapFrom(Vec3 position, float height, const Object *o) {
    return gapAt(position, height, o, NULL);
}

static Vec3 rightOf(Vec3 facing) { return vec3_normalize(vec3_cross(facing, UP)); }

static float scaleOf(const Object *who) { return who->look.height / 1.75f; }

static bool liftableOneHand(const Object *o) {
    bool tool = o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_LADDER || o->type == OBJ_UMBRELLA || o->type == OBJ_SCYTHE; /* long, but made for one hand */
    return o->weight <= ONE_HAND_MAX_WEIGHT && (tool || largestSize(o) <= ONE_HAND_MAX_SIZE);
}

static bool liftableTwoHands(const Object *o) {
    return o->weight <= TWO_HAND_MAX_WEIGHT && largestSize(o) <= TWO_HAND_MAX_SIZE;
}

/* Nearest loose object in front of the character within the given gap. */
static Object *findInFront(const Object *who, Object *const *objects, int count, float max_gap) {
    Vec3 facing = Object_facing(who);
    Object *best = NULL;
    float best_gap = max_gap;

    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o == who || o->is_static || o->is_character || o->is_held || o->type == OBJ_DOG) continue;

        Vec3 contact;
        float gap = gapTo(who, o, &contact);
        Vec3 d = vec3_sub(contact, who->position);
        d.y = 0.0f;
        float dist = vec3_length(d);
        Vec3 dir = dist > 1e-3f ? vec3_scale(d, 1.0f / dist) : facing;
        if (dist > 1e-3f && vec3_dot(dir, facing) < FRONT_CONE) continue;
        if (gap < best_gap) {
            best_gap = gap;
            best = o;
        }
    }
    return best;
}

/* How much weight a character can move by pushing or pulling. */
static float pushCapacity(const Object *who) {
    /* About 240 kg for an average person; heavier people push a bit more */
    return 240.0f * (who->strength / 100.0f) * sqrtf(fmaxf(who->weight, 1.0f) / REFERENCE_WEIGHT) * Object_vigor(who);
}

/* Share of the team's effort that actually moves the object (0..1).
   Below a quarter of its weight the object does not budge. */
static float teamShare(float capacity, float weight) {
    if (capacity < weight * 0.25f) return 0.0f;
    return capacity / (capacity + weight);
}

/* Object space: relative to the pivot, before rotation (turns with the object). */
static Vec3 toObject(const Object *o, Vec3 world) {
    Vec3 rel = vec3_sub(world, vec3_add(o->position, o->pivot));
    return mat4_mul_dir_transposed(mat4_rotate_yxz(o->rotation), rel);
}

static Vec3 fromObject(const Object *o, Vec3 local) {
    return vec3_add(vec3_add(o->position, o->pivot), mat4_mul_dir(mat4_rotate_yxz(o->rotation), local));
}

static float yawFacing(Vec3 dir) {
    return atan2f(dir.z, dir.x) + PI_F / 2.0f; /* inverse of Object_facing */
}

static bool twoHanded(const Hands *h) {
    return h->item[HAND_LEFT] && h->item[HAND_LEFT] == h->item[HAND_RIGHT];
}

/* ---------- Actions ---------- */

void Hands_drop(Object *who, int hand) {
    Hands *h = &who->hands;
    Object *item = h->item[hand];
    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++) {
        if (h->item[i] == item) {
            h->item[i]  = NULL;
            h->state[i] = HAND_FREE;
        }
    }
    item->is_held  = false;
    item->velocity = vec3(who->velocity.x, 0.0f, who->velocity.z);
    if (item->type == OBJ_CRATE) { /* (set down standing, not tipped over) */
        item->rotation = vec3(0.0f, who->rotation.y, 0.0f);
        Object_syncColliders(item);
    }
    if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER) { /* put down flat, where it was */
        Vec3 center = Object_colliderCenter(item, 0);
        item->rotation = item->type == OBJ_LADDER ? vec3(PI_F / 2.0f, who->rotation.y, 0.0f)
                                                  : vec3(0.0f, who->rotation.y - PI_F / 2.0f, PI_F / 2.0f); /* (a tool on its side: the blade flat, not up) */
        Object_syncColliders(item);
        item->position = vec3_sub(center, item->colliders[0].offset);
    }
}

void Hands_setAside(Object *who, Object *out[2]) {
    Hands *h = &who->hands;
    out[0] = out[1] = NULL;
    if (h->grabbed) Hands_release(who);
    Vec3 f = Object_facing(who), side = vec3(-f.z, 0.0f, f.x);
    for (int i = 0; i < 2; i++) {
        Object *item = h->item[i];
        if (h->state[i] != HAND_HOLDING || !item) continue;
        if (i == HAND_RIGHT && h->item[HAND_LEFT] == item) continue; /* (two-handed: once) */
        Hands_drop(who, i);
        out[i] = item;
        if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER) continue; /* (laid flat) */
        item->rotation = vec3(0.0f, who->rotation.y, 0.0f); /* upright on the ground, a step to the side */
        Object_syncColliders(item);
        float out_by = (i == HAND_RIGHT ? 1.0f : -1.0f) * (0.35f + item->colliders[0].half_extents.x);
        Vec3 spot = vec3_add(who->position, vec3_add(vec3_scale(side, out_by), vec3_scale(f, 0.15f)));
        spot.y += item->colliders[0].half_extents.y + 0.03f;
        item->position = vec3_sub(spot, item->colliders[0].offset);
        item->velocity = vec3_zero();
        Object_syncColliders(item);
    }
}

void Hands_touch(Object *who, Vec3 point) {
    who->hands.touching = true;
    who->hands.touch_point = point;
}

void Hands_holdEdge(Object *who, Vec3 point, Vec3 across, float crouch) {
    who->hands.on_edge = true;
    who->hands.edge_crouch = crouch;
    who->hands.edge_point = point;
    who->hands.edge_across = across;
}

Object *Hands_inFront(const Object *who, Object *const *objects, int count) {
    return findInFront(who, objects, count, PICKUP_GAP);
}

int Hands_freeHand(const Object *who) {
    const Hands *h = &who->hands;
    int main = who->main_hand;
    return h->state[main] == HAND_FREE ? main : h->state[1 - main] == HAND_FREE ? 1 - main : -1;
}

/* The hand that throws: the main one if it holds something, else the other. */
static int throwHand(const Object *who) {
    const Hands *h = &who->hands;
    int main = who->main_hand;
    return h->state[main] == HAND_HOLDING ? main : h->state[1 - main] == HAND_HOLDING ? 1 - main : -1;
}

/* Wound up this long, it goes overhand (sooner for the lively); light things only. */
static bool goesOverhand(const Object *who, const Object *item) {
    return who->hands.wind >= OVERHAND_AFTER * (1.2f - 0.5f * who->temperament.energy) &&
           item->weight < OVERHAND_WEIGHT && !twoHanded(&who->hands);
}

void Hands_windUp(Object *who) {
    if (throwHand(who) < 0) { who->hands.wind = 0.0f; return; }
    who->hands.winding = true;
    if (who->hands.wind <= 0.0f) who->hands.wind = 1e-3f;
}

bool Hands_isWindingUp(const Object *who) {
    return who->hands.wind > 0.0f;
}

bool Hands_throw(Object *who, Vec3 dir) {
    Hands *h = &who->hands;
    int hand = throwHand(who);
    if (hand < 0) { h->wind = 0.0f; return false; }
    Object *item = h->item[hand];
    bool both = twoHanded(h), over = goesOverhand(who, item);
    float power = clampf(h->wind / FULL_WIND, 0.0f, 1.0f);
    h->wind = 0.0f;

    Hands_drop(who, hand);
    /* Overhand goes harder and flatter; underhand is a lob. Heavier things don't fly as fast */
    float speed = THROW_SPEED * (over ? 0.9f + 0.45f * power : 0.65f + 0.25f * power);
    speed = clampf(speed - item->weight * 0.35f, 2.5f, speed);
    dir = vec3_normalize(vec3_add(vec3_normalize(dir), vec3(0.0f, over ? 0.1f : 0.22f, 0.0f)));
    item->velocity    = vec3_add(vec3_scale(dir, speed), vec3(who->velocity.x, 0.0f, who->velocity.z));
    item->thrown_by   = who;
    item->thrown_time = 0.0f;
    for (int i = 0; i < 2; i++)
        if (i == hand || both) {
            h->throw_time[i] = THROW_TIME;
            h->overhand[i] = over;
        }
    return true;
}

/* Starts reaching for item with this hand, or both hands if it is too big. */
static bool startPickUp(Object *who, int hand, Object *item) {
    Hands *h = &who->hands;
    int other = 1 - hand;
    if (liftableOneHand(item)) {
        h->state[hand] = HAND_REACHING;
        h->item[hand]  = item;
        h->timer[hand] = 0.0f;
        return true;
    }
    if (liftableTwoHands(item) && h->state[other] != HAND_HOLDING && h->state[other] != HAND_REACHING) {
        if (h->state[other] == HAND_GRABBING) Hands_release(who);
        for (int i = 0; i < 2; i++) {
            h->state[i] = HAND_REACHING;
            h->item[i]  = item;
            h->timer[i] = 0.0f;
        }
        return true;
    }
    return false;
}

/* Put down near a table, a counter, a crate (something solid at about hand
   height in front): set on top of it, not dropped to the floor. */
#define PLACE_LOWEST  0.3f  /* m over the feet: a surface this high and up counts ... */
#define PLACE_HIGHEST 1.5f  /* ... up to this */
#define PLACE_HEAVY   20.0f /* kg: loose things this heavy hold something put on them */

static float surfaceUnder(const Object *who, const Object *item, Object *const *objects, int count, Vec3 at, float from_y) {
    float best = -1e9f;
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o == who || o == item || o->is_character || o->is_held || o->collider_count == 0 || o->type == OBJ_DECAL ||
            o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->type == OBJ_STRUCTURE || (!o->is_static && o->weight < PLACE_HEAVY)) continue;
        if (fabsf(o->position.x - at.x) > 4.0f || fabsf(o->position.z - at.z) > 4.0f) continue;
        for (int c = 0; c < o->collider_count; c++) {
            if (fabsf(o->colliders[c].axes[1].y) < 0.95f) continue; /* (a level top only) */
            float t = Object_rayToCollider(o, c, vec3(at.x, from_y, at.z), vec3(0.0f, -1.0f, 0.0f), NULL);
            if (t >= 0.0f) best = fmaxf(best, from_y - t);
        }
    }
    return best;
}

static bool placeOnSurface(Object *who, Object *item, Object *const *objects, int count) {
    if (item->type == OBJ_LADDER || item->weight > 15.0f || item->collider_count == 0) return false;
    const Vec3 f = Object_facing(who);
    const float feet = who->position.y, from = feet + PLACE_HIGHEST + 0.3f;
    const float r = fmaxf(item->colliders[0].half_extents.x, item->colliders[0].half_extents.z);
    Vec3 tries[4] = { Object_colliderCenter(item, 0) };
    for (int k = 1; k < 4; k++) tries[k] = vec3_add(who->position, vec3_scale(f, 0.25f + 0.2f * (float)k));
    int pick = -1;
    float pick_top = -1e9f;
    for (int k = 0; k < 4; k++) { /* (the highest: a table over the chair in front of it) */
        float top = surfaceUnder(who, item, objects, count, tries[k], from);
        if (top < feet + PLACE_LOWEST || top > feet + PLACE_HIGHEST || top <= pick_top + 0.05f) continue;
        /* room for all of it up there (its edges on the top too) */
        bool room = true;
        Vec3 side = vec3(-f.z, 0.0f, f.x);
        for (int e = 0; e < 4 && room; e++) {
            Vec3 d = vec3_scale(e < 2 ? f : side, (e % 2 ? -1.0f : 1.0f) * r * 0.8f);
            room = fabsf(surfaceUnder(who, item, objects, count, vec3_add(tries[k], d), from) - top) < 0.03f;
        }
        if (room) { pick = k; pick_top = top; }
    }
    if (pick >= 0) {
        if (item->type != OBJ_AXE && item->type != OBJ_SHEARS) item->rotation = vec3(0.0f, who->rotation.y, 0.0f); /* (upright; tools lie flat) */
        Object_syncColliders(item);
        float bottom = Object_bottom(item) - item->position.y;
        item->position = vec3(tries[pick].x - item->colliders[0].offset.x, pick_top - bottom + 0.005f, tries[pick].z - item->colliders[0].offset.z);
        item->velocity = vec3_zero();
        Object_syncColliders(item);
        return true;
    }
    return false;
}

bool Hands_pickUpOrDrop(Object *who, int hand, Object *const *objects, int count) {
    Hands *h = &who->hands;
    if (h->state[hand] == HAND_HOLDING || h->state[hand] == HAND_REACHING) {
        Object *item = h->state[hand] == HAND_HOLDING ? h->item[hand] : NULL;
        Hands_drop(who, hand);
        if (item && !item->is_held && !placeOnSurface(who, item, objects, count) && Object_isBottle(item)) item->drop_tip = true;
        return true;
    }
    if (h->state[hand] == HAND_GRABBING) Hands_release(who);

    Object *item = findInFront(who, objects, count, PICKUP_GAP);
    return item && startPickUp(who, hand, item);
}

int Hands_takeInFront(Object *who, Object *const *objects, int count) {
    Hands *h = &who->hands;
    if (h->grabbed) return 0;
    Object *item = findInFront(who, objects, count, PICKUP_GAP);
    if (!item) return 0;
    if (liftableOneHand(item)) {
        int hand = Hands_freeHand(who);
        if (hand < 0) return -1;
        return startPickUp(who, hand, item) ? 1 : 0;
    }
    if (liftableTwoHands(item)) {
        if (h->state[HAND_LEFT] != HAND_FREE || h->state[HAND_RIGHT] != HAND_FREE) return -1;
        return startPickUp(who, HAND_RIGHT, item) ? 1 : 0;
    }
    /* Too big to carry in the hands: take hold of it instead */
    if (h->state[HAND_LEFT] != HAND_FREE && h->state[HAND_RIGHT] != HAND_FREE) return -1;
    return Hands_grabObject(who, item) ? 2 : 0;
}

bool Hands_pickUpObject(Object *who, int hand, Object *item) {
    const Hands *h = &who->hands;
    if (h->state[hand] != HAND_FREE || h->grabbed) return false;
    if (!item || item->is_static || item->is_character || item->is_held) return false;
    if (gapTo(who, item, NULL) > PICKUP_GAP) return false;
    return startPickUp(who, hand, item);
}

static void startHolding(Object *who, int hand);

bool Hands_receive(Object *who, int hand, Object *item) {
    Hands *h = &who->hands;
    if (h->state[hand] != HAND_FREE || h->grabbed || !item || item->is_held || !liftableOneHand(item)) return false;
    h->state[hand] = HAND_REACHING;
    h->item[hand]  = item;
    startHolding(who, hand);
    return true;
}

bool Hands_grab(Object *who, Object *const *objects, int count) {
    if (who->hands.grabbed) return false;
    Object *target = findInFront(who, objects, count, GRAB_GAP);
    return target && Hands_grabObject(who, target);
}

bool Hands_grabObject(Object *who, Object *target) {
    Hands *h = &who->hands;
    if (h->grabbed || !target || target->is_static || target->is_held) return false;
    if (h->state[HAND_LEFT] != HAND_FREE && h->state[HAND_RIGHT] != HAND_FREE) return false;

    /* Close enough? Touch the nearest point of the surface at a comfortable height. */
    const float s = scaleOf(who);
    Vec3 nearest;
    if (gapTo(who, target, &nearest) > GRAB_GAP) return false;
    Vec3 center = itemCenter(target);
    float ext_y = extentAlong(target, UP);
    float height = clampf(who->position.y + 1.0f * s, center.y - ext_y + 0.05f, center.y + ext_y - 0.05f);
    Vec3 contact = closestPoint(target, vec3(nearest.x, height, nearest.z));

    Vec3 toward = vec3_sub(who->position, contact);   /* from the object towards us */
    toward.y = 0.0f;
    if (vec3_length(toward) < 1e-3f) toward = vec3_sub(who->position, center), toward.y = 0.0f;
    if (vec3_length(toward) < 1e-3f) return false;
    toward = vec3_normalize(toward);
    Vec3 side = vec3_normalize(vec3_cross(UP, toward)); /* our right, seen facing the object */

    bool both = h->state[HAND_LEFT] == HAND_FREE && h->state[HAND_RIGHT] == HAND_FREE;
    float spacing = fminf(GRAB_HAND_SPACING, extentAlong(target, side));
    for (int i = 0; i < 2; i++) {
        if (h->state[i] != HAND_FREE) continue;
        float offset = both ? (i == HAND_LEFT ? -spacing : spacing) : 0.0f;
        Vec3 point = closestPoint(target, vec3_add(contact, vec3_scale(side, offset)));
        h->grab_point[i] = toObject(target, point);
        h->state[i] = HAND_GRABBING;
    }

    /* Face the object; from now on the pair keeps this layout in the world */
    h->grabbed      = target;
    h->grab_heading = vec3_scale(toward, -1.0f);
    h->grab_vector  = vec3_sub(target->position, who->position);
    h->grab_share   = teamShare(pushCapacity(who), Object_dragWeight(target));
    who->rotation.y = yawFacing(h->grab_heading);
    Object_syncColliders(who);
    return true;
}

#define MAX_TEAM 8          /* people holding one thing at most */
#define TURN_RATE   2.0f   /* rad/s an easy thing turns (less for heavy or big ones) */
#define TURN_SOLID 40.0f   /* kg: loose things lighter than this are pushed aside, not in the way */

static Vec3 turnVec(Vec3 v, float a) { /* about the vertical, a: the way the heading angle grows */
    float c = cosf(a), s = sinf(a);
    return vec3(v.x * c - v.z * s, v.y, v.x * s + v.z * c);
}

/* How deep the object is in solid things around it (the deepest). */
static float solidOverlap(const Object *o, Object *const *objects, int count) {
    float most = 0.0f;
    for (int i = 0; i < count; i++) {
        const Object *b = objects[i];
        if (b == o || b->is_character || b->is_held || b->collider_count == 0 || b->type == OBJ_BUSH) continue;
        if (!b->is_static && b->weight < TURN_SOLID) continue;
        if (fabsf(b->position.x - o->position.x) > 8.0f || fabsf(b->position.z - o->position.z) > 8.0f) {
            if (!b->is_static || vec3_length(b->colliders[0].half_extents) < 4.0f) continue; /* (far: only big buildings matter) */
        }
        most = fmaxf(most, Physics_penetration(o, b));
    }
    return most;
}

float Hands_turnGrabbed(Object *who, Object *const *objects, int count, float want, float dt) {
    Object *o = who->hands.grabbed;
    if (!o || o->is_static || dt <= 0.0f || fabsf(want) < 1e-4f) return 0.0f;
    /* Everybody holding it, and how strong they are together */
    Object *team[MAX_TEAM];
    int n = 0;
    float capacity = 0.0f;
    for (int k = 0; k < count && n < MAX_TEAM; k++)
        if (objects[k]->is_character && objects[k]->hands.grabbed == o) {
            team[n++] = objects[k];
            capacity += pushCapacity(objects[k]);
        }
    float share = o->lifted ? 1.0f : teamShare(capacity, Object_dragWeight(o));
    float reach = fmaxf(extentAlong(o, vec3(1.0f, 0.0f, 0.0f)), extentAlong(o, vec3(0.0f, 0.0f, 1.0f)));
    float most = TURN_RATE * share / (0.5f + reach) * dt;
    float a = clampf(want, -most, most);
    if (fabsf(a) < 1e-5f) return 0.0f;

    /* Turn it; if that takes it (deeper) into something solid, it doesn't turn */
    float before = solidOverlap(o, objects, count);
    o->rotation.y += a;
    Object_syncColliders(o);
    if (solidOverlap(o, objects, count) > before + 0.01f) {
        o->rotation.y -= a;
        Object_syncColliders(o);
        return 0.0f;
    }
    /* Everybody holding it walks round with it, still facing it the same way */
    for (int k = 0; k < n; k++) {
        Hands *h = &team[k]->hands;
        h->grab_vector = turnVec(h->grab_vector, a);
        h->grab_heading = turnVec(h->grab_heading, a);
        Vec3 at = vec3_sub(o->position, h->grab_vector);
        team[k]->position.x = at.x;
        team[k]->position.z = at.z;
        team[k]->rotation.y = yawFacing(h->grab_heading);
        Object_syncColliders(team[k]);
    }
    return a;
}

void Hands_release(Object *who) {
    Hands *h = &who->hands;
    for (int i = 0; i < 2; i++)
        if (h->state[i] == HAND_GRABBING) h->state[i] = HAND_FREE;
    h->grabbed = NULL;
}

bool Hands_isGrabbing(const Object *who) {
    return who->hands.grabbed != NULL;
}

bool Hands_carries(const Object *who, int type) {
    for (int i = 0; i < 2; i++)
        if (who->hands.item[i] && who->hands.state[i] == HAND_HOLDING && (int)who->hands.item[i]->type == type) return true;
    return false;
}

float Hands_moveFactor(const Object *who) {
    const Hands *h = &who->hands;
    if (h->grabbed && h->grabbed->lifted) return 0.6f;
    if (h->grabbed) return 0.35f + 0.45f * h->grab_share;
    if (twoHanded(h)) return 0.7f;
    return 1.0f;
}

/* ---------- Drinking ---------- */

int Hands_bottleHand(const Object *who) {
    const Hands *h = &who->hands;
    for (int k = 0; k < 2; k++) {
        int i = k == 0 ? who->main_hand : 1 - who->main_hand;
        Object *item = h->item[i];
        bool consumable = item && (Object_isBottle(item) || Object_isFood(item));
        if (h->state[i] == HAND_HOLDING && consumable && item->sips > 0 && !twoHanded(h))
            return i;
    }
    return -1;
}

bool Hands_isDrinking(const Object *who) {
    return who->hands.drink_hand >= 0;
}

bool Hands_drink(Object *who) {
    Hands *h = &who->hands;
    if (h->drink_hand >= 0) return false;
    int hand = Hands_bottleHand(who);
    if (hand < 0) return false;
    h->drink_hand   = hand;
    h->drink_time   = 0.0f;
    h->drink_sipped = false;
    return true;
}

/* How far the bottle is tipped back (radians) and how much it is held at its
   middle instead of hanging from the hand (0..1) at time t of a sip. */
static float drinkTilt(float t) {
    return DRINK_TILT * smoothstepf(0.25f, 0.6f, t) * (1.0f - smoothstepf(1.15f, 1.5f, t));
}

/* Food isn't tipped back, a bottle is. */
static float tiltOf(const Object *item, float t) {
    return item && Object_isFood(item) ? 0.0f : drinkTilt(t);
}

static float drinkGrip(float t) {
    return smoothstepf(0.0f, 0.25f, t) * (1.0f - smoothstepf(1.35f, DRINK_TIME, t));
}

/* Where along a bottle the hand holds it, from its middle towards the top (m):
   low around the body, or (rarely) at the neck. Sliding lower to drink. */
#define FIST_AT 0.6f /* from the wrist to the knuckles: where a held thing sits in the hand */

static float bottleGrip(const Object *who, int hand, const Object *bottle) {
    const Hands *h = &who->hands;
    float half = bottle->colliders[0].half_extents.y;
    float hold = h->by_neck[hand] ? half - 0.035f : -0.45f * half;
    if (h->drink_hand != hand) return hold;
    return lerpf(hold, -0.2f * half, drinkGrip(h->drink_time));
}

/* Direction the bottle's opening points when tipped back by angle a. */
static Vec3 bottleTop(Vec3 facing, float a) {
    return vec3_sub(vec3_scale(UP, cosf(a)), vec3_scale(facing, sinf(a)));
}

static Vec3 mouthOf(const Object *who) {
    const float s = scaleOf(who);
    const Skeleton *sk = &who->skeleton;
    Vec3 local = vec3_add(sk->joints[JOINT_HEAD],
                          vec3_add(vec3_scale(sk->head_forward, 0.085f * s), vec3_scale(sk->head_up, -0.05f * s)));
    return Figure_toWorld(who->position, Object_facing(who), local);
}

/* Hand target for a sip: carry pose, then bottle opening at the mouth. */
static Vec3 drinkTarget(const Object *who, Vec3 carry_target, float t) {
    const int hand = who->hands.drink_hand;
    const Object *bottle = who->hands.item[hand];
    float half = bottle->colliders[0].half_extents.y; /* along the bottle, before tipping */
    Vec3 top = bottleTop(Object_facing(who), tiltOf(bottle, t));
    Vec3 at_mouth = Object_isFood(bottle)
        ? vec3_add(mouthOf(who), vec3_scale(Object_facing(who), half + 0.01f)) /* a bite */
        : vec3_sub(mouthOf(who), vec3_scale(top, half - bottleGrip(who, hand, bottle) + 0.01f));
    float k = smoothstepf(0.0f, 0.35f, t) * (1.0f - smoothstepf(1.3f, DRINK_TIME, t));
    return vec3_lerp(carry_target, at_mouth, k);
}

static void advanceDrink(Object *who, float dt) {
    Hands *h = &who->hands;
    if (h->drink_hand < 0) return;
    h->drink_time += dt;
    Object *bottle = h->item[h->drink_hand];
    if (!h->drink_sipped && h->drink_time >= SIP_AT) {
        h->drink_sipped = true;
        if (bottle && bottle->sips > 0) {
            bottle->sips--;
            /* Heavier and more hardened people get drunk slower */
            float body = REFERENCE_WEIGHT / fmaxf(who->weight, 30.0f);
            who->drunk = fminf(who->drunk + bottle->alcohol * body / fmaxf(who->tolerance, 0.2f), 1.5f);
            Needs *n = &who->needs;
            n->thirst  = fmaxf(0.0f, n->thirst - bottle->quench);
            n->hunger  = fmaxf(0.0f, n->hunger - bottle->nourish);
            n->stamina = fminf(1.0f, n->stamina + bottle->refresh);
            n->sleepiness = fmaxf(0.0f, n->sleepiness - bottle->wake);
            n->bladder = fminf(1.0f, n->bladder + (bottle->quench * 0.25f + bottle->alcohol * 0.3f) / Object_bladder(who));
        }
    }
    if (h->drink_time >= DRINK_TIME) {
        int hand = h->drink_hand;
        h->drink_hand = -1;
        if (bottle && Object_isFood(bottle) && bottle->sips <= 0) { /* all eaten */
            Hands_drop(who, hand);
            Object_useUp(bottle);
        }
    }
}

/* ---------- Per frame ---------- */

/* Crouch needed to bring the hands down to a world height. */
static float crouchFor(const Object *who, float world_y) {
    const float s = scaleOf(who);
    return clampf((0.95f * s - (world_y - who->position.y)) / (0.75f * s), 0.0f, 1.0f);
}

static void startHolding(Object *who, int hand) {
    Hands *h = &who->hands;
    Object *item = h->item[hand];
    const Vec3 facing = Object_facing(who);
    const Mat4 r = mat4_rotate_yxz(item->rotation);
    for (int i = 0; i < 2; i++) {
        if (h->item[i] != item) continue;
        h->state[i] = HAND_HOLDING;
        /* Tools are held the right way round; anything else as it was picked up */
        bool tool = item->type == OBJ_CHAINSAW;
        h->item_yaw[i] = tool ? 0.0f : item->rotation.y - who->rotation.y;
        /* From the way it lay, it turns into the hand */
        h->settle[i]   = 0.0f;
        /* Bottles: now and then (more so when drunk) grabbed by the neck */
        float dice = fabsf(fmodf(sinf(who->body.clock * 12.9898f + item->position.x * 78.233f) * 43758.547f, 1.0f));
        h->by_neck[i] = Object_isBottle(item) && dice < 0.12f + 0.35f * fminf(who->drunk, 1.0f);
        h->settle_y[i] = Figure_toLocal(vec3_zero(), facing, mat4_mul_dir(r, UP));
        h->settle_z[i] = Figure_toLocal(vec3_zero(), facing, mat4_mul_dir(r, vec3(0.0f, 0.0f, 1.0f)));
    }
    if (item->type == OBJ_CHAINSAW) item->rotation.x = item->rotation.z = 0.0f;
    item->is_held = true;
    if (item->type == OBJ_FIREWOOD) item->stored = 0; /* taken off the pile */
    item->toppled = false;
    item->thrown_by = NULL;
    item->velocity = vec3_zero();
    item->angular_velocity = vec3_zero();
}

/* What a character can lift with the hands that hold on. */
static float liftCapacity(const Object *who) {
    int hands = 0;
    for (int i = 0; i < 2; i++) hands += who->hands.state[i] == HAND_GRABBING;
    return LIFT_CAPACITY * 0.5f * (float)hands * (who->strength / 100.0f) *
           sqrtf(fmaxf(who->weight, 1.0f) / REFERENCE_WEIGHT) * Object_vigor(who);
}

#define END_LIFT_SHARE 0.5f  /* one end up: that end's half of the weight */
#define END_DRAG       0.75f /* dragging the other end along: slower */
#define END_REACH      0.8f  /* held this close to an end to lift it there */

/* Long things (logs): one end can be lifted, the other drags on the ground. */
static bool endLiftable(const Object *o) {
    return o->type == OBJ_LOG && o->collider_count > 0 && o->colliders[0].half_extents.y >= 0.7f;
}

/* One end up at the holder's hip, the other on the ground behind: it pivots
   on the end on the ground. Eased up (LIFT_SPEED). */
static void endLift(Object *o, Object *holder, float dt) {
    const Collider *c = &o->colliders[0];
    const float hl = c->half_extents.y, r = c->half_extents.x;
    Vec3 center = itemCenter(o), axis = c->axes[1];
    Vec3 e1 = vec3_add(center, vec3_scale(axis, hl)), e2 = vec3_sub(center, vec3_scale(axis, hl));
    Vec3 hp = holder->position;
    float d1 = vec3_length(vec3(e1.x - hp.x, 0.0f, e1.z - hp.z)), d2 = vec3_length(vec3(e2.x - hp.x, 0.0f, e2.z - hp.z));
    Vec3 near = d1 < d2 ? e1 : e2, far = d1 < d2 ? e2 : e1;
    far.y = hp.y + r; /* (on the ground: as level as where you stand) */
    Vec3 flat = vec3(near.x - far.x, 0.0f, near.z - far.z);
    if (vec3_length(flat) < 1e-3f) return;
    flat = vec3_normalize(flat);
    float goal = hp.y + LIFT_HEIGHT;
    float y = near.y + clampf(goal - near.y, -LIFT_SPEED * dt, LIFT_SPEED * dt);
    float dh = clampf(y - far.y, 0.0f, 1.8f * hl);
    Vec3 u = vec3_normalize(vec3_add(vec3_scale(flat, sqrtf(fmaxf(4.0f * hl * hl - dh * dh, 1e-4f))), vec3(0.0f, dh, 0.0f)));
    if (vec3_dot(u, axis) < 0.0f) u = vec3_scale(u, -1.0f); /* (keep its own way round) */
    Vec3 z = c->axes[2];
    z = vec3_sub(z, vec3_scale(u, vec3_dot(z, u)));
    if (vec3_length(z) < 1e-3f) return;
    Vec3 new_center = vec3_add(far, vec3_scale(vec3_dot(u, flat) >= 0.0f ? u : vec3_scale(u, -1.0f), hl));
    o->rotation = euler_yxz_from_axes(u, vec3_normalize(z));
    o->position = vec3_add(o->position, vec3_sub(new_center, center));
    Object_syncColliders(o);
    Vec3 shift = vec3_sub(new_center, center); /* (tilting it moved its middle: it's held there now) */
    holder->hands.grab_vector = vec3_add(holder->hands.grab_vector, vec3(shift.x, 0.0f, shift.z));
}

static float teamLift(const Object *o, Object *const *objects, int count) {
    float capacity = 0.0f;
    for (int k = 0; k < count; k++)
        if (objects[k]->is_character && objects[k]->hands.grabbed == o) capacity += liftCapacity(objects[k]);
    return capacity;
}

int Hands_toggleLift(Object *who, Object *const *objects, int count) {
    Object *o = who->hands.grabbed;
    if (!o) return -1;
    if (o->lifted) {
        o->lifted = o->end_up = false; /* put it down: gravity takes over */
        return 0;
    }
    float can = teamLift(o, objects, count);
    if (can < o->weight) {
        /* long: one end up, the other stays on the ground (half the weight) */
        if (!endLiftable(o) || can < END_LIFT_SHARE * o->weight) return -1;
        int holders = 0;
        for (int k = 0; k < count; k++) holders += objects[k]->is_character && objects[k]->hands.grabbed == o;
        if (holders != 1) return -1;
        /* held near one end: the hands slide to that end, either side of it */
        Hands *h = &who->hands;
        const Collider *c = &o->colliders[0];
        Vec3 center = itemCenter(o), grip = vec3_zero();
        int n = 0;
        for (int i = 0; i < 2; i++)
            if (h->state[i] == HAND_GRABBING) { grip = vec3_add(grip, fromObject(o, h->grab_point[i])); n++; }
        if (n == 0) return -1;
        float along = vec3_dot(vec3_sub(vec3_scale(grip, 1.0f / (float)n), center), c->axes[1]);
        if (c->half_extents.y - fabsf(along) > END_REACH) return -3;
        Vec3 end = vec3_add(center, vec3_scale(c->axes[1], (along >= 0.0f ? 1.0f : -1.0f) * (c->half_extents.y - 0.12f)));
        Vec3 right = rightOf(Object_facing(who));
        for (int i = 0; i < 2; i++)
            if (h->state[i] == HAND_GRABBING)
                h->grab_point[i] = toObject(o, vec3_add(end, vec3_scale(right, (i == HAND_LEFT ? -1.0f : 1.0f) * c->half_extents.x * 0.9f)));
        o->lifted = o->end_up = true;
        o->velocity = vec3_zero();
        return 2;
    }
    o->lifted = true;
    o->end_up = false;
    o->velocity = vec3_zero();
    return 1;
}

/* Lowest point of an object's main collider. */
static float bottomOf(const Object *o) {
    return itemCenter(o).y - extentAlong(o, UP);
}


/*
 * A long thing carried by several people: each end goes up to the height of
 * the people holding it there, so it's lifted evenly (and follows a slope).
 * Tilts it a little at a time; returns the height its lowest point should go to.
 */
static float levelLift(Object *o, Object *const *team, int n, float dt) {
    Vec3 center = itemCenter(o);
    Vec3 axis = o->colliders[0].axes[1]; /* logs and trunks run along their local Y */
    Vec3 along = vec3(axis.x, 0.0f, axis.z);
    if (vec3_length(along) < 1e-3f) return center.y;
    along = vec3_normalize(along);

    /* Least squares: wanted height of the bottom over the position along the log */
    float st = 0.0f, sh = 0.0f, stt = 0.0f, sth = 0.0f;
    for (int k = 0; k < n; k++) {
        float t = vec3_dot(vec3_sub(team[k]->position, center), along);
        float h = team[k]->position.y + LIFT_HEIGHT;
        st += t; sh += h; stt += t * t; sth += t * h;
    }
    float fn = (float)n, var = stt - st * st / fn;
    float slope = var > 0.05f ? clampf((sth - st * sh / fn) / var, -0.6f, 0.6f) : 0.0f;
    float mean_t = st / fn, base = sh / fn - slope * mean_t; /* height at the center */

    /* Tilt towards that slope */
    float now = axis.y / fmaxf(vec3_length(vec3(axis.x, 0.0f, axis.z)), 1e-3f);
    if (vec3_dot(vec3(axis.x, 0.0f, axis.z), along) < 0.0f) now = -now;
    now += (slope - now) * fminf(1.0f, LEVEL_RATE * dt);
    Vec3 tilted = vec3_normalize(vec3_add(along, vec3(0.0f, now, 0.0f)));
    if (vec3_dot(tilted, axis) < 0.0f) tilted = vec3_scale(tilted, -1.0f);
    Vec3 z = o->colliders[0].axes[2];
    z = vec3_sub(z, vec3_scale(tilted, vec3_dot(z, tilted)));
    if (vec3_length(z) > 1e-3f) {
        o->rotation = euler_yxz_from_axes(tilted, vec3_normalize(z));
        Object_syncColliders(o);
    }
    return base - o->colliders[0].half_extents.x; /* its underside at the middle */
}

/* People (not holding it) the load would go into, moved by move: their
   weight (they stand against it), and each gets told it shoves them. */
#define BYSTANDER_HOLD 1.0f /* x their weight against the load */

static float inTheWay(Object *o, Object *const *team, int n, Object *const *objects, int count, Vec3 move) {
    Vec3 dir = vec3(move.x, 0.0f, move.z);
    float len = vec3_length(dir);
    if (len < 1e-5f) return 0.0f;
    dir = vec3_scale(dir, 1.0f / len);
    Vec3 was = o->position;
    o->position = vec3_add(was, vec3_scale(dir, fmaxf(len, 0.02f))); /* (a little ahead) */
    Object_syncColliders(o);
    float weight = 0.0f;
    for (int k = 0; k < count; k++) {
        Object *c = objects[k];
        if (!c->is_character || c->hands.grabbed == o || c->on_ladder || c->climbing) continue;
        bool mine = false;
        for (int t = 0; t < n; t++) mine = mine || team[t] == c;
        if (mine || fabsf(c->position.x - was.x) > 4.0f || fabsf(c->position.z - was.z) > 4.0f) continue;
        if (vec3_dot(vec3_sub(c->position, was), dir) <= 0.0f || Physics_penetration(o, c) <= 0.0f) continue;
        weight += c->weight;
        c->shoved = dir;
        c->shove_load = was;
    }
    o->position = was;
    Object_syncColliders(o);
    return weight;
}

void Hands_solveGrabs(Object *const *objects, int count, float dt) {
    for (int i = 0; i < count; i++) objects[i]->shoved = vec3_zero(); /* (told again below while it goes on) */
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        if (o->is_static || o->is_character) continue;

        /* Everybody holding this object */
        Object *team[MAX_TEAM];
        Vec3 want[MAX_TEAM];
        int n = 0;
        Vec3 sum = vec3_zero();
        float capacity = 0.0f, lift = 0.0f, ground = 0.0f;
        for (int k = 0; k < count && n < MAX_TEAM; k++) {
            Object *c = objects[k];
            if (!c->is_character || c->hands.grabbed != o) continue;
            Vec3 d = vec3_sub(vec3_add(c->position, c->hands.grab_vector), o->position);
            d.y = 0.0f;
            if (o->is_held || vec3_length(d) > GRIP_BREAK_DISTANCE) { /* lost grip */
                Hands_release(c);
                continue;
            }
            team[n] = c;
            want[n] = d;
            sum = vec3_add(sum, d);
            capacity += pushCapacity(c);
            lift += liftCapacity(c);
            ground += c->position.y;
            n++;
        }
        /* Not enough hands left to carry it: it drops */
        if (o->lifted && (n == 0 || lift < o->weight * (o->end_up ? END_LIFT_SHARE : 1.0f))) o->lifted = o->end_up = false;
        if (o->end_up && (n != 1 || lift >= o->weight)) o->end_up = false; /* (more hands: carried all of it) */
        if (n == 0) continue;

        /* Lifted: it goes where the team goes, carried at hip height.
           On the ground: it follows the team's average wish as far as their
           strength allows; whatever it doesn't follow holds each of them back. */
        float share = o->end_up ? END_DRAG : o->lifted ? 1.0f : teamShare(capacity, Object_dragWeight(o));
        Vec3 move = vec3_scale(sum, share / (float)n);
        if (!o->lifted && !o->end_up && share > 0.0f) { /* somebody in the way: they lean against it (and get told) */
            float in_way = inTheWay(o, team, n, objects, count, move);
            if (in_way > 0.0f) {
                share = teamShare(capacity, Object_dragWeight(o) + BYSTANDER_HOLD * in_way);
                move = vec3_scale(sum, share / (float)n);
            }
        }
        if (o->end_up) {
            move.y = 0.0f;
        } else if (o->lifted) {
            float goal = ground / (float)n + LIFT_HEIGHT;
            if (n >= 2 && (o->type == OBJ_LOG || o->type == OBJ_TREE)) goal = levelLift(o, team, n, dt);
            move.y = clampf(goal - bottomOf(o), -LIFT_SPEED * dt, LIFT_SPEED * dt);
        }
        o->position = vec3_add(o->position, move);
        if (o->end_up) endLift(o, team[0], dt);
        o->velocity.x = o->velocity.z = 0.0f;
        if (o->lifted) o->velocity.y = 0.0f;
        o->angular_velocity.y = 0.0f;
        for (int k = 0; k < n; k++) {
            Vec3 held_back = vec3_sub(want[k], vec3(move.x, 0.0f, move.z));
            team[k]->position = vec3_sub(team[k]->position, held_back);
            team[k]->hands.grab_share = share;
        }
    }
}

void Hands_update(Object *who, float dt) {
    Hands *h = &who->hands;
    Body *b = &who->body;
    const float s = scaleOf(who);
    const Vec3 facing = Object_facing(who);
    const Vec3 right = rightOf(facing);

    b->crouch_goal = 0.0f;
    b->lean_goal = 0.0f;
    b->chin_goal = 0.0f;
    b->hand_active[0] = b->hand_active[1] = false;
    advanceDrink(who, dt);
    bool missed = false;
    bool can = (h->item[0] && h->item[0]->type == OBJ_CAN) || (h->item[1] && h->item[1]->type == OBJ_CAN);
    float pour_goal = who->spraying && can ? 1.0f : 0.0f; /* lifting the can takes a moment */
    who->pour += (pour_goal - who->pour) * fminf(1.0f, 5.0f * dt);

    if (h->winding) h->wind = fminf(h->wind + dt, FULL_WIND); /* (held on: harder) */
    h->winding = false;
    if (throwHand(who) < 0) h->wind = 0.0f;

    for (int i = 0; i < 2; i++) {
        float sign = i == HAND_LEFT ? -1.0f : 1.0f;
        Object *item = h->item[i];
        h->settle[i] = fminf(1.0f, h->settle[i] + dt / SETTLE_TIME);

        switch (h->state[i]) {
        case HAND_FREE:
            if (h->throw_time[i] > 0.0f) {
                h->throw_time[i] = fmaxf(0.0f, h->throw_time[i] - dt);
                float u = 1.0f - h->throw_time[i] / THROW_TIME, k = sinf(PI_F * u);
                Vec3 local;
                if (h->overhand[i]) { /* over the shoulder, forward and down across the body */
                    float e = smoothstepf(0.0f, 0.7f, u);
                    local = vec3_lerp(vec3(sign * 0.2f * s, 1.6f * s, -0.1f * s), vec3(-sign * 0.08f * s, 1.05f * s, 0.55f * s), e);
                    b->lean_goal = fmaxf(b->lean_goal, 0.35f * k);
                } else {              /* underhand: arm stretched forward and up */
                    local = vec3(sign * 0.12f * s, (1.25f + 0.2f * k) * s, 0.62f * s);
                }
                b->hand_active[i] = h->overhand[i] ? u < 0.85f : k > 0.05f;
                b->hand_target[i] = Figure_toWorld(who->position, facing, local);
            }
            break;

        case HAND_REACHING: {
            if (!item || item->is_held || item->broken) { /* someone else got it first (or it's gone) */
                for (int k = 0; k < 2; k++)
                    if (h->item[k] == item) { h->state[k] = HAND_FREE; h->item[k] = NULL; }
                break;
            }
            Vec3 c = itemCenter(item);
            Vec3 target = twoHanded(h)
                ? vec3_add(c, vec3_scale(right, sign * (extentAlong(item, right) + 0.01f)))
                : vec3_add(c, vec3(0.0f, extentAlong(item, UP), 0.0f));
            if (!twoHanded(h) && (item->type == OBJ_LADDER || target.y > who->position.y + 1.5f * s)) /* (long, or up high: where it's in reach) */
                target = closestPoint(item, vec3(target.x, who->position.y + 1.0f * s, target.z));
            b->hand_active[i] = true;
            b->hand_target[i] = target;
            b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, target.y));
            b->lean_goal = 0.25f;
            h->timer[i] += dt;
            if (h->timer[i] >= REACH_TIME && (!twoHanded(h) || i == HAND_RIGHT))
                startHolding(who, i);
            break;
        }

        case HAND_HOLDING: {
            Vec3 local;
            if (twoHanded(h)) {
                /* Item in front of the chest: palms on the back half of its sides,
                   just outside the surface, so the forearms run along the outside */
                local = vec3(sign * (extentAlong(item, right) + CARRY_PALM_GAP), 1.0f * s,
                             CARRY_CHEST_GAP * s + CARRY_GRIP_DEPTH);
            } else if (Object_isBottle(item) && !h->by_neck[i]) {
                local = vec3(sign * 0.22f * s, 0.86f * s, 0.15f * s); /* held up a little, forearm forward */
            } else { /* hanging at the side (a wide one, a full bag say: the arm a little out, held by the neck) */
                float out = fminf(fmaxf(0.25f * s, BODY_HALF_WIDTH * s + extentAlong(item, right) + 0.01f), 0.42f * s);
                local = vec3(sign * out, fmaxf(0.80f * s, 2.0f * extentAlong(item, UP) + 0.14f), 0.08f * s);
            }
            if (h->wind > 0.0f && (i == throwHand(who) || twoHanded(h))) { /* winding up to throw */
                float back = smoothstepf(0.0f, 0.2f, h->wind);
                Vec3 low = twoHanded(h) ? vec3(local.x, 0.7f * s, -0.05f * s) : vec3(sign * 0.24f * s, 0.78f * s, -0.24f * s);
                local = vec3_lerp(local, low, back);
                if (goesOverhand(who, item)) { /* the arm comes back over the shoulder, the other one points ahead */
                    float over = smoothstepf(0.0f, 0.25f, h->wind - OVERHAND_AFTER * (1.2f - 0.5f * who->temperament.energy));
                    local = vec3_lerp(local, vec3(sign * 0.24f * s, 1.62f * s, -0.3f * s), over);
                    b->lean_goal = fminf(b->lean_goal, -0.2f * over);
                    if (h->state[1 - i] == HAND_FREE) {
                        b->hand_active[1 - i] = true;
                        b->hand_target[1 - i] = Figure_toWorld(who->position, facing, vec3(-sign * 0.15f * s, 1.3f * s, 0.45f * s));
                    }
                }
            }
            if (Ladder_isRaised(item)) { /* a slid-out ladder held up: the hand in front, the other higher up the rail */
                local = vec3(sign * 0.1f * s, 0.95f * s, 0.32f * s);
                if (h->state[1 - i] == HAND_FREE && item->raised > 0.0f) {
                    Vec3 rail = vec3_scale(right, -sign * LADDER_WIDTH * 0.5f);
                    b->hand_active[1 - i] = true;
                    b->hand_target[1 - i] = vec3_add(Ladder_raisedGrip(item, 1.0f), vec3_add(rail, vec3_scale(right, -sign * 0.02f)));
                }
            }
            if (item->type == OBJ_UMBRELLA && item->umbrella_open) /* the hand up in front of its shoulder, the forearm up (the shaft leans in over the head) */
                local = vec3(sign * UMBRELLA_HAND_OUT * s, UMBRELLA_HAND_UP * s, UMBRELLA_HAND_AHEAD * s);
            if (item->type == OBJ_CAN) { /* watering: the arm comes up and forward */
                Vec3 lifted = vec3(sign * 0.13f * s, 1.02f * s, 0.40f * s);
                local = vec3_lerp(local, lifted, smoothstepf(0.0f, 1.0f, who->pour));
            }
            b->hand_active[i] = true;
            b->hand_target[i] = Figure_toWorld(who->position, facing, local);
            if (h->drink_hand == i) {
                b->hand_target[i] = drinkTarget(who, b->hand_target[i], h->drink_time);
                float tipped = tiltOf(item, h->drink_time) / DRINK_TILT;
                b->lean_goal = -0.15f * tipped;
                b->chin_goal = 0.45f * tipped;                                /* head back */
            }
            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;
            b->hand_target[i] = target;
            /* The hand couldn't get there last frame: not really holding */
            Vec3 palm = Figure_toWorld(who->position, facing, who->skeleton.joints[i == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R]);
            if (b->hand_weight[i] > 0.95f && vec3_length(vec3_sub(palm, target)) > GRIP_MISS) missed = true;
            b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, target.y) * 0.8f);
            float intent = who->move_local.z;      /* forward = push, backward = pull */
            b->lean_goal = intent > 0.1f ? 0.55f : (intent < -0.1f ? -0.45f : 0.15f);
            break;
        }
        }
    }

    /* Can't reach the grip for a moment: take hold again from where we stand.
       If that's too far, let go (helpers step back, wait and try again). */
    h->miss_time = missed ? h->miss_time + dt : fmaxf(0.0f, h->miss_time - 2.0f * dt);
    if (h->grabbed && h->miss_time > REGRAB_TIME) {
        Object *o = h->grabbed;
        h->miss_time = 0.0f;
        Hands_release(who);
        Hands_grabObject(who, o);
    }

    /* Touching someone (waking them up): a free hand reaches out */
    if (h->touching) {
        int hand = Hands_freeHand(who);
        if (hand >= 0) {
            b->hand_active[hand] = true;
            b->hand_target[hand] = h->touch_point;
            b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, h->touch_point.y));
            b->lean_goal = 0.3f;
        }
        h->touching = false;
    }
    if (h->rope > 0.0f) { /* on a rope: both hands on it, one behind the other; leaning back to pull */
        for (int hand = 0; hand < 2; hand++) {
            if (h->state[hand] != HAND_FREE) continue;
            b->hand_active[hand] = true;
            b->hand_target[hand] = vec3_add(h->rope_at, vec3_scale(h->rope_dir, hand == who->main_hand ? 0.0f : 0.35f));
        }
        b->lean_goal = fminf(b->lean_goal, -0.6f * fminf(h->rope, 1.0f) + 0.1f);
        h->rope = 0.0f;
    }
    if (!h->on_edge) b->reach_turn = 0.0f;
    if (h->on_edge) { /* hanging on, pulling up: both hands on the edge */
        for (int hand = 0; hand < 2; hand++) {
            if (h->state[hand] != HAND_FREE) continue;
            float side = hand == HAND_LEFT ? -1.0f : 1.0f;
            b->hand_active[hand] = true;
            b->hand_target[hand] = vec3_add(h->edge_point, vec3_scale(h->edge_across, side * 0.2f * s));
            if (hand != h->edge_lead && h->edge_trail > 0.0f) /* (one hand reaches up first; the other follows from down by the shoulder) */
                b->hand_target[hand] = vec3_add(b->hand_target[hand], vec3(0.0f, -0.5f * s * h->edge_trail, 0.0f));
        }
        b->reach_turn = h->edge_turn;
        b->crouch_goal = fmaxf(b->crouch_goal, h->edge_crouch);
        h->on_edge = false;
        h->edge_trail = h->edge_turn = 0.0f;
    }
}

/* Just taken: the item turns from the way it lay into the carry pose, around the palm. */
/* The middle of the closed hand (between the wrist and the knuckles): what's held is in it. */
static Vec3 fistOf(const Object *who, int hand) {
    const Vec3 *j = who->skeleton.joints;
    int w = hand == HAND_LEFT ? JOINT_WRIST_L : JOINT_WRIST_R;
    return Figure_toWorld(who->position, Object_facing(who), vec3_lerp(j[w], j[w + 1], FIST_AT));
}

static void settleInHand(const Object *who, int hand, Object *item) {
    const Hands *h = &who->hands;
    if (h->settle[hand] >= 1.0f) return;
    float k = smoothstepf(0.0f, 1.0f, h->settle[hand]);
    const Vec3 facing = Object_facing(who);
    Vec3 palm = fistOf(who, hand);

    Mat4 target = mat4_rotate_yxz(item->rotation);
    Vec3 y = vec3_normalize(vec3_lerp(Figure_toWorld(vec3_zero(), facing, h->settle_y[hand]), mat4_mul_dir(target, UP), k));
    Vec3 z = vec3_lerp(Figure_toWorld(vec3_zero(), facing, h->settle_z[hand]), mat4_mul_dir(target, vec3(0.0f, 0.0f, 1.0f)), k);
    z = vec3_sub(z, vec3_scale(y, vec3_dot(z, y)));
    if (vec3_length(z) < 1e-4f) return;
    z = vec3_normalize(z);

    Vec3 origin = vec3_add(item->position, item->pivot);
    Vec3 local = mat4_mul_dir_transposed(target, vec3_sub(origin, palm)); /* origin as seen from the palm */
    item->rotation = euler_yxz_from_axes(y, z);
    Object_syncColliders(item);
    origin = vec3_add(palm, mat4_mul_dir(mat4_rotate_yxz(item->rotation), local));
    item->position = vec3_sub(origin, item->pivot);
}

/* Carried things never sink into the body: pushed out of a box around the spine
   (forward for things in front, sideways for things at the side). */
static void keepOffBody(const Object *who, Object *item) {
    const float s = scaleOf(who);
    const Vec3 facing = Object_facing(who), right = rightOf(facing);
    const Vec3 *j = who->skeleton.joints;
    Vec3 local = Figure_toLocal(who->position, facing, itemCenter(item));
    float ex = extentAlong(item, right), ey = extentAlong(item, UP), ez = extentAlong(item, facing);
    if (local.y - ey > j[JOINT_NECK].y || local.y + ey < 0.1f * s) return;

    /* The spine at the item's height (the legs below the hips) */
    Vec3 pelvis = j[JOINT_PELVIS], chest = j[JOINT_CHEST];
    float k = clampf((local.y - pelvis.y) / fmaxf(chest.y - pelvis.y, 1e-3f), 0.0f, 1.0f);
    Vec3 spine = vec3_lerp(pelvis, chest, k);
    float dx = local.x - spine.x, dz = local.z - spine.z;
    float push_x = BODY_HALF_WIDTH * s + ex - fabsf(dx);
    float push_z = BODY_CLEARANCE * s + ez - fabsf(dz);
    if (push_x <= 0.0f || push_z <= 0.0f) return;

    Vec3 push = push_z <= push_x ? vec3_scale(facing, dz >= 0.0f ? push_z : -push_z)
                                 : vec3_scale(right, dx >= 0.0f ? push_x : -push_x);
    item->position = vec3_add(item->position, push);
}

#define CAN_TIP 1.2f  /* radians the can tips forward to pour (the wrist turns) */

static void placeCan(const Object *who, int hand, Object *can) {
    const Vec3 facing = Object_facing(who);
    Vec3 palm = Figure_toWorld(who->position, facing, who->skeleton.joints[hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R]);
    can->rotation = vec3(-CAN_TIP * smoothstepf(0.2f, 1.0f, who->pour), who->rotation.y + PI_F, 0.0f);
    Object_syncColliders(can);
    Vec3 grip = mat4_mul_dir(mat4_rotate_yxz(can->rotation), vec3(0.0f, 0.19f, -0.02f)); /* the top of the handle */
    can->position = vec3_sub(vec3_sub(palm, grip), can->pivot);
    can->velocity = vec3_zero();
}

void Hands_placeHeld(Object *who) {
    Hands *h = &who->hands;
    const Vec3 facing = Object_facing(who);

    for (int i = 0; i < 2; i++) {
        if (h->state[i] != HAND_HOLDING) continue;
        if (twoHanded(h) && i == HAND_RIGHT) continue; /* placed with the left hand */
        Object *item = h->item[i];
        if (item->type == OBJ_LADDER) { /* on edge at the side */
            Ladder_placeHeld(who, i, item);
            continue;
        }
        if (item->type == OBJ_CAN) { /* by the handle, spout ahead; tipped forward to pour */
            placeCan(who, i, item);
            continue;
        }

        if (Tools_placeHeld(who, i, item)) {
            if (!twoHanded(h)) settleInHand(who, i, item);
            else keepOffBody(who, item);
            continue;
        }

        item->rotation.y = who->rotation.y + h->item_yaw[i];
        Object_syncColliders(item);

        Vec3 center;
        if (twoHanded(h)) {
            /* Between the palms, pushed forward so the palms grip its back half */
            Vec3 l = Figure_toWorld(who->position, facing, who->skeleton.joints[JOINT_HAND_L]);
            Vec3 r = Figure_toWorld(who->position, facing, who->skeleton.joints[JOINT_HAND_R]);
            float forward = extentAlong(item, facing) - CARRY_GRIP_DEPTH;
            center = vec3_add(vec3_scale(vec3_add(l, r), 0.5f), vec3_scale(facing, forward));
        } else {
            Vec3 hand = fistOf(who, i); /* (in the fist, not at the fingertips) */
            if (h->drink_hand == i) {
                /* Tipped back towards the mouth */
                item->rotation = vec3(-tiltOf(item, h->drink_time), who->rotation.y, 0.0f);
                Object_syncColliders(item);
            } else if (Object_isBottle(item)) {
                item->rotation.x = item->rotation.z = 0.0f;
                Object_syncColliders(item);
            }
            if (item->type == OBJ_UMBRELLA && item->umbrella_open) { /* the handle in the fist, the shaft leaning in so the canopy is over the head (not the shaft in the face) */
                const float s = who->look.height / 1.75f;
                Vec3 over = Figure_toWorld(who->position, facing, vec3(0.0f, UMBRELLA_TOP * s, 0.03f * s));
                const Object *g = who->umbrella_guest;
                if (g && who->umbrella_guest_t > 0.0f && hypotf(g->position.x - who->position.x, g->position.z - who->position.z) < 1.2f) { /* somebody under it too: over the two of them */
                    over.x = 0.5f * (over.x + g->position.x);
                    over.z = 0.5f * (over.z + g->position.z);
                }
                Vec3 up = vec3_normalize(vec3_sub(over, hand));
                Vec3 side = vec3_cross(up, facing);
                Vec3 ahead = vec3_length(side) > 1e-3f ? vec3_normalize(vec3_cross(vec3_normalize(side), up)) : facing;
                item->rotation = euler_yxz_from_axes(up, ahead);
                Object_syncColliders(item);
                center = vec3_add(hand, vec3_scale(up, UMBRELLA_LENGTH * 0.5f - 0.07f));
            } else if (item->type == OBJ_UMBRELLA && h->drink_hand != i) { /* folded: in the fist by the middle, the tip ahead and down, the crook behind */
                Vec3 along = vec3_normalize(vec3_add(vec3_scale(facing, 0.9f), vec3(0.0f, -0.42f, 0.0f)));
                item->rotation = euler_yxz_from_axes(along, vec3_normalize(vec3_cross(vec3_cross(along, facing), along)));
                Object_syncColliders(item);
                center = vec3_add(hand, vec3_scale(along, 0.06f));
            } else if (item->type == OBJ_SCYTHE) { /* carried over the shoulder, the blade up behind; mowing: the blade sweeping the grass in front */
                Vec3 along, right = vec3_normalize(vec3_cross(facing, UP));
                const float s = who->look.height / 1.75f;
                if (who->mowing > 0.0f) {
                    Vec3 bottom = vec3_add(who->position, vec3_add(vec3_scale(facing, 1.05f * s), vec3_scale(right, 0.7f * s * sinf(who->mowing))));
                    bottom.y += 0.06f;
                    along = vec3_normalize(vec3_sub(hand, bottom));
                    center = vec3_add(bottom, vec3_scale(along, 0.8f));
                } else {
                    along = vec3_normalize(vec3_add(vec3_scale(facing, 0.75f), vec3(0.0f, -0.55f, 0.0f)));
                    center = vec3_sub(hand, vec3_scale(along, 0.15f));
                }
                Vec3 side = vec3_cross(along, facing);
                Vec3 ahead = vec3_length(side) > 1e-3f ? vec3_normalize(vec3_cross(vec3_normalize(side), along)) : facing;
                item->rotation = euler_yxz_from_axes(along, ahead);
                Object_syncColliders(item);
            } else if (Object_isBottle(item)) {
                /* The hand around the bottle where it grips it */
                center = vec3_sub(hand, vec3_scale(item->colliders[0].axes[1], bottleGrip(who, i, item)));
            } else {
                float hang = h->drink_hand == i ? 1.0f - drinkGrip(h->drink_time) : 1.0f;
                center = vec3_sub(hand, vec3(0.0f, (extentAlong(item, UP) + 0.01f) * hang, 0.0f)); /* hangs from the hand */
            }
        }
        item->position = vec3_sub(center, item->colliders[0].offset);
        item->velocity = vec3_zero();
        if (!twoHanded(h)) settleInHand(who, i, item);
        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;
}