Nimbin[12]?Games / Entropie / entropie/src/world/tools.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/tools.c 38.8 KB · 874 lines raw
#include "world/tools.h"
#include "world/object.h"

#define FELL_WORK        12.0f
#define BRANCH_WORK      1.5f   /* one branch */
#define CUT_WORK         8.0f
#define SPLIT_WORK       3.0f
#define FELL_REACH       0.75f  /* max gap between body and trunk */
#define DELIMB_REACH     0.8f
#define CUT_REACH        0.75f
#define MIN_PIECE        0.3f   /* logs aren't cut shorter than this */
#define ROUND_LENGTH     0.75f  /* logs this short are split instead of cut */
#define CUT_TOLERANCE    0.15f  /* strokes this close count for the same cut */
#define FRONT            0.3f   /* cos of the half angle a job must be in front */
#define NEARBY_REACH     0.6f   /* this much further (scaled), ... */
#define NEARBY_FRONT    -0.2f   /* ... and this far round to the side: still "at the work" (see Tools_jobNearby) */
#define BODY_HALF_DEPTH  0.15f
#define EYE_HEIGHT       1.6f   /* scaled */
#define LOOK_RANGE       2.0f   /* how far along the line of sight a stroke may aim */

/* Axe geometry (see Shapes_axe): the head sits this far below the top of the
   handle, the edge this far in front of the handle. */
#define AXE_HEAD_DROP    0.06f
#define AXE_EDGE         0.135f
#define AXE_REST_GRIP    0.24f  /* carried: the hand this far below the top, near the head */
#define AXE_END_GRIP     0.05f  /* working: the hand this far from the bottom */
#define AXE_HAND_GAP     0.12f  /* the second hand above the first */
#define READY_HOLD       1.5f   /* seconds the hands stay at the end of the handle after a stroke */
#define READY_RATE       3.5f   /* how fast the hands slide along the handle */
#define BITE_TIME        0.12f  /* the edge stays in the wood this long */
#define SAW_REACH        0.48f  /* chainsaw: from the grip to the tip of the bar */

typedef struct {
    float stroke;   /* seconds per stroke */
    float strike;   /* the tool hits at this time */
    float fell, delimb, cut, split;
    float height;   /* where a tree gets cut unless the worker looks elsewhere (scaled meters) */
    float low, high; /* range of cut heights (scaled) */
} ToolSpec;

static const ToolSpec SPECS[] = {
    [TOOL_NONE]     = { 0.0f,  0.0f,  0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  0.0f, 0.0f },
    [TOOL_HANDS]    = { 0.8f,  0.45f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f,  0.0f, 0.0f },
    [TOOL_AXE]      = { 1.15f, 0.6f,  1.0f, 1.0f, 1.0f, 1.0f, 0.95f, 0.45f, 1.35f },
    [TOOL_SHEARS]   = { 0.5f,  0.28f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,  0.0f, 0.0f },
    [TOOL_CHAINSAW] = { 0.8f,  0.3f,  3.0f, 1.0f, 2.0f, 0.0f, 0.45f, 0.3f, 0.9f },
};

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

#define AIM_UP_FROM 0.3f /* m over the eyes on the trunk: looking higher than that raises the felling cut */

static float workFor(ToolKind tool, Job job) {
    switch (job) {
    case JOB_FELL:   return SPECS[tool].fell;
    case JOB_DELIMB: return SPECS[tool].delimb;
    case JOB_CUT:    return SPECS[tool].cut;
    case JOB_SPLIT:  return SPECS[tool].split;
    case JOB_STUMP:  return SPECS[tool].fell > 0.0f ? SPECS[tool].cut : 0.0f; /* (what fells cuts the stump down) */
    case JOB_NONE:   break;
    }
    return 0.0f;
}

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

static Vec3 flat(Vec3 v) { return vec3(v.x, 0.0f, v.z); }

/* v without its part along axis, normalized (fallback if parallel). */
static Vec3 across(Vec3 v, Vec3 axis, Vec3 fallback) {
    Vec3 p = vec3_sub(v, vec3_scale(axis, vec3_dot(v, axis)));
    if (vec3_length(p) < 1e-4f) p = vec3_sub(fallback, vec3_scale(axis, vec3_dot(fallback, axis)));
    return vec3_normalize(p);
}

/* ---------- What is in hand ---------- */

ToolKind Tools_held(const Object *who, int *hand) {
    const Hands *h = &who->hands;
    if (hand) *hand = -1;
    if (h->grabbed) return TOOL_NONE;
    if (h->item[0] && h->item[0] == h->item[1])
        return h->item[0]->type == OBJ_CHAINSAW && h->state[0] == HAND_HOLDING ? TOOL_CHAINSAW : TOOL_NONE;
    for (int k = 0; k < 2; k++) {
        int i = k == 0 ? who->main_hand : 1 - who->main_hand;
        const Object *item = h->item[i];
        if (h->state[i] != HAND_HOLDING || !item) continue;
        if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_CHAINSAW) {
            if (hand) *hand = i;
            return item->type == OBJ_AXE ? TOOL_AXE : item->type == OBJ_SHEARS ? TOOL_SHEARS : TOOL_CHAINSAW;
        }
    }
    bool free = h->state[0] == HAND_FREE && h->state[1] == HAND_FREE;
    return free ? TOOL_HANDS : TOOL_NONE;
}

bool Tools_isWorking(const Object *who) {
    return who->work_state.time > 0.0f;
}

void Tools_logEnds(const Object *log, Vec3 *a, Vec3 *b) {
    const Collider *c = &log->colliders[0];
    Vec3 center = Object_colliderCenter(log, 0);
    Vec3 half = vec3_scale(c->axes[1], c->half_extents.y);
    *a = vec3_sub(center, half);
    *b = vec3_add(center, half);
}

static float logLength(const Object *o) { return 2.0f * o->colliders[0].half_extents.y; }

