Nimbin[12]?Games / Entropie / entropie/src/world/object.c

Entropie git · main

Entropie festival game; first steps

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

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/src/world/object.c 80.8 KB · 1736 lines raw
#include "world/object.h"
#include "world/stage.h"
#include "world/car.h"

#include <stdio.h>
#include <stdlib.h>
#include <SDL3/SDL.h>

#define DEFAULT_STRENGTH   100.0f
#define CYLINDER_SEGMENTS  30
#define ROOF_PITCH         (5.0f / 12.0f) /* roof height per building width */
#define ROOF_THICKNESS     0.4f           /* roof collider slab, below the surface */
#define HALL_PITCH         0.14f          /* farm halls have shallow roofs */
#define BEER_PER_SIP       0.07f
#define VODKA_SIPS         8
#define VODKA_PER_SIP      0.16f
#define BOTTLE_FALL_TIME   0.35f
#define TREE_FALL_TIME     2.4f
#define TRUNK_RADIUS       0.2f
#define BIG_TRUNK          0.5f    /* the big old tree (variant 2): a metre through */
#define WOOD_DENSITY       500.0f   /* kg per cubic meter */
#define WOODPILE_BASE      0.08f    /* height of the sleepers */
#define SPRINT_SPEED       6.6f     /* an average person flat out (m/s) */
#define JUMP_SPEED         3.0f     /* about 0.45 m off the ground */

static void addCollider(Object *obj, Vec3 center, Vec3 size, Vec3 tilt) {
    if (obj->collider_count >= OBJECT_MAX_COLLIDERS) return;
    obj->colliders[obj->collider_count++] = (Collider){
        .center       = center,
        .half_extents = vec3_scale(size, 0.5f),
        .tilt         = tilt,
    };
}

/* Walls block plus one thin slab lying under each roof slope. */
static void setupBuilding(Object *obj, BuildingSize b) {
    addCollider(obj, vec3(0.0f, b.wall_height * 0.5f, 0.0f),
                vec3(b.length, b.wall_height, b.width), vec3_zero());

    const float half_w = b.width * 0.5f;
    const float slope  = sqrtf(half_w * half_w + b.roof_height * b.roof_height);
    const float angle  = atanf(b.roof_height / half_w);
    const Vec3  size   = vec3(b.length, ROOF_THICKNESS, slope);

    for (int side = -1; side <= 1; side += 2) {
        /* side = -1: back slope (-Z), side = +1: front slope (+Z) */
        Vec3 normal  = vec3(0.0f, cosf(angle), (float)side * sinf(angle));
        Vec3 surface = vec3(0.0f, b.wall_height + b.roof_height * 0.5f, (float)side * half_w * 0.5f);
        Vec3 center  = vec3_sub(surface, vec3_scale(normal, ROOF_THICKNESS * 0.5f));
        addCollider(obj, center, size, vec3((float)-side * angle, 0.0f, 0.0f));
    }
}

/* Trunk plus a box around the canopy. */
#define TREE_LAYERS 8

static TreeSize treeSize(const Object *obj) {
    return (TreeSize){ .trunk_height = obj->size.y, .trunk_radius = obj->variant == 2 ? BIG_TRUNK : obj->variant ? TRUNK_RADIUS * 0.8f : TRUNK_RADIUS,
                       .leaf_layers = obj->branches > 0 ? obj->branches : 1, .leaf_radius = obj->size.x * 0.5f,
                       .fruit = obj->variant == 1, .cut = obj->trunk_cut, .old = obj->variant == 2,
                       .seed = Shapes_treeSeed(obj->position.x, obj->position.z) };
}

static void setupTree(Object *obj, TreeSize t) {
    const float canopy_h = Shapes_treeHeight(t) - t.trunk_height;
    const float canopy_w = t.leaf_radius * 1.2f;
    addCollider(obj, vec3(0.0f, t.trunk_height * 0.5f, 0.0f),
                vec3(t.trunk_radius * 2.0f, t.trunk_height, t.trunk_radius * 2.0f), vec3_zero());
    addCollider(obj, vec3(0.0f, t.trunk_height + canopy_h * 0.5f, 0.0f),
                vec3(canopy_w, canopy_h, canopy_w), vec3_zero());
}

/* Firewood: a half or a quarter of a log, centered on its box. */
static void setupFirewood(Object *obj, float diameter, float length, int parts) {
    const float r = diameter * 0.5f;
    obj->mesh   = Shapes_firewood(diameter, length, parts);
    obj->pivot  = vec3(0.0f, length * 0.5f, 0.0f);
    obj->size   = vec3(diameter, length, (float)parts);
    obj->weight = WOOD_DENSITY * PI_F * r * r * length / (float)parts;
    obj->collider_count = 0;
    addCollider(obj, vec3_zero(), vec3(parts == 2 ? diameter : r, length, r), vec3_zero());
}

static float flatGround(const void *ctx, float x, float z) {
    (void)x;
    (void)z;
    return *(const float *)ctx;
}