bool Tools_splittable(const Object *o) {
    if (o->is_held || o->collider_count == 0) return false;
    if (o->type == OBJ_FIREWOOD) return o->size.z < 4.0f;
    return o->type == OBJ_LOG && logLength(o) <= ROUND_LENGTH;
}

bool Tools_cuttable(const Object *o) {
    return o->type == OBJ_LOG && !o->is_held && o->collider_count > 0 && logLength(o) > ROUND_LENGTH;
}

float Tools_cutPoint(const Object *log, Vec3 near) {
    Vec3 a, b;
    Tools_logEnds(log, &a, &b);
    float length = logLength(log);
    float piece = 0.6f; /* splittable, and not too heavy */
    if (length - piece < MIN_PIECE) return length * 0.5f;
    bool from_a = vec3_length(vec3_sub(near, a)) <= vec3_length(vec3_sub(near, b));
    return from_a ? piece : length - piece;
}

/* ---------- Finding a job ---------- */

/* Point of collider i closest to p. */
static Vec3 closestOn(const Object *o, int i, Vec3 p) {
    const Collider *c = &o->colliders[i];
    Vec3 center = Object_colliderCenter(o, i);
    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;
}

static float extentUp(const Object *o) {
    const Collider *c = &o->colliders[0];
    return 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;
}

typedef struct {
    Job     job;
    Object *target;
    Vec3    point;
    float   cut_at;
    float   gap;
} Plan;

static float gapTo(const Object *who, Vec3 point) {
    return vec3_length(flat(vec3_sub(point, who->position))) - BODY_HALF_DEPTH;
}

/* How strict planning is: the real thing, or "is there something to work on nearby at all". */
static float g_extra_reach = 0.0f;
static float g_front = FRONT;

static void consider(Plan *best, const Object *who, Job job, Object *target, Vec3 point, float cut_at, float reach) {
    float gap = gapTo(who, point);
    if (gap > reach + g_extra_reach || gap >= best->gap) return;
    Vec3 to = flat(vec3_sub(point, who->position));
    if (vec3_length(to) > 1e-3f && vec3_dot(vec3_normalize(to), Object_facing(who)) <= g_front) return;
    *best = (Plan){ .job = job, .target = target, .point = point, .cut_at = cut_at, .gap = gap };
}

static bool hasAim(const Object *who) {
    Vec3 a = who->work_state.aim;
    return vec3_length(a) > 0.5f;
}

static Vec3 eyeOf(const Object *who) {
    return vec3_add(who->position, vec3(0.0f, EYE_HEIGHT * scaleOf(who), 0.0f));
}

/* Height above the feet where the line of sight passes a point at this horizontal distance. */
static float lookHeight(const Object *who, float distance, float fallback) {
    if (!hasAim(who)) return fallback;
    Vec3 aim = who->work_state.aim;
    float horizontal = sqrtf(aim.x * aim.x + aim.z * aim.z);
    if (horizontal < 0.15f) return fallback;
    return EYE_HEIGHT * scaleOf(who) + aim.y * distance / horizontal;
}

/* Where the line of sight meets the height y, or a point in front. */
static Vec3 lookAtHeight(const Object *who, float y) {
    const float s = scaleOf(who);
    Vec3 front = vec3_add(who->position, vec3_scale(Object_facing(who), 0.55f * s));
    if (!hasAim(who)) return front;
    Vec3 aim = who->work_state.aim, eye = eyeOf(who);
    if (aim.y > -0.05f) return front;
    float t = (y - eye.y) / aim.y;
    if (t < 0.0f || t > LOOK_RANGE * s / fmaxf(sqrtf(aim.x * aim.x + aim.z * aim.z), 0.05f)) return front;
    return vec3_add(eye, vec3_scale(aim, t));
}

static Plan planFor(const Object *who, ToolKind tool, Object *const *objects, int count) {
    const float s = scaleOf(who);
    const Vec3 chest = vec3_add(who->position, vec3(0.0f, 1.0f * s, 0.0f));
    const ToolSpec *spec = &SPECS[tool];
    Plan best = { .job = JOB_NONE, .gap = 1e9f };

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

        if (o->type == OBJ_TREE && !o->toppled && (!o->keep || who->type == OBJ_PLAYER) && workFor(tool, JOB_FELL) > 0.0f) { /* (you may fell any tree - some cost dear: forestry.c) */
            /* The cut goes in where the worker looks, within the tool's range */
            Vec3 c = Object_colliderCenter(o, 0);
            float distance = vec3_length(flat(vec3_sub(c, who->position)));
            /* About hip high. Looking lower: right where you look. Looking straight ahead isn't aiming
               up the trunk; looking up past AIM_UP_FROM over the eyes, the cut goes up that much */
            float look = lookHeight(who, distance, spec->height * s), hip = spec->height * s, from = (EYE_HEIGHT + AIM_UP_FROM) * s;
            float h = clampf(look <= hip ? look : look >= from ? hip + (look - from) : hip, spec->low * s, spec->high * s);
            float top = o->position.y + 2.0f * o->colliders[0].half_extents.y - 0.3f;
            float y = who->position.y + h;
            /* A cut already started: keep hitting the same notch, whoever swings, as long as it's in reach */
            float notch = o->position.y + o->cut_at - who->position.y;
            if (o->work > 0.0f && notch >= spec->low * s - 0.05f && notch <= spec->high * s + 0.05f) y = o->position.y + o->cut_at;
            Vec3 at = vec3(who->position.x, clampf(y, o->position.y + 0.1f, top), who->position.z);
            consider(&best, who, JOB_FELL, o, closestOn(o, 0, at), 0.0f, FELL_REACH);
        } else if (o->type == OBJ_TREE && o->toppled && o->topple >= 1.0f && workFor(tool, JOB_DELIMB) > 0.0f) {
            for (int c = 1; c < o->collider_count; c++) /* the crown */
                consider(&best, who, JOB_DELIMB, o, closestOn(o, c, chest), 0.0f, DELIMB_REACH);
        } else if (o->type == OBJ_STUMP && workFor(tool, JOB_STUMP) > 0.0f) {
            /* Chopped down from above, into the middle */
            Vec3 c = Object_colliderCenter(o, 0);
            consider(&best, who, JOB_STUMP, o, vec3(c.x, c.y + o->colliders[0].half_extents.y, c.z), 0.0f, CUT_REACH);
        } else if (o->type == OBJ_LOG || o->type == OBJ_FIREWOOD) {
            Vec3 a, b;
            Tools_logEnds(o, &a, &b);
            Vec3 axis = vec3_sub(b, a);
            float length = vec3_length(axis);
            axis = vec3_scale(axis, 1.0f / fmaxf(length, 1e-4f));
            float top = Object_colliderCenter(o, 0).y + extentUp(o);
            Vec3 aim = lookAtHeight(who, top);
            bool short_piece = o->type == OBJ_FIREWOOD || length <= ROUND_LENGTH;
            if (short_piece) {
                /* Split along its length, from above */
                if (o->type == OBJ_FIREWOOD && o->size.z >= 4.0f) continue; /* quarters: done */
                if (workFor(tool, JOB_SPLIT) <= 0.0f) continue;
                Vec3 middle = vec3_add(a, vec3_scale(axis, length * 0.5f));
                consider(&best, who, JOB_SPLIT, o, vec3(middle.x, top, middle.z), 0.0f, CUT_REACH);
            } else {
                if (workFor(tool, JOB_CUT) <= 0.0f) continue;
                float t = clampf(vec3_dot(vec3_sub(aim, a), axis), MIN_PIECE, length - MIN_PIECE);
                if (o->work > 0.0f && fabsf(o->cut_at - t) < 0.5f) t = o->cut_at; /* same cut as before */
                Vec3 on_axis = vec3_add(a, vec3_scale(axis, t));
                consider(&best, who, JOB_CUT, o, vec3(on_axis.x, top, on_axis.z), t, CUT_REACH);
            }
        }
    }
    return best;
}