static Mesh decalMesh(ObjectType type, Palette palette, Vec3 position, Vec3 size, ShapeGround ground) {
    unsigned seed = (unsigned)(int)(position.x * 811.0f) ^ ((unsigned)(int)(position.z * 677.0f) << 10);
    (void)palette;
    return type == OBJ_STINK ? Shapes_stink(size.x, seed, ground) : Shapes_vomit(size.x, seed, ground);
}

Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size, float weight) {
    Object *obj = calloc(1, sizeof(*obj));
    if (!obj) return NULL;

    obj->type     = type;
    obj->palette  = palette;
    obj->make_arg = weight;
    obj->hands.drink_hand = -1;
    obj->cast_id = -1;
    obj->work_state.since = 1e3f; /* hasn't worked yet */
    obj->main_hand = HAND_RIGHT;
    obj->top_speed = SPRINT_SPEED;
    obj->fitness = 0.5f;
    obj->wits    = 0.5f;
    obj->height_fear = 0.3f;
    obj->jump_speed = JUMP_SPEED;
    obj->tap_timer = 1e3f;
    obj->position = position;
    obj->weight   = weight;
    obj->strength = DEFAULT_STRENGTH;
    obj->tolerance = 1.0f;
    obj->size     = size;
    obj->needs    = (Needs){ .stamina = 1.0f, .hunger = 0.25f, .thirst = 0.25f, .bladder = 0.1f, .sleepiness = 0.2f };

    /* Centered meshes: pivot in the middle of the bounding box. */
    const Vec3 centered = vec3(0.0f, size.y * 0.5f, 0.0f);

    switch (type) {
    case OBJ_BOX:
    case OBJ_BOX_STATIC:
        obj->mesh      = Shapes_box(size, palette);
        obj->pivot     = centered;
        obj->is_static = (type == OBJ_BOX_STATIC);
        addCollider(obj, vec3_zero(), size, vec3_zero());
        break;

    case OBJ_CYLINDER:
    case OBJ_CYLINDER_STATIC:
        obj->mesh      = Shapes_cylinder(size.x, size.y, CYLINDER_SEGMENTS, palette);
        obj->pivot     = centered;
        obj->is_static = (type == OBJ_CYLINDER_STATIC);
        addCollider(obj, vec3_zero(), size, vec3_zero());
        break;

    case OBJ_STUMP: {
        obj->mesh      = Shapes_stump(size.x, size.y, Shapes_treeSeed(position.x, position.z)); /* (the tree's own foot) */
        obj->pivot     = centered;
        obj->is_static = true;
        addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
        break;
    }

    case OBJ_HUMAN:
    case OBJ_PLAYER:
        obj->pivot        = centered;
        obj->is_character = true;
        obj->step_height  = size.y / 3.0f;
        obj->look         = Figure_wardrobe(0);
        obj->look.height  = size.y;
        addCollider(obj, vec3_zero(), size, vec3_zero());
        break;

    case OBJ_BOTTLE:
    case OBJ_VODKA:
    case OBJ_WATER:
        obj->mesh    = Shapes_bottle(size.x, size.y, type == OBJ_VODKA ? BOTTLE_SPIRITS
                                                     : type == OBJ_WATER ? BOTTLE_WATER : BOTTLE_BEER);
        obj->pivot   = centered;
        obj->sips    = type == OBJ_VODKA ? VODKA_SIPS : BOTTLE_SIPS;
        obj->alcohol = type == OBJ_VODKA ? VODKA_PER_SIP : (type == OBJ_BOTTLE ? BEER_PER_SIP : 0.0f);
        obj->quench  = type == OBJ_WATER ? 0.2f : (type == OBJ_BOTTLE ? 0.12f : 0.02f);
        obj->refresh = type == OBJ_WATER ? 0.15f : (type == OBJ_BOTTLE ? 0.08f : 0.03f);
        addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
        break;

    case OBJ_TOY: /* (weight picks which: see object.h) */
        obj->variant = (int)weight;
        obj->weight  = obj->variant == 0 ? 0.15f : 0.3f;
        obj->mesh    = Shapes_toy(obj->variant, size);
        obj->pivot   = centered;
        addCollider(obj, vec3_zero(), obj->variant == 0 ? vec3(size.x, size.x, size.x) : vec3(size.x, size.y, size.y), vec3_zero());
        break;

    case OBJ_APPLE:
        obj->mesh    = Shapes_apple(size.x);
        obj->pivot   = vec3(0.0f, size.x * 0.5f, 0.0f);
        obj->sips    = 3;
        obj->nourish = 0.05f;
        obj->quench  = 0.02f;
        obj->refresh = 0.03f;
        addCollider(obj, vec3_zero(), vec3(size.x, size.x, size.x), vec3_zero());
        break;

    case OBJ_BREAD:
        obj->mesh    = Shapes_bread(size);
        obj->pivot   = centered;
        obj->sips    = 5;
        obj->nourish = 0.09f;
        obj->refresh = 0.04f;
        addCollider(obj, vec3_zero(), size, vec3_zero());
        break;

    case OBJ_SUPPLY: {
        bool round = palette == PALETTE_MODERN;
        obj->mesh      = palette == PALETTE_WARM ? Shapes_appleCrate(size)
                       : palette == PALETTE_STANDARD ? Shapes_coolBox(size)
                       : palette == PALETTE_GROUND ? Shapes_bagCarton(size) : Shapes_barrel(size.x, size.y);
        obj->pivot     = centered;
        obj->is_static = weight >= 0.0f; /* (weight < 0: a bought one, -weight kg: carried in from the car) */
        if (weight < 0.0f) obj->weight = -weight;
        obj->dispenses = palette == PALETTE_WARM ? OBJ_APPLE : palette == PALETTE_STANDARD ? OBJ_SAUSAGE
                       : palette == PALETTE_GROUND ? OBJ_BAG : OBJ_WATER;
        addCollider(obj, vec3_zero(), round ? vec3(size.x, size.y, size.x) : size, vec3_zero());
        break;
    }

    case OBJ_SAUSAGE:
        obj->mesh    = Shapes_sausage(size.y, false);
        obj->pivot   = centered;
        obj->sips    = 3;
        obj->nourish = 0.02f; /* raw: hardly worth it */
        obj->refresh = 0.01f;
        addCollider(obj, vec3_zero(), vec3(0.04f, size.y, 0.04f), vec3_zero());
        break;

    case OBJ_FIREPLACE:
        obj->mesh      = Shapes_fireplace(size.x);
        obj->flames    = Shapes_flames();
        obj->grill_mesh = Shapes_grill();
        obj->is_static = true;
        obj->pivot     = vec3_zero();
        /* Low enough to step over; wood put in rests on the ashes. Inside the
           ring of stones (its corners at the stones: a square as wide as the
           ring stuck out past them, and people beside the fire stepped on and
           off its corners, jittering) */
        addCollider(obj, vec3(0.0f, 0.03f, 0.0f), vec3(size.x * 0.6f, 0.06f, size.x * 0.6f), vec3_zero());
        break;

    case OBJ_TRASH:
        obj->variant = (int)palette;
        obj->size    = size.x < 0.0f ? vec3(0.03f, 0.012f, 0.012f) : Shapes_trashSize(palette); /* (a cigarette butt) */
        obj->mesh    = size.x < 0.0f ? Shapes_butt() : Shapes_trash(palette, (unsigned)(position.x * 131.0f + position.z * 71.0f));
        obj->pivot   = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;

    case OBJ_BAG:
        obj->size  = Shapes_bagSize(0.0f);
        obj->mesh  = Shapes_bag(0.0f);
        obj->pivot = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;

    case OBJ_BUSH:
        obj->mesh      = Shapes_bush(size.x, size.y, (unsigned)(position.x * 37.0f) ^ (unsigned)(position.z * 91.0f));
        obj->is_static = true;
        addCollider(obj, vec3(0.0f, size.y * 0.45f, 0.0f), vec3(size.x * 0.8f, size.y * 0.9f, size.x * 0.8f), vec3_zero());
        break;

    case OBJ_FURNITURE:
        obj->variant   = (int)weight; /* the kind, see Object_createFurniture */
        obj->shown     = -1;
        obj->weight    = 100.0f;
        obj->mesh      = Shapes_furniture((FurnitureKind)obj->variant, size);
        obj->is_static = true;
        addCollider(obj, vec3(0.0f, (obj->variant == FURN_CHAIR || obj->variant == FURN_BENCH ? SEAT_HEIGHT : size.y) * 0.5f, 0.0f),
                    vec3(size.x, obj->variant == FURN_CHAIR || obj->variant == FURN_BENCH ? SEAT_HEIGHT : size.y, size.z), vec3_zero());
        break;

    case OBJ_HOSE:
        obj->mesh  = Shapes_hoseNozzle();
        obj->pivot = vec3(0.0f, 0.1f, 0.0f);
        addCollider(obj, vec3_zero(), vec3(0.05f, 0.22f, 0.05f), vec3_zero());
        break;

    case OBJ_CAN:
        obj->mesh  = Shapes_wateringCan();
        obj->pivot = vec3(0.0f, 0.14f, 0.0f);
        addCollider(obj, vec3(0.0f, 0.0f, 0.05f), vec3(0.26f, 0.28f, 0.4f), vec3_zero());
        break;

    case OBJ_STONE:
        obj->mesh  = Shapes_stone(size, (unsigned)(position.x * 977.0f) ^ ((unsigned)(position.z * 311.0f) << 5));
        obj->pivot = vec3(0.0f, size.y * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), vec3_scale(size, 0.85f), vec3_zero());
        break;

    case OBJ_LADDER: {
        float len = Shapes_ladderLength(0.0f);
        obj->mesh  = Shapes_ladder(0.0f);
        obj->pivot = vec3_zero();
        addCollider(obj, vec3(0.0f, len * 0.5f, 0.0f), vec3(LADDER_WIDTH + 0.06f, len, 0.08f), vec3_zero());
        break;
    }

    case OBJ_DOG:
        obj->mesh = Shapes_dogBody();
        obj->look.height = 0.4f;
        addCollider(obj, vec3(0.0f, 0.18f, -0.05f), vec3(0.2f, 0.34f, 0.7f), vec3_zero());
        break;

    case OBJ_BIN:
        obj->mesh      = Shapes_bin(size);
        obj->pivot     = centered;
        obj->is_static = true;
        addCollider(obj, vec3_zero(), size, vec3_zero());
        break;

    case OBJ_TAPE:
        obj->mesh  = Shapes_tapeRoll();
        obj->pivot = vec3(0.0f, 0.025f, 0.0f);
        addCollider(obj, vec3_zero(), vec3(0.1f, 0.05f, 0.1f), vec3_zero());
        break;

    case OBJ_URINELLA: /* (never made: they're only counted, in pockets) */
        Object_destroy(obj);
        return NULL;

    case OBJ_SHEARS:
        obj->mesh  = Shapes_shears(size.y);
        obj->pivot = centered;
        addCollider(obj, vec3_zero(), vec3(0.06f, size.y, 0.03f), vec3_zero());
        break;

    case OBJ_CHAINSAW:
        obj->mesh  = Shapes_chainsaw();
        obj->pivot = vec3(0.0f, 0.09f, 0.0f); /* (the motor's underside on the ground) */
        addCollider(obj, vec3(0.0f, 0.03f, 0.02f), vec3(0.18f, 0.24f, 0.74f), vec3_zero());
        break;

    case OBJ_LOG:
        obj->mesh   = Shapes_log(size.x, size.y);
        obj->pivot  = centered;
        obj->weight = weight > 0.0f ? weight : WOOD_DENSITY * PI_F * size.x * size.x * 0.25f * size.y;
        addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
        break;

    case OBJ_FIREWOOD:
        setupFirewood(obj, size.x, size.y, size.z >= 4.0f ? 4 : 2);
        break;

    case OBJ_WOODPILE:
        obj->mesh      = Shapes_woodpile(size.x, size.z);
        obj->pivot     = vec3(0.0f, WOODPILE_BASE * 0.5f, 0.0f);
        obj->is_static = true;
        addCollider(obj, vec3_zero(), vec3(size.x, WOODPILE_BASE, size.z), vec3_zero());
        break;

    case OBJ_BRANCH:
        obj->mesh  = Shapes_branch(size.y);
        obj->pivot = centered;
        addCollider(obj, vec3_zero(), vec3(0.2f, size.y, 0.2f), vec3_zero());
        break;

    case OBJ_PUDDLE:
    case OBJ_STINK: {
        ShapeGround flat = { .height = flatGround, .ctx = &position.y, .origin = position };
        obj->mesh = decalMesh(type, palette, position, size, flat);
        obj->is_static = true;
        obj->work = 1.0f;
        break;
    }

    case OBJ_CAR: { /* (see car.c Car_create) */
        const float back = -size.z * 0.5f;
        obj->mesh      = Shapes_car(CAR_COLOR, false);
        obj->inside    = Shapes_carInside(CAR_COLOR);
        obj->is_static = true;
        obj->weight    = 1300.0f;
        addCollider(obj, vec3(0.0f, 0.62f, 0.0f), vec3(size.x, 0.74f, size.z), vec3_zero());
        addCollider(obj, vec3(0.0f, 1.27f, (back + 0.05f + 0.5f) * 0.5f), vec3(size.x * 0.9f, 0.6f, 0.5f - back - 0.05f), vec3_zero());
        break;
    }

    case OBJ_CRATE:
        obj->variant = (int)(weight + 0.5f) < 0 ? 0 : (int)(weight + 0.5f) > 3 ? 3 : (int)(weight + 0.5f);
        obj->size    = vec3(CRATE_SIZE_X, CRATE_SIZE_Y, CRATE_SIZE_Z);
        Crate_fill(obj); /* (full of its own bottles: stored, weight) */
        obj->mesh    = Shapes_crate(obj->variant, obj->crate_slot, Crate_bottles(obj->variant));
        obj->shown   = Crate_look(obj);
        obj->pivot   = vec3(0.0f, CRATE_SIZE_Y * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;

    case OBJ_GEAR: { /* kind = weight; its colour: see car.c */
        static const Vec3  SIZE[] = { { 0.42f, 0.26f, 0.26f }, { 0.6f, 0.18f, 0.18f }, { 0.5f, 0.16f, 0.16f }, { 0.6f, 0.2f, 0.2f } };
        static const float KG[]   = { 1.6f, 0.6f, 2.4f, 4.5f };
        obj->variant = (int)weight & 3;
        obj->size    = SIZE[obj->variant];
        obj->weight  = KG[obj->variant];
        obj->mesh    = Shapes_gear(obj->variant, vec3(0.5f, 0.5f, 0.5f));
        obj->pivot   = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;
    }

    case OBJ_UMBRELLA: {
        static const Vec3 COLORS[] = { { 0.15f, 0.20f, 0.42f }, { 0.70f, 0.14f, 0.16f }, { 0.20f, 0.32f, 0.22f } };
        obj->size    = vec3(0.08f, UMBRELLA_LENGTH, 0.08f);
        obj->weight  = 0.5f;
        obj->variant = (int)weight % 3;
        obj->mesh    = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, UMBRELLA_FOLDED, COLORS[obj->variant]);
        obj->pivot   = vec3(0.0f, UMBRELLA_LENGTH * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;
    }

    case OBJ_STAGE: /* (world/stage.c: its look by how far it's built; the deck solid once it's in) */
        obj->variant   = (int)weight;
        obj->weight    = 500.0f;
        obj->is_static = true;
        obj->size      = size;
        obj->mesh      = Shapes_stage(0, 0.0f, size.x, size.z, vec3(0.5f, 0.5f, 0.5f));
        addCollider(obj, vec3(0.0f, STAGE_DECK * 0.5f, 0.0f), vec3(size.x, STAGE_DECK, size.z), vec3_zero());
        obj->collider_count = 0;
        break;

    case OBJ_MATERIAL: { /* kind = weight */
        static const Vec3  SIZE[MATERIAL_KINDS] = { { 0.6f, 0.18f, 0.24f }, /* (deck boards cut to length: short enough to carry past things) */ { 0.16f, 0.09f, 0.11f }, { 0.16f, 0.09f, 0.11f }, { 0.27f, 0.3f, 0.27f } };
        static const float KG[MATERIAL_KINDS]   = { 17.0f, 1.0f, 1.0f, 6.0f };
        obj->variant = (int)weight < 0 ? 0 : (int)weight >= MATERIAL_KINDS ? MATERIAL_KINDS - 1 : (int)weight;
        obj->size    = SIZE[obj->variant];
        obj->weight  = KG[obj->variant];
        obj->mesh    = Shapes_material(obj->variant, obj->size);
        obj->pivot   = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;
    }

    case OBJ_SCYTHE:
        obj->size    = vec3(0.06f, 1.6f, 0.06f);
        obj->weight  = 2.5f;
        obj->mesh    = Shapes_scythe(1.6f);
        obj->pivot   = vec3(0.0f, 0.8f, 0.0f);
        addCollider(obj, vec3_zero(), obj->size, vec3_zero());
        break;

    case OBJ_SNAIL:
        obj->variant = (int)weight ? 1 : 0;
        obj->size    = vec3(0.05f, 0.045f, 0.08f);
        obj->weight  = 0.02f;
        obj->mesh    = Shapes_snail();
        obj->pivot   = vec3_zero();
        addCollider(obj, vec3(0.0f, 0.022f, 0.0f), obj->size, vec3_zero());
        break;

    case OBJ_ROPE:
        obj->size    = vec3(0.32f, 0.1f, 0.32f);
        obj->weight  = 0.6f;
        obj->mesh    = Shapes_throwBag();
        obj->pivot   = vec3(0.0f, 0.05f, 0.0f);
        addCollider(obj, vec3(0.0f, 0.05f, 0.0f), vec3(0.12f, 0.1f, 0.12f), vec3_zero());
        snprintf(obj->name, sizeof obj->name, "Throw line");
        break;

    case OBJ_SCULPTURE:
        obj->variant   = (int)weight % 3;
        obj->size      = size;
        obj->weight    = 200.0f;
        obj->is_static = true;
        obj->mesh      = Shapes_sculpture(obj->variant, size.x, size.y);
        obj->pivot     = vec3_zero();
        addCollider(obj, vec3(0.0f, size.y * 0.5f, 0.0f), vec3(size.x, size.y, size.x), vec3_zero());
        break;

    case OBJ_HAT: /* (grey here: its colour once it's known - world.c, showProps) */
        obj->size    = vec3(0.21f, 0.1f, 0.24f);
        obj->weight  = 0.1f;
        obj->variant = (int)weight == 2 ? 2 : 1;
        obj->mesh    = Shapes_hat(obj->variant, vec3(0.5f, 0.5f, 0.5f));
        addCollider(obj, vec3(0.0f, 0.0f, 0.02f), obj->size, vec3_zero());
        break;

    case OBJ_TENT: { /* (see tent.c: its mesh by how far it's up) */
        obj->variant   = weight > 0.5f ? 1 : 0;
        obj->weight    = 50.0f;
        obj->is_static = true;
        obj->shown     = -1;
        obj->size      = vec3(TENT_W(obj->variant), TENT_H(obj->variant), TENT_L(obj->variant));
        obj->mesh      = Shapes_tent(obj->variant, 0, 0.0f, 0, vec3(0.9f, 0.86f, 0.62f), vec3(0.3f, 0.5f, 0.3f));
        addCollider(obj, vec3(0.0f, 0.02f, 0.0f), vec3(obj->size.x, 0.04f, obj->size.z), vec3_zero());
        break;
    }

    case OBJ_CLOTHES: /* (made in clothes.c, its mesh given) */
        obj->is_static = true;
        break;

    case OBJ_WALL: /* nothing to see: the building's OBJ_STRUCTURE shows it */
        obj->pivot     = vec3(0.0f, size.y * 0.5f, 0.0f);
        obj->is_static = true;
        obj->weight    = 1000.0f;
        addCollider(obj, vec3_zero(), size, vec3_zero());
        break;

    case OBJ_STRUCTURE:
    case OBJ_DECAL: /* made with its mesh by Object_createDecal */
        obj->is_static = true;
        break;

    case OBJ_AXE:
        obj->mesh  = Shapes_axe(size.y);
        obj->pivot = centered;
        addCollider(obj, vec3(0.0f, 0.0f, 0.03f), vec3(0.04f, size.y, 0.16f), vec3_zero());
        break;

    case OBJ_TREE: {
        obj->variant   = palette == PALETTE_STANDARD; /* a fruit tree */
        obj->branches  = TREE_LAYERS;
        TreeSize t = treeSize(obj);
        obj->mesh      = Shapes_tree(t);
        obj->is_static = true;
        setupTree(obj, t);
        break;
    }

    case OBJ_HOUSE:
    case OBJ_BARN: {
        BuildingSize b = { .length = size.x, .width = size.z, .wall_height = size.y,
                           .look = type == OBJ_HOUSE ? (palette == PALETTE_STANDARD ? BUILDING_PLASTER
                                                        : palette == PALETTE_MODERN ? BUILDING_PLASTER_DARK : BUILDING_BRICK)
                                 : palette == PALETTE_MODERN ? BUILDING_HALL_SOLAR
                                 : palette == PALETTE_STANDARD ? BUILDING_HALL_RED
                                 : palette == PALETTE_GROUND ? BUILDING_HALL : BUILDING_BRICK,
                           .roof_height = size.z * (type == OBJ_HOUSE || palette == PALETTE_WARM ? ROOF_PITCH : HALL_PITCH) };
        obj->mesh      = Shapes_building(b, type == OBJ_HOUSE);
        obj->variant   = (int)b.look;
        obj->is_static = true;
        setupBuilding(obj, b);
        break;
    }
    }

    if (!obj->is_character && !obj->mesh.vao && type != OBJ_WALL) {
        SDL_Log("Object_create: mesh creation failed (type %d)", (int)type);
        Object_destroy(obj);
        return NULL;
    }

    Object_syncColliders(obj);
    return obj;
}

Object *Object_createOnGround(ObjectType type, Palette palette, Vec3 position, Vec3 size, ShapeGround ground) {
    Object *obj = Object_create(type, palette, position, size, 0.0f);
    if (!obj) return NULL;
    ground.origin = position;
    Mesh mesh = decalMesh(type, palette, position, size, ground);
    if (mesh.vao) {
        Mesh_destroy(&obj->mesh);
        obj->mesh = mesh;
    }
    return obj;
}

Object *Object_createStructure(Mesh mesh, Vec3 position, float yaw, Vec3 size) {
    Object *obj = Object_createDecal(mesh, position);
    if (!obj) return NULL;
    obj->type = OBJ_STRUCTURE;
    obj->size = size; /* (big: always drawn) */
    obj->rotation = vec3(0.0f, yaw, 0.0f);
    return obj;
}

Object *Object_createDecal(Mesh mesh, Vec3 position) {
    Object *obj = calloc(1, sizeof(*obj));
    if (!obj) {
        Mesh_destroy(&mesh);
        return NULL;
    }
    obj->type      = OBJ_DECAL;
    obj->position  = position;
    obj->mesh      = mesh;
    obj->is_static = true;
    obj->hands.drink_hand = -1;
    obj->cast_id   = -1;
    return obj;
}

#define STINK_RADIUS 2.2f /* how far a fresh puddle can be smelled */

static Object       **g_kinds[KIND_COUNT];
static int            g_kind_n[KIND_COUNT], g_kind_cap[KIND_COUNT];
static Object *const *g_kind_src;
static int            g_kind_count = -1;

void Object_indexKinds(Object *const *objects, int count) {
    g_kind_src = NULL;
    for (int k = 0; k < KIND_COUNT; k++) {
        g_kind_n[k] = 0;
        if (g_kind_cap[k] < count) {
            Object **grown = realloc(g_kinds[k], (size_t)(count + 64) * sizeof *grown);
            if (!grown) return;
            g_kinds[k] = grown;
            g_kind_cap[k] = count + 64;
        }
    }
    for (int i = 0; i < count; i++) {
        Object *o = objects[i];
        int k = o->is_character ? KIND_CHARACTERS : o->type == OBJ_FIREPLACE ? KIND_FIREPLACES : o->type == OBJ_STINK ? KIND_STINKS : -1;
        if (k >= 0) g_kinds[k][g_kind_n[k]++] = o;
    }
    g_kind_src = objects;
    g_kind_count = count;
}

Object *const *Object_kind(int kind, Object *const *objects, int count, int *n) {
    if (objects && objects == g_kind_src && count == g_kind_count && kind >= 0 && kind < KIND_COUNT) {
        *n = g_kind_n[kind];
        return g_kinds[kind];
    }
    *n = count;
    return objects;
}

float Object_stinkAt(Object *const *objects, int count, Vec3 pos) {
    float worst = 0.0f;
    objects = Object_kind(KIND_STINKS, objects, count, &count);
    for (int i = 0; i < count; i++) {
        const Object *o = objects[i];
        if (o->type != OBJ_STINK || o->broken) continue;
        float dx = o->position.x - pos.x, dz = o->position.z - pos.z;
        float near = 1.0f - sqrtf(dx * dx + dz * dz) / STINK_RADIUS;
        if (near > 0.0f) worst = fmaxf(worst, near * o->work);
    }
    return worst;
}

void Object_destroy(Object *obj) {
    if (!obj) return;
    Mesh_destroy(&obj->mesh);
    Mesh_destroy(&obj->flames);
    Mesh_destroy(&obj->grill_mesh);
    Mesh_destroy(&obj->inside);
    free(obj->load);
    free(obj);
}

void Object_setRotation(Object *obj, Vec3 rotation) {
    obj->rotation = rotation;
    Object_syncColliders(obj);
}

void Object_syncColliders(Object *obj) {
    const Mat4 r = mat4_rotate_yxz(obj->rotation);
    for (int i = 0; i < obj->collider_count; i++) {
        Collider *c = &obj->colliders[i];
        Mat4 rc = mat4_mul(r, mat4_rotate_yxz(c->tilt));
        c->axes[0] = vec3_normalize(mat4_mul_dir(rc, vec3(1, 0, 0)));
        c->axes[1] = vec3_normalize(mat4_mul_dir(rc, vec3(0, 1, 0)));
        c->axes[2] = vec3_normalize(mat4_mul_dir(rc, vec3(0, 0, 1)));
        c->offset  = vec3_add(obj->pivot, mat4_mul_dir(r, c->center));
    }
}

Mat4 Object_modelMatrix(const Object *obj) {
    Mat4 t = mat4_translate(vec3_add(obj->position, obj->pivot));
    return mat4_mul(t, mat4_rotate_yxz(obj->rotation));
}

Vec3 Object_facing(const Object *obj) {
    /* Inverse of the player's yaw: rotation.y = atan2(f.z, f.x) + pi/2 */
    float a = obj->rotation.y - PI_F / 2.0f;
    return vec3(cosf(a), 0.0f, sinf(a));
}

Vec3 Object_colliderCenter(const Object *obj, int index) {
    return vec3_add(obj->position, obj->colliders[index].offset);
}

/* (slab test in the box's frame) */
float Collider_ray(const Collider *c, Vec3 center, Vec3 origin, Vec3 dir, Vec3 *normal) {
    float t_enter = -1e9f, t_exit = 1e9f, enter_sign = 1.0f;
    int enter_axis = -1;
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    Vec3 rel = vec3_sub(origin, center);
    for (int k = 0; k < 3; k++) {
        float o = vec3_dot(rel, c->axes[k]), d = vec3_dot(dir, c->axes[k]);
        if (fabsf(d) < 1e-6f) {
            if (fabsf(o) > half[k]) return -1.0f;
            continue;
        }
        float t1 = (-half[k] - o) / d, t2 = (half[k] - o) / d;
        float sign = -1.0f; /* entering through the -half face */
        if (t1 > t2) { float t = t1; t1 = t2; t2 = t; sign = 1.0f; }
        if (t1 > t_enter) { t_enter = t1; enter_axis = k; enter_sign = sign; }
        t_exit = fminf(t_exit, t2);
    }
    if (t_enter > t_exit || t_enter < 0.0f || enter_axis < 0) return -1.0f;
    if (normal) *normal = vec3_scale(c->axes[enter_axis], enter_sign);
    return t_enter;
}

float Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir, Vec3 *normal) {
    return Collider_ray(&obj->colliders[index], Object_colliderCenter(obj, index), origin, dir, normal);
}

float Object_reach(const Object *obj) {
    float r = 0.0f;
    for (int i = 0; i < obj->collider_count; i++)
        r = fmaxf(r, vec3_length(obj->colliders[i].offset) + vec3_length(obj->colliders[i].half_extents));
    return r;
}

#define HIGH_UP 1.6f /* m over the ground: up high */

bool Object_upHigh(const Object *obj, float ground_y) {
    const Object *on = obj->standing_on;
    if (on && on->type == OBJ_WALL && on->variant == 3) return true; /* (a roof) */
    return obj->is_grounded && obj->position.y - ground_y > HIGH_UP;
}

float Object_caution(const Object *obj) {
    float d = fminf(obj->drunk, 1.0f);
    float c = 0.35f + 0.5f * obj->height_fear - 0.15f * obj->fitness; /* (sporty: surer-footed) */
    c += d * (obj->wits - 0.45f) * 0.8f; /* drunk: the sharp ones extra careful, the dim ones careless */
    return clampf(c, 0.0f, 1.0f);
}

#define WORN_FEEL 0.6f /* worn out from the days before: this much tireder at most */

/* Worn down by the day: how sleepy, felt more by the sluggish, the unfit and
   the drunk, hardly by the lively and sporty (0 = fresh .. 1 = done in). */
float Object_tiredness(const Object *obj) {
    if (!obj->is_character) return 0.0f;
    float sleepy = clampf((obj->needs.sleepiness - 0.25f) / 0.75f, 0.0f, 1.0f);
    float feels = (1.0f - 0.35f * clampf(obj->temperament.energy, -1.0f, 1.0f)) * (1.2f - 0.4f * obj->fitness)
                + 0.4f * fminf(obj->drunk, 1.0f);
    float worn = WORN_FEEL * obj->worn * (1.15f - 0.3f * obj->fitness); /* (yesterday still in the bones) */
    return clampf(sleepy * feels + worn, 0.0f, 1.0f);
}

float Object_vigor(const Object *obj) {
    return (0.5f + 0.5f * fminf(obj->needs.stamina / 0.3f, 1.0f)) * (1.0f - 0.25f * Object_tiredness(obj));
}