bool Tools_hasJob(const Object *who, Object *const *objects, int count) {
    ToolKind tool = Tools_held(who, NULL);
    return tool != TOOL_NONE && planFor(who, tool, objects, count).job != JOB_NONE;
}

bool Tools_jobNearby(const Object *who, Object *const *objects, int count) {
    ToolKind tool = Tools_held(who, NULL);
    if (tool == TOOL_NONE || tool == TOOL_HANDS) return false;
    g_extra_reach = NEARBY_REACH * scaleOf(who);
    g_front = NEARBY_FRONT;
    bool near = planFor(who, tool, objects, count).job != JOB_NONE;
    g_extra_reach = 0.0f;
    g_front = FRONT;
    return near;
}

Object *Tools_target(const Object *who, Object *const *objects, int count) {
    ToolKind tool = Tools_held(who, NULL);
    return tool != TOOL_NONE ? planFor(who, tool, objects, count).target : NULL;
}

bool Tools_use(Object *who, Object *const *objects, int count) {
    Work *w = &who->work_state;
    if (w->time > 0.0f || who->hands.drink_hand >= 0) return false;
    int hand;
    ToolKind tool = Tools_held(who, &hand);
    if (tool == TOOL_NONE) return false;
    if (tool == TOOL_CHAINSAW && hand >= 0 && who->hands.state[1 - hand] != HAND_FREE) return false; /* needs both hands */

    Plan plan = planFor(who, tool, objects, count);
    if (tool == TOOL_HANDS && plan.job == JOB_NONE) return false; /* nothing to pull at */

    const float s = scaleOf(who);
    if (plan.job != JOB_NONE) {
        w->last_job = plan.job;
        w->last_target = plan.target;
        w->since = 0.0f;
    }
    w->time   = 1e-4f;
    w->tool   = tool;
    w->hand   = hand;
    w->job    = plan.job;
    w->target = plan.target;
    w->cut_at = plan.cut_at;
    w->point  = plan.job != JOB_NONE ? plan.point
              : Figure_toWorld(who->position, Object_facing(who), vec3(0.0f, 1.0f * s, 0.75f * s)); /* air */
    return true;
}

/* ---------- Doing the work ---------- */

static bool stillThere(const Work *w) {
    const Object *o = w->target;
    if (!o) return false;
    switch (w->job) {
    case JOB_FELL:   return o->type == OBJ_TREE && !o->toppled;
    case JOB_DELIMB: return o->type == OBJ_TREE && o->toppled && o->topple >= 1.0f;
    case JOB_CUT:    return o->type == OBJ_LOG && !o->is_held;
    case JOB_SPLIT:  return (o->type == OBJ_LOG || (o->type == OBJ_FIREWOOD && o->size.z < 4.0f)) && !o->is_held;
    case JOB_STUMP:  return o->type == OBJ_STUMP && !o->broken;
    case JOB_NONE:   break;
    }
    return false;
}

static void finish(Work *w, float at) {
    w->jobs_done++;
    if (w->done_count < WORK_DONE_MAX)
        w->done[w->done_count++] = (JobDone){ .job = w->job, .target = w->target, .at = at, .point = w->point };
}

float Tools_fellDone(const Object *tree) { return clampf(tree->work / FELL_WORK, 0.0f, 1.0f); }