void Object_setHeight(Object *obj, float height) {
    if (!obj->is_character || obj->collider_count == 0 || height < 0.5f) return;
    float k = height / fmaxf(obj->size.y, 0.1f);
    obj->size = vec3(obj->size.x * k, height, obj->size.z * k);
    obj->colliders[0].half_extents = vec3_scale(obj->size, 0.5f);
    obj->pivot = vec3(0.0f, height * 0.5f, 0.0f);
    obj->step_height = height / 3.0f;
    obj->look.height = height;
    obj->top_speed *= sqrtf(k);
    obj->jump_speed *= sqrtf(k);
    Object_syncColliders(obj);
}

float Object_stepUpMost(const Object *obj) {
    const float s = obj->look.height / 1.75f;
    if (obj->type == OBJ_PLAYER) return 0.75f * s; /* (you decide yourself: as high as anyone, see physics.c STEP_UP_MOST) */
    float most = (0.38f + 0.37f * obj->fitness) * (0.75f + 0.25f * Object_vigor(obj)) - 0.25f * fminf(obj->drunk, 1.0f);
    return clampf(most, 0.25f, 0.75f) * s;
}

float Object_topSpeed(const Object *obj) {
    return obj->top_speed * Object_vigor(obj) * (1.0f - 0.35f * fminf(obj->drunk, 1.0f));
}

float Object_jumpSpeed(const Object *obj) {
    return obj->jump_speed * (0.85f + 0.15f * Object_vigor(obj)) * (1.0f - 0.3f * fminf(obj->drunk, 1.0f));
}

void Object_setPerson(Object *obj, const char *name, int main_hand) {
    snprintf(obj->name, sizeof(obj->name), "%s", name);
    obj->main_hand = main_hand;
    /* Light and lively people are quicker on their feet */
    float build = 70.0f / fmaxf(obj->weight, 30.0f);
    float fit = obj->fitness - 0.5f; /* (sporty people are quicker, whatever their build) */
    obj->top_speed  = SPRINT_SPEED * powf(build, 0.35f) * (1.0f + 0.12f * obj->temperament.energy) * (1.0f + 0.25f * fit);
    obj->jump_speed = JUMP_SPEED * powf(build, 0.3f) * (1.0f + 0.08f * obj->temperament.energy) * (1.0f + 0.15f * fit);
    /* Bigger people and those with stamina hold more; and everybody a bit their own */
    unsigned h = 2166136261u;
    for (const char *c = obj->name; *c; c++) h = (h ^ (unsigned char)*c) * 16777619u;
    obj->bladder_size = clampf(1.0f + 0.25f * clampf((obj->weight - 70.0f) / 25.0f, -1.0f, 1.0f)
                               + 0.15f * obj->temperament.energy + 0.3f * ((float)(h % 1000u) / 1000.0f - 0.5f), 0.65f, 1.5f);
}

float Object_bladder(const Object *obj) {
    float size = obj->bladder_size > 0.1f ? obj->bladder_size : 1.0f;
    return size * (0.8f + 0.2f * Object_vigor(obj)); /* worn out: it presses sooner */
}

float Object_peeTime(const Object *obj) {
    return clampf(2.5f + 7.0f * obj->needs.bladder * Object_bladder(obj), 2.5f, 12.0f);
}

bool Object_isBottle(const Object *obj) {
    return obj->type == OBJ_BOTTLE || obj->type == OBJ_VODKA || obj->type == OBJ_WATER;
}

bool Object_isFood(const Object *obj) {
    return obj->type == OBJ_APPLE || obj->type == OBJ_BREAD || obj->type == OBJ_SAUSAGE;
}

bool Object_isTrash(const Object *obj) {
    if (!obj->mesh.vao) return false; /* used up */
    if (obj->type == OBJ_TRASH) return true;
    return Object_isBottle(obj) && (obj->sips <= 0 || obj->broken);
}

bool Object_isFuel(const Object *obj) {
    if (!obj->mesh.vao) return false; /* burnt */
    return obj->type == OBJ_FIREWOOD || obj->type == OBJ_BRANCH || (obj->type == OBJ_LOG && obj->size.y <= 0.8f);
}

Object *Object_createFurniture(FurnitureKind kind, Vec3 position, Vec3 size, float yaw) {
    Object *o = Object_create(OBJ_FURNITURE, PALETTE_WARM, position, size, (float)kind);
    if (o) Object_setRotation(o, vec3(0.0f, yaw, 0.0f));
    return o;
}

#define GATE_SWING_TIME 1.8f /* s from shut to open (right round) */

void Object_swingGate(Object *g, float dt) {
    float goal = g->gate_open ? 1.0f : 0.0f;
    g->gate_t += clampf(goal - g->gate_t, -dt / GATE_SWING_TIME, dt / GATE_SWING_TIME);
    float u = smoothstepf(0.0f, 1.0f, g->gate_t);
    float a = u * PI_F * 0.985f; /* (open: right round, flat against the wall outside) */
    Vec3 dir = vec3_normalize(vec3_add(vec3_scale(g->gate_shut, cosf(a)), vec3_scale(g->gate_out, sinf(a))));
    /* (folding back it stands off the wall by its own thickness: it lies against it, not in it) */
    Vec3 mid = vec3_add(g->hinge, vec3_add(vec3_scale(dir, g->size.x * 0.5f), vec3_scale(g->gate_out, smoothstepf(0.6f, 1.0f, u) * (g->size.z + 0.02f))));
    g->position = vec3(mid.x, g->hinge.y, mid.z);
    g->rotation.y = yaw_along(dir); /* (local x along the leaf) */
    Object_syncColliders(g);
}

void Object_useToilet(Object *who, Object *toilet) {
    if (toilet->dog.mode) return; /* occupied */
    who->toilet = toilet;
    toilet->dog.mode = 1;        /* (furniture: occupied) */
    who->sitting = false;
    who->position = vec3(toilet->position.x, toilet->position.y + 0.05f, toilet->position.z);
    who->velocity = vec3_zero();
    Object_syncColliders(who);
    Body_pee(&who->body, Object_peeTime(who));
}

#define SEAT_BACK 0.2f /* the hips sit this far behind where the feet are (see body.c) */
#define CHAIR_SPEED 0.9f  /* m/s it slides */
#define CHAIR_GRAB  0.9f  /* m: within this a free hand is on its backrest while it slides */

void Object_slideChair(Object *chair, float dt) {
    if (!chair->tucked_set) return;
    chair->chair_out += clampf(chair->chair_goal - chair->chair_out, -CHAIR_SPEED * dt, CHAIR_SPEED * dt);
    Vec3 front = chair->colliders[0].axes[2];
    front = vec3_normalize(vec3(front.x, 0.0f, front.z));
    Vec3 at = vec3_sub(chair->tucked, vec3_scale(front, chair->chair_out));
    chair->position = vec3(at.x, chair->position.y, at.z);
    Object_syncColliders(chair);
}

/* While a chair slides for who, a free hand holds its backrest. */
static void handOnChair(Object *who, const Object *chair) {
    if (chair->type != OBJ_FURNITURE || chair->variant != FURN_CHAIR || fabsf(chair->chair_goal - chair->chair_out) < 1e-3f) return;
    Vec3 front = chair->colliders[0].axes[2];
    Vec3 grip = vec3_add(chair->position, vec3_add(vec3_scale(front, -chair->size.z * 0.5f), vec3(0.0f, chair->size.y - 0.04f, 0.0f)));
    if (vec3_length(vec3_sub(vec3(grip.x, who->position.y, grip.z), who->position)) > CHAIR_GRAB) return;
    for (int h = 0; h < 2; h++) {
        if (who->hands.item[h] || who->body.hand_active[h]) continue;
        who->body.hand_active[h] = true;
        who->body.hand_target[h] = grip;
        return; /* (one hand does it) */
    }
}

/* Everybody (set each frame by the world): who sits where already. */
static Object *const *g_people;
static int g_people_n;
void Object_seePeople(Object *const *objects, int count) { g_people = objects; g_people_n = count; }

#define SEAT_SPACE 0.55f /* m along a bench or a log between two people sitting on it */

/* Where along a bench or log (from its middle, axis along it) somebody can
   sit nearest to `want`, clear of everybody already on it; NAN: full. */
static bool clearAt(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float cand) {
    for (int i = 0; i < g_people_n; i++) {
        const Object *o = g_people[i];
        if (o == who || !o->is_character || o->seat != seat || !o->sitting) continue;
        if (fabsf(vec3_dot(vec3_sub(o->sit_at, c), axis) - cand) < SEAT_SPACE - 0.06f) return false; /* (a little give: moved up exactly that far) */
    }
    return true;
}

static float freeAlong(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float want, float half) {
    if (seat->type == OBJ_FURNITURE) { /* a bench: its places, evenly along it (sat anywhere, a third didn't fit) - the free one nearest */
        int n = (int)(2.0f * half / SEAT_SPACE) + 1;
        float gap = n > 1 ? 2.0f * half / (float)(n - 1) : 0.0f, best = NAN;
        for (int k = 0; k < n; k++) {
            float cand = n > 1 ? -half + gap * (float)k : 0.0f;
            if (clearAt(who, seat, c, axis, cand) && (isnan(best) || fabsf(cand - want) < fabsf(best - want))) best = cand;
        }
        if (!isnan(best)) return best;
    }
    for (int k = 0; k <= 2 * (int)(half / 0.1f) + 2; k++) {
        float cand = want + 0.1f * (float)((k + 1) / 2) * (k % 2 ? 1.0f : -1.0f);
        if (cand < -half || cand > half) continue;
        if (clearAt(who, seat, c, axis, cand)) return cand;
    }
    return NAN;
}

/* A bench or a log as a line to sit along: its middle, which way it runs, how far either way. */
static bool seatLine(const Object *seat, Vec3 *c, Vec3 *axis, float *half) {
    if (!seat || seat->collider_count == 0) return false;
    if (seat->type == OBJ_LOG) {
        *c = Object_colliderCenter(seat, 0);
        *axis = seat->colliders[0].axes[1];
        *half = seat->size.y * 0.35f;
    } else if (seat->type == OBJ_FURNITURE && seat->variant == FURN_BENCH) {
        *c = seat->position;
        Vec3 a = seat->colliders[0].axes[0];
        *axis = vec3_normalize(vec3(a.x, 0.0f, a.z));
        *half = fmaxf(seat->colliders[0].half_extents.x - 0.28f, 0.0f);
    } else return false;
    return true;
}

/* Moving up on a bench: everybody on it in order along it, the newcomer at
   `want`; spread out till each has SEAT_SPACE, as little moved as can be.
   Who won't move (you, the unwilling unless asked, sleepers) stays put.
   Fills pos (the newcomer's at index *mine); false: no room even so. */
#define ROOM_MOST 6
static bool spreadOut(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float half, float want, bool asked,
                      const Object **people, float *pos, float *from, int *n_out, int *mine) {
    int n = 0;
    for (int i = 0; i < g_people_n && n < ROOM_MOST - 1; i++) {
        const Object *o = g_people[i];
        if (o == who || !o->is_character || o->seat != seat || !o->sitting) continue;
        people[n] = o;
        from[n] = vec3_dot(vec3_sub(o->sit_at, c), axis);
        n++;
    }
    people[n] = who;
    from[n] = clampf(want, -half, half);
    n++;
    for (int a = 1; a < n; a++) /* (in order along it) */
        for (int b = a; b > 0 && from[b] < from[b - 1]; b--) {
            float t = from[b]; from[b] = from[b - 1]; from[b - 1] = t;
            const Object *q = people[b]; people[b] = people[b - 1]; people[b - 1] = q;
        }
    bool fixed[ROOM_MOST];
    for (int k = 0; k < n; k++) {
        const Object *o = people[k];
        pos[k] = from[k];
        if (o == who) *mine = k;
        fixed[k] = o != who && (o->type == OBJ_PLAYER || o->body.asleep || o->sit_moving ||
                                (!asked && (o->ai.character.willing < 0.35f || o->ai.annoyed > 0.3f)));
    }
    for (int pass = 0; pass < 40; pass++) {
        for (int k = 1; k < n; k++) {
            float gap = pos[k] - pos[k - 1];
            if (gap >= SEAT_SPACE) continue;
            float d = SEAT_SPACE - gap;
            if (fixed[k] && fixed[k - 1]) continue;
            if (fixed[k]) pos[k - 1] -= d;
            else if (fixed[k - 1]) pos[k] += d;
            else { pos[k - 1] -= 0.5f * d; pos[k] += 0.5f * d; }
        }
        for (int k = 0; k < n; k++) if (!fixed[k]) pos[k] = clampf(pos[k], -half, half);
    }
    for (int k = 0; k < n; k++) {
        if (pos[k] < -half - 0.01f || pos[k] > half + 0.01f) return false;
        if (k > 0 && pos[k] - pos[k - 1] < SEAT_SPACE - 0.01f) return false;
    }
    *n_out = n;
    return true;
}

int Object_benchRoom(const Object *who, const Object *seat, Vec3 near, bool asked) {
    Vec3 c, axis;
    float half;
    if (!seatLine(seat, &c, &axis, &half)) return ROOM_NONE;
    float want = clampf(vec3_dot(vec3_sub(near, c), axis), -half, half);
    if (!isnan(freeAlong(who, seat, c, axis, want, half))) return ROOM_FREE;
    const Object *people[ROOM_MOST];
    float pos[ROOM_MOST], from[ROOM_MOST];
    int n = 0, mine = 0;
    return spreadOut(who, seat, c, axis, half, want, asked, people, pos, from, &n, &mine) ? ROOM_SCOOT : ROOM_NONE;
}

bool Object_benchMakeRoom(const Object *who, Object *seat, Vec3 near, bool asked) {
    Vec3 c, axis;
    float half;
    if (!seatLine(seat, &c, &axis, &half)) return false;
    float want = clampf(vec3_dot(vec3_sub(near, c), axis), -half, half);
    if (!isnan(freeAlong(who, seat, c, axis, want, half))) return true;
    const Object *people[ROOM_MOST];
    float pos[ROOM_MOST], from[ROOM_MOST];
    int n = 0, mine = 0;
    if (!spreadOut(who, seat, c, axis, half, want, asked, people, pos, from, &n, &mine)) return false;
    for (int k = 0; k < n; k++) {
        if (k == mine || fabsf(pos[k] - from[k]) < 0.01f) continue;
        Object *o = (Object *)people[k]; /* (one of g_people: the world's own) */
        o->sit_at = vec3_add(o->sit_at, vec3_scale(axis, pos[k] - from[k]));
        o->sit_scoot = true;
    }
    return true;
}

#define SCOOT_SPEED 0.35f /* m/s sliding along a bench, sitting */
void Object_scootOnSeat(Object *who, float dt) {
    if (!who->sit_scoot) return;
    if (!who->sitting || who->sit_moving) { who->sit_scoot = false; return; }
    Vec3 to = vec3(who->sit_at.x - who->position.x, 0.0f, who->sit_at.z - who->position.z);
    float d = vec3_length(to), step = SCOOT_SPEED * dt;
    if (d <= step) { who->position.x = who->sit_at.x; who->position.z = who->sit_at.z; who->sit_scoot = false; }
    else { who->position.x += to.x / d * step; who->position.z += to.z / d * step; }
    Object_syncColliders(who);
}

static bool seatTaken(const Object *who, const Object *seat) {
    for (int i = 0; i < g_people_n; i++)
        if (g_people[i] != who && g_people[i]->is_character && g_people[i]->seat == seat && g_people[i]->sitting) return true;
    return false;
}

static bool noRoom(Object *who) { who->sitting = false; who->seat = NULL; return false; }

bool Object_sit(Object *who, Object *seat, Vec3 toward) {
    who->sitting = true;
    who->seat = seat;
    who->body.crossing = false; /* (not stepping over it: onto it) */
    who->seat_scooted = false;
    who->sit_style = SIT_GROUND;
    who->body.seat_height = SEAT_HEIGHT;
    if (!seat) return true;
    if (seat->type == OBJ_LOG) { /* on a log: beside it, facing away from it */
        Vec3 c = Object_colliderCenter(seat, 0), axis = seat->colliders[0].axes[1];
        Vec3 rel = vec3_sub(who->position, c);
        Vec3 across = vec3_normalize(vec3(-axis.z, 0.0f, axis.x));
        if (vec3_dot(across, rel) < 0.0f) across = vec3_scale(across, -1.0f);
        float want = clampf(vec3_dot(rel, axis), -seat->size.y * 0.35f, seat->size.y * 0.35f);
        float along = freeAlong(who, seat, c, axis, want, seat->size.y * 0.35f);
        if (isnan(along) && Object_benchMakeRoom(who, seat, who->position, false)) along = freeAlong(who, seat, c, axis, want, seat->size.y * 0.35f); /* (the others move up) */
        if (isnan(along)) return noRoom(who); /* (somebody's there already: no sitting on top of them) */
        Vec3 at = vec3_add(vec3_add(c, vec3_scale(axis, along)), vec3_scale(across, SEAT_BACK - 0.02f)); /* the bottom on top of it */
        who->sit_at = vec3(at.x, who->position.y, at.z);
        who->sit_yaw = atan2f(across.z, across.x) + PI_F / 2.0f;
        who->sit_dir = vec3_normalize(vec3(across.x, 0.0f, across.z));
        who->sit_moving = true; /* (see Object_settleOnSeat) */
        who->sit_backing = false;
        who->velocity = vec3_zero();
        who->sit_style = SIT_SEAT;
        who->body.seat_height = fmaxf(seat->size.x, 0.15f);
        return true;
    }
    if (seat->variant == FURN_CHAIR && seatTaken(who, seat)) return noRoom(who);
    bool over = false; /* a beer bench: at its table, stepped over it (the others always; you walking up to it, not turned away) */
    if (seat->variant == FURN_BENCH && seat->table) {
        Vec3 to_seat = vec3(seat->position.x - who->position.x, 0.0f, seat->position.z - who->position.z);
        Vec3 to_table = vec3(seat->table->position.x - who->position.x, 0.0f, seat->table->position.z - who->position.z);
        bool beyond = vec3_length(to_table) > vec3_length(to_seat); /* (we're on the far side of the bench from the table) */
        bool facing = vec3_length(to_seat) < 1e-3f || vec3_dot(Object_facing(who), vec3_normalize(to_seat)) > -0.2f;
        if (who->type != OBJ_PLAYER || facing) { toward = seat->table->position; over = beyond; }
    }
    Vec3 n = seat->colliders[0].axes[2]; /* across the seat */
    Vec3 to = vec3_sub(toward, seat->position);
    if (seat->variant == FURN_CHAIR || vec3_dot(n, to) < 0.0f) n = seat->variant == FURN_CHAIR ? n : vec3_scale(n, -1.0f);
    n = vec3_normalize(vec3(n.x, 0.0f, n.z));
    Vec3 base = seat->position;
    if (seat->variant == FURN_CHAIR) { /* a chair is pulled out first (it may stand at a table): room in front of it */
        if (!seat->tucked_set) { seat->tucked = seat->position; seat->tucked_set = true; }
        seat->chair_goal = CHAIR_PULL;
        base = vec3_sub(seat->tucked, vec3_scale(n, CHAIR_PULL));
    }
    /* the bottom in the middle of the seat (a chair: a little back, against the backrest); a bench: where
       you are along it, clear of whoever sits on it already */
    if (seat->variant != FURN_CHAIR && seat->collider_count > 0) {
        Vec3 along_axis = seat->colliders[0].axes[0];
        along_axis = vec3_normalize(vec3(along_axis.x, 0.0f, along_axis.z));
        float half = fmaxf(seat->colliders[0].half_extents.x - 0.28f, 0.0f);
        float want = clampf(vec3_dot(vec3_sub(who->position, seat->position), along_axis), -half, half);
        float along = freeAlong(who, seat, seat->position, along_axis, want, half);
        if (isnan(along) && Object_benchMakeRoom(who, seat, who->position, false)) along = freeAlong(who, seat, seat->position, along_axis, want, half); /* (the others move up) */
        if (isnan(along)) return noRoom(who);
        base = vec3_add(base, vec3_scale(along_axis, along));
    }
    Vec3 at = vec3_add(base, vec3_scale(n, SEAT_BACK + (seat->variant == FURN_CHAIR ? -0.04f : 0.0f)));
    who->sit_at = vec3(at.x, who->position.y, at.z);
    who->sit_yaw = atan2f(n.z, n.x) + PI_F / 2.0f; /* facing n */
    who->sit_dir = n;
    who->sit_moving = true;
    who->sit_backing = over; /* (over the bench: straight to the spot, no turning round in front of it) */
    who->velocity = vec3_zero();
    who->sit_style = SIT_SEAT;
    who->body.seat_height = SEAT_HEIGHT;
    return true;
}

#define SETTLE_SPEED 1.1f  /* m/s stepping over to the seat */
#define SETTLE_TURN  4.5f  /* rad/s turning round to sit */
#define SETTLE_FRONT 0.3f  /* m in front of the seat spot: turned round there */

void Object_leaveSeat(Object *who, float dt) {
    Object *seat = who->seat;
    if (!seat || who->sitting) return;
    if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR && seat->chair_out < seat->chair_goal - 0.01f) return; /* (out from the table first) */
    /* got up: the seat is solid again once you're clear of it */
    bool clear = true;
    for (int c = 0; c < seat->collider_count && clear; c++) {
        const Collider *col = &seat->colliders[c];
        Vec3 rel = vec3_sub(who->position, Object_colliderCenter(seat, c));
        clear = fabsf(vec3_dot(rel, col->axes[0])) > col->half_extents.x + 0.3f ||
                fabsf(vec3_dot(rel, col->axes[2])) > col->half_extents.z + 0.3f;
    }
    if (clear) {
        if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR) seat->chair_goal = 0.0f; /* pushed in again (see world.c) */
        who->seat = NULL;
        return;
    }
    /* ... and standing up you step forward off it */
    Vec3 front = vec3_add(who->sit_at, vec3_scale(who->sit_dir, SETTLE_FRONT + 0.1f));
    Vec3 to = vec3(front.x - who->position.x, 0.0f, front.z - who->position.z);
    float dist = vec3_length(to), step = 0.7f * SETTLE_SPEED * dt;
    if (dist > 0.02f && dist < 1.0f && vec3_length(vec3(who->movement.x, 0.0f, who->movement.z)) < 0.1f) {
        who->position = vec3_add(who->position, vec3_scale(to, fminf(step, dist) / dist));
        Object_syncColliders(who);
    } else if (dist <= 0.02f || dist >= 1.0f) {
        if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR) seat->chair_goal = 0.0f;
        who->seat = NULL; /* (stood up out of the way) */
    }
}

bool Object_settleOnSeat(Object *who, float dt) {
    if (!who->sitting || !who->sit_moving) return false;
    if (!who->seat) { who->sit_moving = false; return false; }
    handOnChair(who, who->seat); /* (the world slides it: Object_slideChair) */
    /* first in front of it (feet clear of it), then turned round, a step back */
    Vec3 goal = who->sit_backing ? who->sit_at : vec3_add(who->sit_at, vec3_scale(who->sit_dir, SETTLE_FRONT));
    Vec3 to = vec3(goal.x - who->position.x, 0.0f, goal.z - who->position.z);
    float dist = vec3_length(to), step = SETTLE_SPEED * (who->sit_backing ? 0.5f : 1.0f) * dt;
    if (dist > 1.8f) { /* (pushed away meanwhile) */
        who->sitting = false;
        who->sit_moving = false;
        who->seat = NULL;
        return false;
    }
    if (dist > step) { who->position.x += to.x / dist * step; who->position.z += to.z / dist * step; }
    else { who->position.x = goal.x; who->position.z = goal.z; }
    float turn = remainderf(who->sit_yaw - who->rotation.y, 2.0f * PI_F), most = SETTLE_TURN * dt;
    who->rotation.y += clampf(turn, -most, most);
    who->velocity.x = who->velocity.z = 0.0f;
    who->movement = vec3_zero();
    Object_syncColliders(who);
    if (dist <= step && fabsf(turn) <= most) {
        if (who->sit_backing) who->sit_moving = false; /* there: down onto it */
        else who->sit_backing = true;
    }
    return true;
}