static void strike(Work *w) {
    float amount = workFor(w->tool, w->job) * w->power; /* how well the worker is doing */
    if (amount <= 0.0f || !stillThere(w)) return;
    Object *o = w->target;
    w->impact = 0.18f;
    w->struck++;
    switch (w->job) {
    case JOB_FELL:
        /* the tree comes apart where it's hit: a new notch elsewhere starts over
           (never felled at a spot hit only once) */
        if (o->work > 0.0f && fabsf(w->point.y - o->position.y - o->cut_at) > CUT_TOLERANCE) o->work = 0.0f;
        o->cut_at = w->point.y - o->position.y;
        if ((o->work += amount) >= FELL_WORK) {
            o->work = 0.0f;
            Object_markCut(o, w->point, 0.0f);
            finish(w, o->cut_at);
        } else {
            Object_markCut(o, w->point, o->work / FELL_WORK);
        }
        break;
    case JOB_DELIMB:
        /* One branch at a time; strokes of everyone working on it add up */
        o->work += amount;
        while (o->work >= BRANCH_WORK) {
            o->work -= BRANCH_WORK;
            finish(w, 0.0f);
        }
        break;
    case JOB_CUT:
        /* A stroke somewhere else starts a new cut */
        if (fabsf(o->cut_at - w->cut_at) > CUT_TOLERANCE) o->work = 0.0f;
        o->cut_at = w->cut_at;
        if ((o->work += amount) >= CUT_WORK) {
            o->work = 0.0f;
            Object_markCut(o, w->point, 0.0f);
            finish(w, w->cut_at);
        } else {
            Object_markCut(o, w->point, o->work / CUT_WORK);
        }
        break;
    case JOB_SPLIT:
        if ((o->work += amount) >= SPLIT_WORK) {
            o->work = 0.0f;
            finish(w, 0.0f);
        }
        break;
    case JOB_STUMP:
        if ((o->work += amount) >= STUMP_WORK) finish(w, 0.0f);
        Object_markCut(o, w->point, fminf(o->work / STUMP_WORK, 1.0f));
        break;
    case JOB_NONE:
        break;
    }
}

bool Tools_takeDone(Object *who, JobDone *done) {
    Work *w = &who->work_state;
    if (w->done_count == 0) return false;
    *done = w->done[0];
    for (int i = 1; i < w->done_count; i++) w->done[i - 1] = w->done[i];
    w->done_count--;
    return true;
}

bool Tools_canDo(ToolKind tool, Job job) {
    return workFor(tool, job) > 0.0f;
}

/* ---------- Moving the arms ---------- */

/* Smooth path carry -> wind-up -> strike -> follow-through -> carry. */
static Vec3 strokePath(float t, const ToolSpec *spec, Vec3 carry, Vec3 windup, Vec3 hit, Vec3 follow) {
    float wind = spec->strike * 0.7f, after = spec->strike + 0.12f;
    if (t < wind) return vec3_lerp(carry, windup, smoothstepf(0.0f, wind, t));
    if (t < spec->strike) return vec3_lerp(windup, hit, smoothstepf(wind, spec->strike, t));
    if (t < after) return vec3_lerp(hit, follow, smoothstepf(spec->strike, after, t));
    return vec3_lerp(follow, carry, smoothstepf(after, spec->stroke, t));
}

static void reach(Body *b, int hand, Vec3 world) {
    b->hand_active[hand] = true;
    b->hand_target[hand] = world;
}

/* ----- The axe ----- */

/* How the axe is held, in figure space: where the (lower) hand is, where the
   handle points (towards the head) and the edge faces, and where along the
   handle the hand grips it. */
typedef struct {
    Vec3  hand;
    Vec3  handle;
    Vec3  blade;
    float grip;
    float contact;  /* 0..1: the edge sits at edge */
    Vec3  edge;
} AxePose;

static AxePose blendPose(AxePose a, AxePose b, float k) {
    AxePose p = a;
    p.hand   = vec3_lerp(a.hand, b.hand, k);
    p.handle = vec3_normalize(vec3_lerp(a.handle, b.handle, k));
    p.blade  = across(vec3_lerp(a.blade, b.blade, k), p.handle, b.blade);
    p.grip   = lerpf(a.grip, b.grip, k);
    p.contact = 0.0f;
    return p;
}

/* A swing turns the axe on an arc: around the vertical for a horizontal
   swing at a trunk, around the shoulders' axis for a stroke from above. */
typedef struct {
    bool  overhead;
    Vec3  forward;     /* overhead: horizontal direction of the stroke */
    float sign;        /* side of the hand at the end of the handle */
} Arc;

/* Handle and edge direction at an angle on the arc. The edge always faces
   the way the axe moves. */
static void arcAt(const Arc *arc, float angle, Vec3 *handle, Vec3 *blade) {
    if (arc->overhead) {
        /* angle 0: handle forward, > 0 raised; moving down */
        *handle = vec3_add(vec3_scale(arc->forward, cosf(angle)), vec3_scale(UP, sinf(angle)));
        *blade  = vec3_sub(vec3_scale(arc->forward, sinf(angle)), vec3_scale(UP, cosf(angle)));
    } else {
        /* angle 0: handle forward, > 0 towards the right; a right-hander swings from right to left */
        *handle = vec3(sinf(angle), 0.0f, cosf(angle));
        *blade  = vec3_scale(vec3(cosf(angle), 0.0f, -sinf(angle)), -arc->sign);
    }
}

/* Hand position that puts the edge at e for this arc angle. */
static Vec3 handFor(const Arc *arc, float angle, Vec3 e, float reach_a) {
    Vec3 u, v;
    arcAt(arc, angle, &u, &v);
    return vec3_sub(vec3_sub(e, vec3_scale(u, reach_a)), vec3_scale(v, AXE_EDGE));
}