/* Something to sit on: a chair or a bench, else a log lying on the ground. */
static bool isLogSeat(const Object *o) {
    return o->type == OBJ_LOG && !o->is_held && !o->lifted && o->mesh.vao && o->size.x >= 0.18f && o->size.y >= 0.5f &&
           fabsf(o->colliders[0].axes[1].y) < 0.3f; /* lying, not standing on end */
}

Object *Object_seatNear(Object *const *objects, int count, Vec3 pos, float reach) {
    Object *best = NULL;
    float best_reach = reach;
    for (int i = 0; i < count; i++) { /* chairs and benches first */
        Object *o = objects[i];
        if (o->type != OBJ_FURNITURE || (o->variant != FURN_CHAIR && o->variant != FURN_BENCH)) continue;
        float d = sqrtf((o->position.x - pos.x) * (o->position.x - pos.x) + (o->position.z - pos.z) * (o->position.z - pos.z));
        if (d - o->size.x * 0.5f < best_reach) {
            best_reach = d - o->size.x * 0.5f;
            best = o;
        }
    }
    if (best) return best;
    for (int i = 0; i < count; i++) { /* else a log */
        Object *o = objects[i];
        if (!isLogSeat(o)) continue;
        Vec3 c = Object_colliderCenter(o, 0), axis = o->colliders[0].axes[1], rel = vec3_sub(pos, c);
        float along = clampf(vec3_dot(rel, axis), -o->size.y * 0.5f, o->size.y * 0.5f);
        Vec3 near = vec3_add(c, vec3_scale(axis, along));
        float d = sqrtf((near.x - pos.x) * (near.x - pos.x) + (near.z - pos.z) * (near.z - pos.z)) - o->size.x * 0.5f;
        if (d < best_reach) {
            best_reach = d;
            best = o;
        }
    }
    return best;
}

void Object_setDoor(Object *obj, float x) {
    if (obj->type != OBJ_HOUSE) return;
    BuildingSize b = { .length = obj->size.x, .width = obj->size.z, .wall_height = obj->size.y,
                       .roof_height = obj->size.z * ROOF_PITCH, .look = (BuildingLook)obj->variant,
                       .door_set = true, .door_x = x };
    Mesh mesh = Shapes_building(b, true);
    if (!mesh.vao) return;
    Mesh_destroy(&obj->mesh);
    obj->mesh = mesh;
}

#define BAG_FULL 24  /* pieces of trash in a full bag */

void Object_fillBag(Object *bag, float added_weight) {
    float full = fminf((float)bag->stored / (float)BAG_FULL, 1.0f);
    bag->weight += added_weight;
    Mesh mesh = Shapes_bag(full);
    if (!mesh.vao) return;
    Mesh_destroy(&bag->mesh);
    bag->mesh = mesh;
    bag->size = Shapes_bagSize(full);
    bag->pivot = vec3(0.0f, bag->size.y * 0.5f, 0.0f);
    bag->collider_count = 0;
    addCollider(bag, vec3_zero(), bag->size, vec3_zero());
    Object_syncColliders(bag);
}

/* New mesh and colliders for the tree's current trunk and crown. */
static void rebuildTree(Object *obj) {
    TreeSize t = treeSize(obj);
    Mesh mesh = Shapes_tree(t);
    if (!mesh.vao) return;
    Mesh_destroy(&obj->mesh);
    obj->mesh = mesh;
    obj->collider_count = 0;
    setupTree(obj, t);
    Object_syncColliders(obj);
}

void Object_rebuildTree(Object *obj) {
    if (obj->type == OBJ_TREE) rebuildTree(obj);
}

void Object_cutTrunk(Object *obj, float height) {
    if (obj->type != OBJ_TREE) return;
    height = clampf(height, 0.1f, obj->size.y - 0.5f);
    obj->size.y -= height;
    obj->position.y += height; /* the tree now starts at the cut */
    obj->trunk_cut = true;
    rebuildTree(obj);
}

void Object_setBranches(Object *obj, int branches) {
    if (obj->type != OBJ_TREE || branches == obj->branches) return;
    obj->branches = branches;
    if (branches > 0) rebuildTree(obj);
    if (!obj->is_static) obj->weight = Object_treeWeight(obj);
}

#define TREE_BRANCH_KG 18.0f /* a layer of branches */
#define TREE_DRAG      0.5f  /* dragged along: one end on the ground, about half its weight ... */
#define TREE_SNAG_KG   25.0f /* ... and each layer of branches catches in the ground */

float Object_treeWeight(const Object *obj) {
    float r = obj->colliders[0].half_extents.x;
    return WOOD_DENSITY * PI_F * r * r * obj->size.y + TREE_BRANCH_KG * (float)obj->branches;
}

float Object_dragWeight(const Object *obj) {
    if (obj->type == OBJ_TREE && obj->toppled && !obj->lifted)
        return TREE_DRAG * obj->weight + TREE_SNAG_KG * (float)obj->branches;
    return obj->weight;
}

void Object_topple(Object *obj, Vec3 dir) {
    bool tree = obj->type == OBJ_TREE;
    if (!(Object_isBottle(obj) || tree) || obj->toppled || obj->is_held || obj->broken) return;
    dir.y = 0.0f;
    if (vec3_length(dir) < 1e-4f) return;
    dir = vec3_normalize(dir);
    /* Tipping by rotation.x tips the top towards -facing (see Object_facing) */
    obj->rotation = vec3(0.0f, atan2f(-dir.z, -dir.x) + PI_F / 2.0f, 0.0f);
    obj->toppled = true;
    obj->topple  = 0.0f;
    obj->fall_dir = dir;
    obj->fall_base_y = obj->position.y;
    obj->fall_origin = obj->position;
    obj->fall_ground_y = obj->position.y; /* (the world says where the ground is: see Forestry) */
    obj->fall_slide = 0.0f;
    Object_syncColliders(obj);
}

void Object_makeLog(Object *obj, Vec3 center, Vec3 axis, float length, float diameter) {
    Mesh log = Shapes_log(diameter, length);
    if (!log.vao) return;
    Mesh_destroy(&obj->mesh);
    obj->mesh           = log;
    obj->type           = OBJ_LOG;
    obj->is_static      = false;
    obj->toppled        = false;
    obj->topple         = 0.0f;
    obj->work           = 0.0f;
    obj->stump          = NULL;
    obj->pivot          = vec3(0.0f, length * 0.5f, 0.0f);
    obj->size           = vec3(diameter, length, 0.0f);
    obj->weight         = WOOD_DENSITY * PI_F * diameter * diameter * 0.25f * length;
    obj->collider_count = 0;
    addCollider(obj, vec3_zero(), vec3(diameter, length, diameter), vec3_zero());

    /* Local Y along the axis; local Z any direction across it */
    axis = vec3_normalize(axis);
    Vec3 across = fabsf(axis.y) < 0.9f ? vec3(0, 1, 0) : vec3(1, 0, 0);
    across = vec3_normalize(vec3_sub(across, vec3_scale(axis, vec3_dot(across, axis))));
    obj->rotation = euler_yxz_from_axes(axis, across);
    obj->position = vec3_sub(center, obj->pivot);
    obj->velocity = vec3_zero();
    obj->angular_velocity = vec3_zero();
    Object_syncColliders(obj);
}

void Object_makeFirewood(Object *obj, Vec3 center, Vec3 axis, Vec3 up, float length, float diameter, int parts) {
    Mesh_destroy(&obj->mesh);
    obj->type      = OBJ_FIREWOOD;
    obj->is_static = false;
    obj->toppled   = false;
    obj->topple    = 0.0f;
    obj->work      = 0.0f;
    obj->stump     = NULL;
    setupFirewood(obj, diameter, length, parts);
    if (!obj->mesh.vao) return;

    axis = vec3_normalize(axis);
    up = vec3_normalize(vec3_sub(up, vec3_scale(axis, vec3_dot(up, axis))));
    obj->rotation = euler_yxz_from_axes(axis, up);
    obj->position = vec3_sub(center, obj->pivot);
    obj->velocity = vec3_zero();
    obj->angular_velocity = vec3_zero();
    Object_syncColliders(obj);
}

/* Where the k-th piece of firewood lies on a woodpile: rows of quarters lying
   across the sleepers, layer on layer; odd layers have one piece less and sit
   in the gaps of the layer below. */
static Vec3 pileSlot(const Object *pile, float r, int k, int *layer_out) {
    const float width = r + 0.01f;
    const Vec3 along = pile->colliders[0].axes[0], up = vec3(0.0f, 1.0f, 0.0f);
    int per_layer = (int)(pile->size.x / width);
    if (per_layer < 1) per_layer = 1;
    int col = k, layer = 0;
    while (col >= per_layer - (layer % 2) && per_layer > 1) {
        col -= per_layer - (layer % 2);
        layer++;
    }
    int in_layer = per_layer > 1 ? per_layer - (layer % 2) : 1;
    if (per_layer == 1) { layer = col; col = 0; }
    float x = ((float)col - (float)(in_layer - 1) * 0.5f) * width;
    if (layer_out) *layer_out = layer;
    Vec3 top = vec3_add(Object_colliderCenter(pile, 0), vec3(0.0f, WOODPILE_BASE * 0.5f, 0.0f));
    return vec3_add(vec3_add(top, vec3_scale(along, x)), vec3_scale(up, (float)layer * (r + 0.005f) + r * 0.5f + 0.003f));
}

/* Firewood lying on the pile (not carried off). */
bool Object_onPile(const Object *pile, const Object *o) {
    if (o->type != OBJ_FIREWOOD || !o->stored || o->is_held || !o->mesh.vao) return false;
    Vec3 rel = vec3_sub(Object_colliderCenter(o, 0), Object_colliderCenter(pile, 0));
    return fabsf(vec3_dot(rel, pile->colliders[0].axes[0])) < pile->size.x * 0.5f + 0.15f &&
           fabsf(vec3_dot(rel, pile->colliders[0].axes[2])) < pile->size.z * 0.5f + 0.5f && rel.y > -0.2f && rel.y < 2.5f;
}

int Object_pileCount(const Object *pile, Object *const *objects, int count) {
    int n = 0;
    for (int i = 0; i < count; i++)
        if (Object_onPile(pile, objects[i])) n++;
    return n;
}

/* Small dice from a position and a number (no shared state). */
static float pileDice(Vec3 p, int k) {
    unsigned h = (unsigned)(int)(p.x * 331.0f) * 73856093u ^ (unsigned)(int)(p.z * 257.0f) * 19349663u ^ (unsigned)k * 83492791u;
    h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15;
    return (float)(h >> 8) / 16777216.0f;
}

#define PILE_SLIP_FROM 3 /* a loose pile this many layers high may come down */
#define PILE_SORT_CARE 0.7f  /* this careful: sorted by length, the front even */
#define PILE_PIECE     0.6f  /* the usual length of a piece */
#define PILE_SAME      0.04f /* (this close to it counts as usual) */