/* Arc angle at the moment of the hit: the hands end up comfortably in front. */
static float hitAngle(const Arc *arc, Vec3 e, float reach_a, float s) {
    float best = 0.0f, best_cost = 1e9f;
    float from = arc->overhead ? -1.45f : -1.4f, to = arc->overhead ? 0.5f : 1.4f;
    for (float a = from; a <= to; a += 0.04f) {
        Vec3 h = handFor(arc, a, e, reach_a);
        float cost;
        if (arc->overhead) {
            float ahead = vec3_dot(flat(h), arc->forward);
            cost = 0.5f * (h.y - 0.75f * s) * (h.y - 0.75f * s) + 1.5f * (ahead - 0.32f * s) * (ahead - 0.32f * s)
                 + 20.0f * fmaxf(0.0f, 0.18f * s - ahead) * fmaxf(0.0f, 0.18f * s - ahead)
                 + 20.0f * fmaxf(0.0f, 0.35f * s - h.y) * fmaxf(0.0f, 0.35f * s - h.y);
        } else {
            cost = (h.x - arc->sign * 0.06f * s) * (h.x - arc->sign * 0.06f * s)
                 + 1.5f * (h.z - 0.32f * s) * (h.z - 0.32f * s)
                 + 20.0f * fmaxf(0.0f, 0.2f * s - h.z) * fmaxf(0.0f, 0.2f * s - h.z);
        }
        if (cost < best_cost) {
            best_cost = cost;
            best = a;
        }
    }
    return best;
}

static AxePose arcPose(const Arc *arc, float angle, Vec3 hand, float grip) {
    AxePose p = { .hand = hand, .grip = grip };
    arcAt(arc, angle, &p.handle, &p.blade);
    return p;
}

/* Wind up, swing, bite into the wood and come back to where the stroke began. */
static AxePose axeStroke(const Object *who, const ToolSpec *spec, AxePose start, float sign,
                         float length, float s) {
    const Work *w = &who->work_state;
    const float t = w->time, strike_at = spec->strike;
    const float end_grip = -length * 0.5f + AXE_END_GRIP;
    const float reach_a = length - AXE_HEAD_DROP - AXE_END_GRIP; /* hand to head, along the handle */
    Vec3 e = Figure_toLocal(who->position, Object_facing(who), w->point);

    Arc arc = { .sign = sign };
    arc.overhead = w->job == JOB_CUT || w->job == JOB_SPLIT || w->job == JOB_STUMP;
    arc.forward = vec3_length(flat(e)) > 1e-3f ? vec3_normalize(flat(e)) : vec3(0.0f, 0.0f, 1.0f);
    if (!arc.overhead) e.y = clampf(e.y, 0.45f * s, 1.4f * s); /* hands at the height of the cut */

    float hit = hitAngle(&arc, e, reach_a, s);
    float wind = arc.overhead ? 2.4f : hit + sign * 2.0f;
    float follow = arc.overhead ? hit - 0.6f : hit - sign * 0.9f;
    Vec3 hit_hand = handFor(&arc, hit, e, reach_a);
    Vec3 wind_hand = arc.overhead ? vec3(sign * 0.05f * s, 1.72f * s, 0.0f)
                                  : vec3(sign * 0.3f * s, e.y + 0.1f * s, 0.06f * s);
    AxePose windup = arcPose(&arc, wind, wind_hand, end_grip);
    AxePose struck = arcPose(&arc, hit, hit_hand, end_grip);
    bool air = w->job == JOB_NONE;

    const float wind_end = strike_at * 0.62f, bite_end = strike_at + BITE_TIME;
    if (t < wind_end) return blendPose(start, windup, smoothstepf(0.0f, wind_end, t));
    if (t < strike_at) {
        /* Speeding up into the hit */
        float k = (t - wind_end) / (strike_at - wind_end);
        k *= k;
        AxePose p = arcPose(&arc, lerpf(wind, hit, k), vec3_lerp(wind_hand, hit_hand, k), end_grip);
        if (!air && k > 0.8f) {
            p.contact = (k - 0.8f) / 0.2f;
            p.edge = e;
        }
        return p;
    }
    if (air) {
        /* Nothing there: the axe swings on through */
        float k = smoothstepf(strike_at, bite_end, t);
        Vec3 through = handFor(&arc, follow, e, reach_a);
        AxePose p = arcPose(&arc, lerpf(hit, follow, k), vec3_lerp(hit_hand, through, k), end_grip);
        if (t < bite_end) return p;
        AxePose done = arcPose(&arc, follow, through, end_grip);
        return blendPose(done, start, smoothstepf(bite_end, spec->stroke, t));
    }
    if (t < bite_end) {
        /* The edge is stuck in the wood; the hands jolt back a little */
        AxePose p = struck;
        float k = (t - strike_at) / BITE_TIME;
        p.hand = vec3_sub(p.hand, vec3_scale(p.handle, 0.015f * s * sinf(PI_F * k)));
        p.contact = 1.0f;
        p.edge = e;
        return p;
    }
    /* Pulled straight out of the cut, turned away from the wood, then back to the start */
    const float out_end = bite_end + 0.07f, away_end = out_end + 0.14f;
    Vec3 out_hand = vec3_sub(hit_hand, vec3_scale(struck.handle, 0.22f * s));
    if (t < out_end) {
        AxePose p = struck;
        p.hand = vec3_lerp(hit_hand, out_hand, smoothstepf(bite_end, out_end, t));
        p.contact = 1.0f - smoothstepf(bite_end, bite_end + 0.04f, t);
        p.edge = e;
        return p;
    }
    float back = arc.overhead ? hit + 1.2f : hit + sign * 1.6f;
    Vec3 near_hand = arc.overhead ? vec3(sign * 0.05f * s, 1.0f * s, 0.2f * s)
                                  : vec3(sign * 0.22f * s, e.y, 0.18f * s);
    AxePose pulled = arcPose(&arc, back, near_hand, end_grip);
    if (t < away_end) {
        float k = smoothstepf(out_end, away_end, t);
        return arcPose(&arc, lerpf(hit, back, k), vec3_lerp(out_hand, near_hand, k), end_grip);
    }
    return blendPose(pulled, start, smoothstepf(away_end, spec->stroke, t));
}

/* Carried near the head, or ready at the end of the handle; strokes start
   and end in the ready pose. */
static void holdAxe(Object *who, int hand, float dt) {
    Work *w = &who->work_state;
    Body *b = &who->body;
    const Object *axe = who->hands.item[hand];
    const float s = scaleOf(who), sign = hand == HAND_LEFT ? -1.0f : 1.0f;
    const float length = axe->size.y, half = length * 0.5f;
    const Vec3 pos = who->position, facing = Object_facing(who);

    bool ready_goal = w->time > 0.0f || w->since < READY_HOLD;
    w->ready = clampf(w->ready + (ready_goal ? 1.0f : -1.0f) * READY_RATE * dt, 0.0f, 1.0f);

    AxePose rest = { .hand = vec3(sign * 0.24f * s, 0.80f * s, 0.10f * s),
                     .handle = vec3_normalize(vec3(sign * 0.15f, 1.0f, 0.55f)),
                     .grip = half - AXE_REST_GRIP };
    rest.blade = across(vec3(0.0f, 0.0f, 1.0f), rest.handle, UP);
    /* Ready: across the body, head up by the other shoulder, clear of what's in front */
    AxePose ready = { .hand = vec3(sign * 0.15f * s, 0.92f * s, 0.2f * s),
                      .handle = vec3_normalize(vec3(-sign * 0.6f, 1.0f, -0.05f)),
                      .grip = -half + AXE_END_GRIP };
    ready.blade = across(vec3(-sign, 0.0f, 0.0f), ready.handle, UP);
    AxePose pose = blendPose(rest, ready, smoothstepf(0.0f, 1.0f, w->ready));

    if (w->time > 0.0f) {
        const ToolSpec *spec = &SPECS[TOOL_AXE];
        pose = axeStroke(who, spec, pose, sign, length, s);
        float effort = smoothstepf(0.0f, spec->strike, w->time) *
                       (1.0f - smoothstepf(spec->strike + 0.1f, spec->stroke, w->time));
        b->crouch_goal = fmaxf(b->crouch_goal, clampf((0.95f * s - pose.hand.y) / (0.75f * s), 0.0f, 1.0f));
        b->lean_goal = fmaxf(b->lean_goal, 0.25f * effort);
        b->chin_goal = -0.1f * effort;
    }

    reach(b, hand, Figure_toWorld(pos, facing, pose.hand));
    int other = 1 - hand;
    if (who->hands.state[other] == HAND_FREE && (w->ready > 0.25f || w->time > 0.0f))
        reach(b, other, Figure_toWorld(pos, facing, vec3_add(pose.hand, vec3_scale(pose.handle, AXE_HAND_GAP))));

    w->handle  = Figure_toWorld(vec3_zero(), facing, pose.handle);
    w->blade   = Figure_toWorld(vec3_zero(), facing, pose.blade);
    w->grip    = pose.grip;
    w->contact = pose.contact;
    w->edge    = Figure_toWorld(pos, facing, pose.edge);
}

/* ----- Other tools ----- */

static void useShears(Object *who, const ToolSpec *spec, Vec3 hit) {
    Work *w = &who->work_state;
    Body *b = &who->body;
    const float s = scaleOf(who), t = w->time;
    const Vec3 pos = who->position, facing = Object_facing(who);
    int i = w->hand;
    float sign = i == HAND_LEFT ? -1.0f : 1.0f;
    Vec3 carry = Figure_toLocal(pos, facing, b->hand_target[i]);
    Vec3 approach = vec3_add(hit, vec3(0.0f, 0.05f * s, -0.15f * s));
    reach(b, i, Figure_toWorld(pos, facing, strokePath(t, spec, carry, approach, hit, hit)));
    int o = 1 - i; /* the other hand holds the branch */
    if (who->hands.state[o] == HAND_FREE)
        reach(b, o, Figure_toWorld(pos, facing, vec3_add(hit, vec3(-sign * 0.12f * s, 0.05f * s, 0.0f))));
}

/* Both hands on the saw: the bar goes into the wood, saws a moment, comes back. */
static void useChainsaw(Object *who, const ToolSpec *spec) {
    Work *w = &who->work_state;
    Body *b = &who->body;
    const float s = scaleOf(who), t = w->time;
    const Vec3 pos = who->position, facing = Object_facing(who);
    Vec3 e = Figure_toLocal(pos, facing, w->point);
    Vec3 forward = vec3_length(flat(e)) > 1e-3f ? vec3_normalize(flat(e)) : vec3(0.0f, 0.0f, 1.0f);
    bool low = w->job == JOB_CUT || w->job == JOB_STUMP;
    Vec3 dir = low ? vec3_normalize(vec3_sub(vec3_scale(forward, 0.8f), vec3_scale(UP, 0.6f))) : forward;

    float in = smoothstepf(0.0f, spec->strike, t) * (1.0f - smoothstepf(spec->stroke * 0.8f, spec->stroke, t));
    float thickness = w->target ? 2.0f * w->target->colliders[0].half_extents.x : 0.2f;
    Vec3 tip = vec3_add(e, vec3_scale(low ? vec3_scale(UP, -1.0f) : forward, in * 0.7f * thickness));
    Vec3 grip = vec3_sub(tip, vec3_scale(dir, SAW_REACH));
    grip.z = fmaxf(grip.z, 0.15f * s);
    float buzz = sinf(t * 90.0f) * 0.006f * s;
    for (int i = 0; i < 2; i++) {
        /* The main hand on the rear handle, the other one on the front grip */
        float sign = i == HAND_LEFT ? -1.0f : 1.0f;
        Vec3 carry = who->skeleton.joints[i == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R];
        Vec3 at = vec3_add(grip, vec3(sign * 0.09f * s, buzz, 0.0f));
        reach(b, i, Figure_toWorld(pos, facing, vec3_lerp(carry, at, smoothstepf(0.0f, 1.0f, in * 1.5f))));
    }
    float hands_y = grip.y;
    b->crouch_goal = fmaxf(b->crouch_goal, clampf((0.95f * s - hands_y) / (0.75f * s), 0.0f, 1.0f) * in);
    b->lean_goal = fmaxf(b->lean_goal, 0.3f * in);
    w->saw_dir = Figure_toWorld(vec3_zero(), facing, dir);
    w->saw_in = in;
    if (in > 0.6f && w->job != JOB_NONE) w->impact = fmaxf(w->impact, 0.05f); /* the saw shakes */
}