int Object_stackFirewood(Object *pile, Object *piece, Object *const *objects, int count, float care) {
    /* The lowest place that's free (pieces taken off leave gaps lower down). Careful
       people sort by length: long pieces from one end of the layer, short ones from the other */
    const float r = piece->size.x * 0.5f;
    const bool sorts = care >= PILE_SORT_CARE;
    const bool short_one = piece->size.y < PILE_PIECE - PILE_SAME;
    int layer = 0, first_layer = -1;
    Vec3 center = pileSlot(pile, r, 0, &layer), chosen = center;
    int chosen_layer = 0;
    for (int k = 0; k < 400; k++) {
        center = pileSlot(pile, r, k, &layer);
        if (first_layer >= 0 && layer != first_layer) break; /* (sorting: that layer's free places looked at) */
        bool taken = false;
        for (int i = 0; i < count && !taken; i++) {
            const Object *o = objects[i];
            if (o == piece || !Object_onPile(pile, o)) continue;
            Vec3 d = vec3_sub(Object_colliderCenter(o, 0), center);
            if (fabsf(d.y) < r * 0.7f && fabsf(vec3_dot(d, pile->colliders[0].axes[0])) < r * 0.7f) taken = true;
        }
        if (taken) continue;
        if (first_layer < 0) { chosen = center; chosen_layer = layer; first_layer = layer; }
        else if (short_one) { chosen = center; chosen_layer = layer; } /* (the last free one: the other end) */
        if (!sorts || !short_one) break;
    }
    center = chosen;
    layer = chosen_layer;
    Vec3 across = pile->colliders[0].axes[2], along = pile->colliders[0].axes[0];
    if (sorts) /* (and pushed in so the front is even, whatever the length) */
        center = vec3_add(center, vec3_scale(across, (PILE_PIECE - piece->size.y) * 0.5f));
    const Vec3 up = vec3(0.0f, 1.0f, 0.0f);
    /* Careless: not quite in its place - pushed in more or less, a bit to the side, askew */
    const float loose = 1.0f - clampf(care, 0.0f, 1.0f);
    const int n = pile->stored;
    if (loose > 0.05f) {
        center = vec3_add(center, vec3_add(vec3_scale(across, (pileDice(center, n) - 0.5f) * 0.3f * loose),
                                           vec3_scale(along, (pileDice(center, n + 7) - 0.5f) * 0.5f * r * loose)));
        float twist = (pileDice(center, n + 13) - 0.5f) * 0.5f * loose;
        across = vec3_add(vec3_scale(across, cosf(twist)), vec3_scale(along, sinf(twist)));
        center.y += 0.01f * loose; /* (resting on the uneven layer below) */
    }
    Object_makeFirewood(piece, center, across, up, piece->size.y, piece->size.x, 4);
    piece->stored = 1;
    pile->stored = Object_pileCount(pile, objects, count);
    /* A loose pile getting high: now and then the top comes down */
    int slid = 0;
    if (layer >= PILE_SLIP_FROM && pileDice(center, n + 29) < (loose - 0.45f) * 0.25f * (float)(layer - PILE_SLIP_FROM + 1)) {
        float side = pileDice(center, n + 31) < 0.5f ? -1.0f : 1.0f;
        const float top = center.y - 1.5f * r;
        for (int i = 0; i < count; i++) {
            Object *o = objects[i];
            if (!Object_onPile(pile, o) || Object_colliderCenter(o, 0).y < top) continue;
            Vec3 push = vec3_add(vec3_scale(pile->colliders[0].axes[2], side * (0.9f + 0.6f * pileDice(o->position, i))),
                                 vec3_scale(along, (pileDice(o->position, i + 3) - 0.5f) * 0.8f));
            o->velocity = vec3_add(push, vec3(0.0f, 0.4f, 0.0f));
            o->angular_velocity = vec3_scale(along, side * 3.0f);
            o->stored = 0;
            o->rest_time = 0.0f;
            slid++;
        }
        pile->stored = Object_pileCount(pile, objects, count);
    }
    return slid;
}

void Object_markCut(Object *o, Vec3 point, float progress) {
    o->mark = progress;
    if (progress <= 0.0f || o->collider_count == 0) { o->mark = 0.0f; return; }
    /* in the object's own frame (local Y along the wood), outwards from the axis */
    Mat4 r = mat4_rotate_yxz(o->rotation);
    Vec3 ax = mat4_mul_dir(r, vec3(1, 0, 0)), ay = mat4_mul_dir(r, vec3(0, 1, 0)), az = mat4_mul_dir(r, vec3(0, 0, 1));
    Vec3 v = vec3_sub(point, vec3_add(o->position, o->pivot));
    Vec3 local = vec3(vec3_dot(v, ax), vec3_dot(v, ay), vec3_dot(v, az));
    Vec3 out = vec3(local.x, 0.0f, local.z);
    o->mark_at = local;
    o->mark_out = vec3_length(out) > 1e-3f ? vec3_normalize(out) : vec3(1.0f, 0.0f, 0.0f);
}

float Object_bottom(const Object *obj) {
    if (obj->collider_count == 0) return obj->position.y;
    const Collider *c = &obj->colliders[0];
    float extent = fabsf(c->axes[0].y) * c->half_extents.x + fabsf(c->axes[1].y) * c->half_extents.y +
                   fabsf(c->axes[2].y) * c->half_extents.z;
    return Object_colliderCenter(obj, 0).y - extent;
}

static const char *bottleName(const Object *b) {
    static const char *const DRINK[] = { "Water", "Cola", "Orange soda", "Mate" };
    return b->type == OBJ_VODKA ? "Vodka" : b->type == OBJ_WATER ? DRINK[b->variant & 3] : "Beer";
}

void Object_showUmbrella(Object *u) {
    static const Vec3 COLORS[] = { { 0.15f, 0.20f, 0.42f }, { 0.70f, 0.14f, 0.16f }, { 0.20f, 0.32f, 0.22f } };
    int want = u->umbrella_flip > 0.0f || u->umbrella_ruined ? UMBRELLA_INSIDE_OUT : u->umbrella_open ? UMBRELLA_OPEN : UMBRELLA_FOLDED;
    if (u->type != OBJ_UMBRELLA || u->shown == want) return;
    Mesh_destroy(&u->mesh);
    u->mesh = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, want, COLORS[u->variant % 3]);
    u->shown = want;
}

void Object_setDrink(Object *b, int drink) {
    static const BottleStyle STYLE[] = { BOTTLE_WATER, BOTTLE_COLA, BOTTLE_ORANGE, BOTTLE_MATE };
    if (b->type != OBJ_WATER) return;
    drink &= 3;
    b->variant = drink;
    Mesh_destroy(&b->mesh);
    b->mesh = Shapes_bottle(2.0f * b->colliders[0].half_extents.x, 2.0f * b->colliders[0].half_extents.y, STYLE[drink]);
    b->quench  = drink == 0 ? 0.2f : 0.15f;
    b->refresh = drink == 0 ? 0.15f : drink == 3 ? 0.12f : 0.1f;  /* (sugar, or the mate's kick) */
    b->nourish = drink == 1 || drink == 2 ? 0.02f : 0.0f;
    b->wake    = drink == 3 ? 0.04f : drink == 1 ? 0.01f : 0.0f;   /* (caffeine: less sleepy) */
}

bool Object_isHard(const Object *o) {
    switch (o->type) {
    case OBJ_STONE: case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_HOUSE: case OBJ_BARN: case OBJ_STUMP: case OBJ_WALL:
    case OBJ_FIREPLACE: case OBJ_FURNITURE: case OBJ_BIN: case OBJ_SUPPLY: case OBJ_LADDER:
    case OBJ_AXE: case OBJ_CHAINSAW: case OBJ_BOTTLE: case OBJ_VODKA: case OBJ_WATER: case OBJ_CAN:
        return true;
    default:
        return false; /* grass, wood, people, bushes, bags: it just lands */
    }
}