/* Grab the branches and tear them off towards you. */
static void useHands(Object *who, const ToolSpec *spec, Vec3 hit) {
    Body *b = &who->body;
    const float s = scaleOf(who), t = who->work_state.time;
    const Vec3 pos = who->position, facing = Object_facing(who);
    for (int i = 0; i < 2; i++) {
        float sign = i == HAND_LEFT ? -1.0f : 1.0f;
        Vec3 carry = vec3(sign * 0.22f * s, 0.85f * s, 0.15f * s);
        Vec3 grab = vec3_add(hit, vec3(sign * 0.08f * s, 0.0f, 0.0f));
        Vec3 pulled = vec3(sign * 0.15f * s, hit.y + 0.15f * s, 0.22f * s);
        reach(b, i, Figure_toWorld(pos, facing, strokePath(t, spec, carry, grab, grab, pulled)));
    }
    float effort = smoothstepf(0.0f, spec->strike, t);
    b->lean_goal = 0.3f * effort - 0.45f * smoothstepf(spec->strike, spec->stroke, t); /* lean back to pull */
}

void Tools_update(Object *who, float dt) {
    Work *w = &who->work_state;
    w->power = who->work_rate > 0.0f ? who->work_rate : 1.0f;
    w->since += dt;
    w->impact = fmaxf(0.0f, w->impact - dt);
    w->contact = 0.0f;
    w->saw_in = 0.0f;
    int hand;
    ToolKind held = Tools_held(who, &hand);
    if (w->time > 0.0f && held != w->tool) w->time = 0.0f; /* dropped the tool */

    if (w->time > 0.0f) {
        const ToolSpec *spec = &SPECS[w->tool];
        float before = w->time;
        w->time += dt * Object_vigor(who) * (who->tempo > 0.0f ? who->tempo : 1.0f); /* tired: slower strokes; and everyone's own pace */
        if (before < spec->strike && w->time >= spec->strike) strike(w);
        if (w->time >= spec->stroke) w->time = 0.0f;
    }

    if (held == TOOL_AXE) {
        holdAxe(who, hand, dt);
        return;
    }
    w->ready = 0.0f;
    if (w->time <= 0.0f) return;

    const ToolSpec *spec = &SPECS[w->tool];
    Body *b = &who->body;
    const float s = scaleOf(who);
    /* Where the stroke lands, within reach of the arms */
    Vec3 hit = Figure_toLocal(who->position, Object_facing(who), w->point);
    hit.x = clampf(hit.x, -0.35f * s, 0.35f * s);
    hit.z = clampf(hit.z, 0.3f * s, 0.62f * s);
    hit.y = fmaxf(hit.y, 0.12f * s);
    float effort = smoothstepf(0.0f, spec->strike, w->time) * (1.0f - smoothstepf(spec->strike + 0.1f, spec->stroke, w->time));
    b->crouch_goal = fmaxf(b->crouch_goal, clampf((0.95f * s - hit.y) / (0.75f * s), 0.0f, 1.0f) * effort);
    b->lean_goal = fmaxf(b->lean_goal, 0.3f * effort);

    switch (w->tool) {
    case TOOL_SHEARS:   useShears(who, spec, hit); break;
    case TOOL_CHAINSAW: useChainsaw(who, spec);    break;
    case TOOL_HANDS:    useHands(who, spec, hit);  break;
    case TOOL_AXE:
    case TOOL_NONE:     break;
    }
}

/* ---------- Tools in the hand ---------- */

static Vec3 jointWorld(const Object *who, int joint) {
    return Figure_toWorld(who->position, Object_facing(who), who->skeleton.joints[joint]);
}

/* Puts an item with its local Y along y_axis and local Z along z_axis, so
   that the local point anchor (relative to the mesh origin) is at world. */
static void poseItem(Object *item, Vec3 y_axis, Vec3 z_axis, Vec3 anchor, Vec3 world) {
    item->rotation = euler_yxz_from_axes(y_axis, z_axis);
    Object_syncColliders(item);
    Vec3 origin = vec3_sub(world, mat4_mul_dir(mat4_rotate_yxz(item->rotation), anchor));
    item->position = vec3_sub(origin, item->pivot);
    item->velocity = vec3_zero();
}

/* Turns the axe around the hand (k = 0..1 of the way) so the edge lands on point;
   the hand slides a little along the handle if the arms fall short. */
static void edgeTo(Vec3 grip_at, Vec3 point, float half, float k, Vec3 *u, Vec3 *v, float *g) {
    Vec3 n = vec3_cross(*u, *v);
    Vec3 e = vec3_sub(point, grip_at);
    e = vec3_sub(e, vec3_scale(n, vec3_dot(e, n)));
    float d = vec3_length(e);
    if (d < AXE_EDGE + 0.05f) return;
    float a = sqrtf(d * d - AXE_EDGE * AXE_EDGE);
    Vec3 along = vec3_scale(e, 1.0f / d);
    Vec3 side = across(*v, along, n);
    Vec3 u_hit = vec3_scale(vec3_sub(vec3_scale(along, a), vec3_scale(side, AXE_EDGE)), 1.0f / d);
    Vec3 v_hit = vec3_scale(vec3_add(vec3_scale(along, AXE_EDGE), vec3_scale(side, a)), 1.0f / d);
    float g_hit = clampf(half - AXE_HEAD_DROP - a, -half + 0.02f, half - 0.15f);
    *u = vec3_normalize(vec3_lerp(*u, u_hit, k));
    *v = across(vec3_lerp(*v, v_hit, k), *u, v_hit);
    *g = lerpf(*g, g_hit, k);
}

/* Inside collider i of o? Then *out is the nearest point on its surface. */
static bool insideOf(const Object *o, int i, Vec3 p, Vec3 *out) {
    const Collider *c = &o->colliders[i];
    Vec3 rel = vec3_sub(p, Object_colliderCenter(o, i));
    const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
    int axis = -1;
    float least = 1e9f, d[3];
    for (int k = 0; k < 3; k++) {
        d[k] = vec3_dot(rel, c->axes[k]);
        float room = half[k] - fabsf(d[k]);
        if (room <= 0.0f) return false;
        if (room < least) { least = room; axis = k; }
    }
    *out = vec3_add(p, vec3_scale(c->axes[axis], d[axis] >= 0.0f ? least : -least));
    return true;
}

/* The axe: handle through the hand. In the wood the edge sits exactly where it
   bit, and it never passes through what is being worked on. */
static void placeAxe(Object *who, int hand, Object *item) {
    const Work *w = &who->work_state;
    Vec3 grip_at = jointWorld(who, hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R);
    const float half = item->size.y * 0.5f;
    Vec3 u = vec3_length(w->handle) > 0.5f ? w->handle : UP;
    Vec3 v = across(w->blade, u, Object_facing(who));
    float g = w->grip;

    if (w->contact > 0.0f) edgeTo(grip_at, w->edge, half, w->contact, &u, &v, &g);

    const Object *wood = w->target;
    if (w->time > 0.0f && wood && wood->collider_count > 0 && w->job != JOB_DELIMB) {
        Vec3 edge = vec3_add(grip_at, vec3_add(vec3_scale(u, half - AXE_HEAD_DROP - g), vec3_scale(v, AXE_EDGE)));
        Vec3 surface;
        if (insideOf(wood, 0, edge, &surface)) edgeTo(grip_at, surface, half, 1.0f, &u, &v, &g);
    }
    poseItem(item, u, v, vec3(0.0f, g, 0.0f), grip_at);
}

static void placeShears(Object *who, int hand, Object *item) {
    const Work *w = &who->work_state;
    Vec3 wrist = jointWorld(who, hand == HAND_LEFT ? JOINT_WRIST_L : JOINT_WRIST_R);
    Vec3 palm  = jointWorld(who, hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R);
    const float half = item->colliders[0].half_extents.y;
    Vec3 forearm = vec3_normalize(vec3_sub(palm, wrist));
    Vec3 aim = w->time > 0.0f ? vec3_normalize(vec3_sub(w->point, palm)) : Object_facing(who);
    Vec3 handle = w->time > 0.0f ? vec3_normalize(vec3_lerp(forearm, aim, 0.6f)) : forearm;
    poseItem(item, handle, across(aim, handle, UP), vec3(0.0f, -(half - 0.1f), 0.0f), palm);
}

/* Where the mesh origin goes and how it turns, for an item posed by axes and an anchor point. */
static void poseOf(Vec3 y_axis, Vec3 z_axis, Vec3 anchor, Vec3 world, Vec3 *rotation, Vec3 *origin) {
    *rotation = euler_yxz_from_axes(y_axis, z_axis);
    *origin = vec3_sub(world, mat4_mul_dir(mat4_rotate_yxz(*rotation), anchor));
}

/* Chainsaw: carried relaxed in one hand by the top handle, the bar forward and
   a little down; while sawing both hands hold it and the bar goes into the wood. */
static void placeChainsaw(Object *who, int hand, Object *item) {
    const Work *w = &who->work_state;
    const Vec3 facing = Object_facing(who);
    Vec3 palm = jointWorld(who, hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R);
    Vec3 bar = vec3_normalize(vec3_sub(facing, vec3_scale(UP, 0.35f)));
    Vec3 carried_rot, carried_at;
    poseOf(across(UP, bar, facing), vec3_scale(bar, -1.0f), vec3(0.0f, 0.15f, 0.24f), palm, &carried_rot, &carried_at);

    float k = smoothstepf(0.0f, 1.0f, clampf(w->saw_in * 1.5f, 0.0f, 1.0f));
    Vec3 rotation = carried_rot, origin = carried_at;
    if (k > 0.0f && vec3_length(w->saw_dir) > 0.5f) {
        Vec3 grip_at = vec3_scale(vec3_add(jointWorld(who, JOINT_HAND_L), jointWorld(who, JOINT_HAND_R)), 0.5f);
        Vec3 dir = w->saw_dir, working_rot, working_at;
        poseOf(across(UP, dir, facing), vec3_scale(dir, -1.0f), vec3(0.0f, 0.05f, 0.1f), grip_at, &working_rot, &working_at);
        Mat4 a = mat4_rotate_yxz(carried_rot), b = mat4_rotate_yxz(working_rot);
        Vec3 y = vec3_normalize(vec3_lerp(mat4_mul_dir(a, UP), mat4_mul_dir(b, UP), k));
        Vec3 z = across(vec3_lerp(mat4_mul_dir(a, vec3(0.0f, 0.0f, 1.0f)), mat4_mul_dir(b, vec3(0.0f, 0.0f, 1.0f)), k), y, facing);
        rotation = euler_yxz_from_axes(y, z);
        origin = vec3_lerp(carried_at, working_at, k);
    }
    item->rotation = rotation;
    Object_syncColliders(item);
    item->position = vec3_sub(origin, item->pivot);
    item->velocity = vec3_zero();
}

bool Tools_placeHeld(Object *who, int hand, Object *item) {
    const Hands *h = &who->hands;
    switch (item->type) {
    case OBJ_AXE:
        if (Tools_held(who, NULL) != TOOL_AXE) return false;
        placeAxe(who, hand, item);
        return true;
    case OBJ_SHEARS:
        placeShears(who, hand, item);
        return true;
    case OBJ_CHAINSAW:
        if (h->item[0] == item && h->item[1] == item) return false; /* an old two-handed carry */
        placeChainsaw(who, hand, item);
        return true;
    default:
        return false;
    }
}