void Object_describe(const Object *obj, char *out, size_t size) {
    const int kg = (int)(obj->weight + 0.5f);
    switch (obj->type) {
    case OBJ_BOTTLE:
    case OBJ_VODKA:
    case OBJ_WATER:
        if (obj->sips > 0) snprintf(out, size, "%s - %d sip%s left", bottleName(obj), obj->sips, obj->sips == 1 ? "" : "s");
        else snprintf(out, size, "%s - empty (back into a crate with it)", bottleName(obj));
        break;
    case OBJ_APPLE:
    case OBJ_BREAD:
    case OBJ_SAUSAGE: {
        const char *name = obj->type == OBJ_APPLE ? "apple" : obj->type == OBJ_BREAD ? "bread" : "sausage";
        const char *state = obj->burnt ? "Burnt " : obj->grilled ? "Grilled " : "";
        char what[48];
        snprintf(what, sizeof what, "%s%s", state, name);
        if (!*state) what[0] = (char)(what[0] - 'a' + 'A');
        char how[64] = "";
        if (obj->burnt) snprintf(how, sizeof how, " - charcoal");
        else if (obj->grill >= FOOD_DARK) snprintf(how, sizeof how, " - getting black, take it off!");
        else if (obj->grilled && obj->on_grill) snprintf(how, sizeof how, " - done, take it off");
        else if (obj->grill > 0.0f && !obj->grilled) snprintf(how, sizeof how, " - %d%% done", (int)(100.0f * obj->grill / FOOD_DONE));
        else if (obj->type == OBJ_SAUSAGE && !obj->grilled) snprintf(how, sizeof how, " - grill it: click the fire with it");
        snprintf(out, size, "%s - %d bite%s left%s", what, obj->sips, obj->sips == 1 ? "" : "s", how);
        break;
    }
    case OBJ_TRASH: {
        static const char *const WHAT[] = { [PALETTE_STANDARD] = "Plastic bottle", [PALETTE_GROUND] = "Broken board",
                                            [PALETTE_MODERN] = "Empty can", [PALETTE_WARM] = "Paper" };
        snprintf(out, size, "%s - trash", obj->name[0] ? obj->name : WHAT[obj->variant & 3]);
        break;
    }
    case OBJ_BAG:
        snprintf(out, size, obj->stored >= BAG_FULL ? "Garbage bag - full, take it to the container"
                 : obj->stored > 0 ? "Garbage bag - %d pieces in it" : "Garbage bag - click on trash to put it in", obj->stored);
        break;
    case OBJ_BIN: snprintf(out, size, "Waste container - throw trash and bags in"); break;
    case OBJ_FURNITURE: {
        static const char *const WHAT[FURN_COUNT] = {
            [FURN_WORKBENCH] = "Workbench", [FURN_TOOLWALL] = "Tool wall", [FURN_SHELF] = "Shelf",
            [FURN_STOVE] = "Stove - hold food over it to cook it", [FURN_COUNTER] = "Kitchen sink - click to wash",
            [FURN_TABLE] = "Table", [FURN_CHAIR] = "Chair - C to sit down", [FURN_BENCH] = "Log bench - C to sit down",
            [FURN_CAR] = "An old car, up for repairs", [FURN_TIRES] = "Old tyres",
            [FURN_TOILET] = "Toilet - P to use it", [FURN_TAP] = "Tap - the garden hose; fill the watering can here",
            [FURN_BAR] = "The bar",
        };
        if (obj->variant == FURN_ASHTRAY) { snprintf(out, size, "Ashtray (an old tin) - %d butts in it", obj->stored); break; }
        if (obj->variant == FURN_SMOKES) { snprintf(out, size, "Cigarette machine - click: a pack of %d", SMOKES_PACKS); break; }
        if (obj->variant == FURN_BENCH && obj->table) { snprintf(out, size, "Beer bench - C: over it and sit at the table (turned away: your back to it)"); break; }
        snprintf(out, size, "%s", obj->variant >= 0 && obj->variant < FURN_COUNT && WHAT[obj->variant] ? WHAT[obj->variant] : "Furniture");
        break;
    }
    case OBJ_HOSE: snprintf(out, size, "Garden hose - left click (hold) to spray: washes away puddles and dirt"); break;
    case OBJ_LADDER:
        if (obj->variant == 2) /* (step ladder) */
            snprintf(out, size, obj->leaning ? "Step ladder - left click: climb (W / S),  E / Q: fold it up"
                              : "Step ladder - left click: stand it up in front of you (at a wall: with its back to it)");
        else if (obj->variant == 1) /* (folding) */
            snprintf(out, size, obj->leaning ? "Folding ladder - left click: climb (W / S),  E / Q: take it down"
                              : obj->extend_goal > 0.5f ? "Folding ladder (unfolded) - left click where it should lean, look down: fold it"
                              : "Folding ladder (folded) - left click at a wall: unfold it and lean it there");
        else
            snprintf(out, size, obj->leaning ? "Ladder - left click: climb (W / S),  E / Q: take it down"
                              : obj->extend_goal > 0.5f ? "Ladder (slid out) - left click at a wall: lean it there, else slide it together"
                              : "Ladder (slid together) - left click: slide it out, then lean it against something");
        break;
    case OBJ_STONE: snprintf(out, size, kg >= 1 ? "A stone - %d kg" : "A pebble", kg); break;
    case OBJ_CAR:
        if (obj->variant == 1) { snprintf(out, size, "The ambulance"); break; }
        if (!obj->load || obj->load->n == 0) snprintf(out, size, "Volkram's car - the boot is empty");
        else if (!obj->boot_open) snprintf(out, size, "Volkram's car - full of your things");
        else snprintf(out, size, "Volkram's car - left click at the boot: take out %s", Car_nextName(obj));
        break;
    case OBJ_CRATE:
        { /* its own bottles, full and empty; and what doesn't belong in it (right click takes that out) */
            int full = 0, empty = 0, wrong = 0;
            for (int k = 0; k < CRATE_SLOTS; k++) {
                int kind = CRATE_KIND(obj->crate_slot[k]);
                if (!kind) continue;
                if (!Crate_belongs(obj, kind)) wrong++;
                else if (CRATE_SIPS(obj->crate_slot[k]) > 0) full++;
                else empty++;
            }
            char extra[64] = "";
            if (wrong > 0) snprintf(extra, sizeof extra, ", %d not %s%s", wrong, Crate_name(obj->variant), obj->is_held ? "" : " (right click: out)");
            if (empty > 0) snprintf(out, size, "Crate of %s - %d full, %d empt%s%s%s", Crate_name(obj->variant), full, empty, empty == 1 ? "y" : "ies", extra,
                                    obj->stored > obj->crate_empties && !obj->is_held ? " (left click: take one)" : "");
            else snprintf(out, size, "Crate of %s - %d bottle%s left%s%s", Crate_name(obj->variant), full, full == 1 ? "" : "s", extra,
                          obj->stored > 0 && !obj->is_held ? " (left click: take one)" : "");
        }
        break;
    case OBJ_TENT: {
        static const char *const STAGE[] = { "spread out - left click: put the poles in", "the poles going in - keep clicking",
                                             "the cover going over it - keep clicking", "pegging it down - keep clicking", "up" };
        const char *whose = obj->owner && obj->owner->type == OBJ_PLAYER ? "Your" : NULL;
        if (whose) snprintf(out, size, "Your tent - %s", STAGE[obj->tent_stage % 5]);
        else snprintf(out, size, "%s's tent - %s", obj->owner ? obj->owner->name : "Somebody", STAGE[obj->tent_stage % 5]);
        break;
    }
    case OBJ_STAGE: {
        int pct = (int)(Stage_part(obj) * 100.0f);
        if (obj->build_stage >= BUILD_DONE) snprintf(out, size, "The %s stage - built!", Cast_projectName((Project)obj->variant));
        else snprintf(out, size, "The %s stage - %s %d%% (hold left click to work on it)", Cast_projectName((Project)obj->variant), Stage_stepName(obj->build_stage), pct);
        break;
    }
    case OBJ_MATERIAL: {
        static const char *const WHAT[MATERIAL_KINDS] = { "A bundle of boards (8, cut to length, for a deck or a roof) - both hands", "A box of nails (for a deck)",
                                                          "A box of screws (for a roof)", "A bucket of paint" };
        snprintf(out, size, "%s - %d kg", WHAT[obj->variant % MATERIAL_KINDS], kg);
        break;
    }
    case OBJ_SCYTHE: snprintf(out, size, "Scythe - for the Kartoffelacker's grass (in the field: hold left click and walk)"); break;
    case OBJ_ROPE:   snprintf(out, size, "Throw line - up over a branch of the big tree, as high as you can (T: hold to wind up, look up, let go)"); break;
    case OBJ_SNAIL:
        snprintf(out, size, obj->variant ? "A vineyard snail - safe here" : "A vineyard snail (protected) - left click: into your bucket, let go at the wood by the Kartoffelacker");
        break;
    case OBJ_SCULPTURE: {
        static const char *const WHAT[] = { "an owl", "a bear", "a mushroom man" };
        snprintf(out, size, "Kerry's wood art - %s carved with the chainsaw", WHAT[obj->variant % 3]);
        break;
    }
    case OBJ_HAT: {
        const char *what = obj->variant == 2 ? "beanie" : "cap";
        if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s - pick it up to put it back on", what);
        else if (obj->owner) snprintf(out, size, "%s's %s (blown off)", obj->owner->name, what);
        else snprintf(out, size, "A %s", what);
        break;
    }
    case OBJ_UMBRELLA:
        snprintf(out, size, obj->umbrella_ruined ? "A broken umbrella - blown inside out, the ribs bent" : obj->umbrella_flip > 0.0f ? "Umbrella - blown inside out!" :
                            obj->umbrella_open ? "Umbrella - open" : "Umbrella (carried, it opens in the rain)");
        break;
    case OBJ_GEAR: {
        static const char *const WHAT[] = { "sleeping bag", "sleeping mat", "tent", "tent (the big one)" };
        if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s", WHAT[obj->variant & 3]);
        else snprintf(out, size, "%s's %s", obj->owner ? obj->owner->name : "Somebody", WHAT[obj->variant & 3]);
        break;
    }
    case OBJ_CLOTHES:
        if (obj->cloth_of && obj->cloth_of->type == OBJ_PLAYER) snprintf(out, size, "Your pullover - click: take it");
        else snprintf(out, size, "%s's pullover", obj->cloth_of ? obj->cloth_of->name : "Somebody");
        break;
    case OBJ_TOY:   snprintf(out, size, obj->variant == 0 ? "Alice's ball - throw it (T), she brings it back" : "Alice's rope toy - throw it (T), she loves to chew it"); break;
    case OBJ_CAN:
        if (obj->water > 0.05f) snprintf(out, size, "Watering can - %.0f l, left click (hold) to pour", obj->water);
        else snprintf(out, size, "Watering can - empty, fill it at the tap");
        break;
    case OBJ_FIREPLACE:
        if (obj->burning) snprintf(out, size, "Fire - %d kg of wood left", (int)(obj->fuel + 0.5f));
        else if (obj->fuel > 0.0f) snprintf(out, size, "Fireplace - %d kg of wood, click to light it", (int)(obj->fuel + 0.5f));
        else snprintf(out, size, "Fireplace - put firewood or branches in");
        break;
    case OBJ_AXE:      snprintf(out, size, "Axe - fells, cuts and splits wood"); break;
    case OBJ_SHEARS:   snprintf(out, size, "Pruning shears - cut branches off (or a burnt bit out of the hose)"); break;
    case OBJ_TAPE:     snprintf(out, size, "Duct tape - left click at a melted bit of the hose: tape it up"); break;
    case OBJ_CHAINSAW: snprintf(out, size, "Chainsaw - fells and cuts fast, %d kg", kg); break;
    case OBJ_LOG:
        snprintf(out, size, "Log %.1f m, %d kg%s", obj->size.y, kg,
                 obj->size.y <= 0.75f ? " - split it with the axe" : "");
        break;
    case OBJ_FIREWOOD:
        snprintf(out, size, "Firewood, %d kg%s", kg, obj->size.z < 4.0f ? " - can be split once more" : "");
        break;
    case OBJ_BRANCH: snprintf(out, size, "Branch"); break;
    case OBJ_WOODPILE: snprintf(out, size, "Woodpile - %d pieces of firewood", obj->stored); break;
    case OBJ_TREE:
        if (obj->variant == 2 && !obj->toppled) snprintf(out, size, "The big old tree - half dead, top broken off, leaning to the road. It must come down before it falls: Enrico's job");
        else if (obj->variant == 2) snprintf(out, size, "The big old tree, down - %d branch%s left", obj->branches, obj->branches == 1 ? "" : "es");
        else if (!obj->toppled && obj->variant) snprintf(out, size, "Apple tree - pick an apple");
        else if (!obj->toppled && obj->keep) snprintf(out, size, "Tree - this one stays (Kurti's rule)");
        else if (!obj->toppled) snprintf(out, size, "Tree - fell it with the axe or the chainsaw");
        else snprintf(out, size, "Fallen tree - %d branch%s left", obj->branches, obj->branches == 1 ? "" : "es");
        break;
    case OBJ_STUMP:
        if (obj->work > 0.0f) snprintf(out, size, "Tree stump - %d%% chopped out", (int)(100.0f * fminf(obj->work / STUMP_WORK, 1.0f)));
        else snprintf(out, size, "Tree stump - chop it out with the axe or the chainsaw");
        break;
    case OBJ_SUPPLY:
        if (obj->stock < 0) { snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - empty" : "Cool box - empty"); break; }
        if (obj->stock > 0) {
            snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Food crate - %d portions left (take an apple)" : "Cool box - %d sausages left (take one)", obj->stock);
            break;
        }
        snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - take an apple"
                 : obj->dispenses == OBJ_SAUSAGE ? "Cool box - take a sausage"
                 : obj->dispenses == OBJ_BAG ? "Garbage bags - take one"
                 : obj->dispenses == OBJ_URINELLA ? "Urinella box - fill your pocket (one-time use)" : "Water barrel - take a bottle");
        break;
    case OBJ_BOX:
    case OBJ_CYLINDER:
        snprintf(out, size, "%s, %d kg", obj->type == OBJ_BOX ? "Crate" : "Drum", kg);
        break;
    case OBJ_WALL:
        snprintf(out, size, "%s", obj->name[0] ? obj->name : "Wall");
        break;
    default:
        snprintf(out, size, "%d kg", kg);
        break;
    }
}

void Object_advanceTopple(Object *obj, float dt) {
    if (!obj->toppled || obj->topple >= 1.0f) return;
    bool tree = obj->type == OBJ_TREE;
    obj->topple = fminf(1.0f, obj->topple + dt / (tree ? TREE_FALL_TIME : BOTTLE_FALL_TIME));
    float t = obj->topple;
    obj->rotation.x = -(PI_F / 2.0f) * t * t; /* speeds up as it falls */
    if (tree) { /* the butt slides off the stump as it comes down, the trunk rests on the ground, not in it */
        float off = smoothstepf(0.55f, 1.0f, t);
        Vec3 at = vec3_add(obj->fall_origin, vec3_scale(obj->fall_dir, obj->fall_slide * off));
        obj->position = vec3(at.x, lerpf(obj->fall_base_y, obj->fall_ground_y, off) + obj->colliders[0].half_extents.x * smoothstepf(0.8f, 1.0f, t), at.z);
    }
    Object_syncColliders(obj);
}

void Object_useUp(Object *obj) {
    Mesh_destroy(&obj->mesh);
    obj->broken         = true;
    obj->is_static      = true;
    obj->is_held        = false;
    obj->collider_count = 0;
    obj->sips           = 0;
}

void Object_shatter(Object *obj, float ground_y) {
    if (!Object_isBottle(obj) || obj->broken) return;
    float diameter = 2.0f * obj->colliders[0].half_extents.x;
    unsigned seed = (unsigned)(obj->position.x * 997.0f) ^ ((unsigned)(obj->position.z * 991.0f) << 12);
    Mesh shards = Shapes_shards(diameter, seed);
    if (!shards.vao) return;

    Mesh_destroy(&obj->mesh);
    obj->mesh           = shards;
    obj->broken         = true;
    obj->is_static      = true;   /* no physics, can't be picked up */
    obj->collider_count = 0;
    obj->sips           = 0;
    obj->thrown_by      = NULL;
    obj->pivot          = vec3_zero();
    obj->rotation       = vec3(0.0f, obj->rotation.y, 0.0f);
    obj->position.y     = ground_y;
    obj->velocity       = vec3_zero();
}