Entropie git · main
Entropie festival game; first steps
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.gzentropie/src/people/hands.c raw
#include "people/hands.h"
#include "world/object.h"
#include "world/ladder.h"
#include "world/physics.h"
#define UMBRELLA_HAND_OUT 0.16f /* an open umbrella: the hand this far out to its side ... */
#define UMBRELLA_HAND_UP 1.16f /* ... this high ... */
#define UMBRELLA_HAND_AHEAD 0.17f /* ... and in front (all at 1.75 m tall) */
#define UMBRELLA_TOP 2.05f /* the shaft points at this high over the head's middle */
#define ONE_HAND_MAX_WEIGHT 10.0f
#define ONE_HAND_MAX_SIZE 0.6f
#define TWO_HAND_MAX_WEIGHT 35.0f
#define TWO_HAND_MAX_SIZE 1.1f
#define REACH_TIME 0.45f /* seconds to bend down and take an item */
#define PICKUP_GAP 0.9f /* max gap between body and item */
#define GRAB_GAP 0.6f
#define FRONT_CONE 0.35f /* cos of the half angle in front of the character */
#define BODY_HALF_DEPTH 0.15f
#define GRIP_BREAK_DISTANCE 1.0f /* pushed/pulled object got stuck too far away */
#define GRAB_HAND_SPACING 0.17f
#define CARRY_PALM_GAP 0.04f /* palms just outside the item's sides */
#define CARRY_CHEST_GAP 0.18f /* item distance from the chest (scaled) */
#define CARRY_GRIP_DEPTH 0.12f /* palms this far behind the item's back face */
#define DRINK_TIME 1.6f /* raise, tip, swallow, lower */
#define SIP_AT 0.85f
#define REFERENCE_WEIGHT 70.0f /* bottle strength is given for this body weight */
#define DRINK_TILT 1.9f /* radians the bottle tips back */
#define THROW_TIME 0.35f /* arm swings forward */
#define THROW_SPEED 11.0f /* m/s for something light */
#define FULL_WIND 0.8f /* seconds winding up for the hardest throw */
#define READY_OUT 0.55f /* m (scaled) out from the shoulder: a hand reaching out (E / Q) ... */
#define CATCH_NEAR 0.22f /* ... takes hold of what's this near to it */
#define OVERHAND_AFTER 0.3f /* wound up this long (about), the arm goes back over the shoulder */
#define OVERHAND_WEIGHT 3.0f /* kg: heavier things go underhand */
#define SETTLE_TIME 0.45f /* a taken item turns into the carry pose */
#define BODY_HALF_WIDTH 0.17f /* carried things stay outside this box around the spine */
#define BODY_CLEARANCE 0.11f /* ... half its depth */
#define LIFT_CAPACITY 40.0f /* kg an average person lifts with both hands */
#define LIFT_HEIGHT 0.6f /* a team carries things this high off the ground */
#define LIFT_SPEED 1.5f /* m/s up or down */
#define LEVEL_RATE 3.0f /* a carried log tilts towards the carriers' heights */
#define GRIP_MISS 0.08f /* a hand this far from its grip doesn't hold */
#define REGRAB_TIME 0.4f /* ... for this long: take hold again */
static const Vec3 UP = { 0.0f, 1.0f, 0.0f };
/* ---------- Geometry helpers (main collider) ---------- */
static float extentAlong(const Object *o, Vec3 dir) {
const Collider *c = &o->colliders[0];
return fabsf(vec3_dot(c->axes[0], dir)) * c->half_extents.x +
fabsf(vec3_dot(c->axes[1], dir)) * c->half_extents.y +
fabsf(vec3_dot(c->axes[2], dir)) * c->half_extents.z;
}
static float largestSize(const Object *o) {
Vec3 h = o->colliders[0].half_extents;
return 2.0f * fmaxf(h.x, fmaxf(h.y, h.z));
}
static Vec3 itemCenter(const Object *o) { return Object_colliderCenter(o, 0); }
/* Point of the main collider closest to p (on its surface when p is outside). */
static Vec3 closestPoint(const Object *o, Vec3 p) {
const Collider *c = &o->colliders[0];
Vec3 center = itemCenter(o);
Vec3 rel = vec3_sub(p, center);
const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
Vec3 q = center;
for (int k = 0; k < 3; k++)
q = vec3_add(q, vec3_scale(c->axes[k], clampf(vec3_dot(rel, c->axes[k]), -half[k], half[k])));
return q;
}
/* Horizontal distance from a body to the object's surface. */
static float gapAt(Vec3 position, float height, const Object *o, Vec3 *contact) {
Vec3 chest = vec3_add(position, vec3(0.0f, height * 0.55f, 0.0f));
Vec3 q = closestPoint(o, chest);
if (contact) *contact = q;
return vec3_length(vec3(q.x - position.x, 0.0f, q.z - position.z)) - BODY_HALF_DEPTH;
}
static float gapTo(const Object *who, const Object *o, Vec3 *contact) {
return gapAt(who->position, who->look.height, o, contact);
}
float Hands_gapFrom(Vec3 position, float height, const Object *o) {
return gapAt(position, height, o, NULL);
}
static Vec3 rightOf(Vec3 facing) { return vec3_normalize(vec3_cross(facing, UP)); }
static float scaleOf(const Object *who) { return who->look.height / 1.75f; }
static bool liftableOneHand(const Object *o) {
bool tool = o->type == OBJ_AXE || o->type == OBJ_SHEARS || o->type == OBJ_CHAINSAW || o->type == OBJ_LADDER || o->type == OBJ_UMBRELLA || o->type == OBJ_SCYTHE; /* long, but made for one hand */
return o->weight <= ONE_HAND_MAX_WEIGHT && (tool || largestSize(o) <= ONE_HAND_MAX_SIZE);
}
static bool liftableTwoHands(const Object *o) {
return o->weight <= TWO_HAND_MAX_WEIGHT && largestSize(o) <= TWO_HAND_MAX_SIZE;
}
/* Nearest loose object in front of the character within the given gap. */
static Object *findInFront(const Object *who, Object *const *objects, int count, float max_gap) {
Vec3 facing = Object_facing(who);
Object *best = NULL;
float best_gap = max_gap;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o == who || o->is_static || o->is_character || o->is_held || o->type == OBJ_DOG) continue;
Vec3 contact;
float gap = gapTo(who, o, &contact);
Vec3 d = vec3_sub(contact, who->position);
d.y = 0.0f;
float dist = vec3_length(d);
Vec3 dir = dist > 1e-3f ? vec3_scale(d, 1.0f / dist) : facing;
if (dist > 1e-3f && vec3_dot(dir, facing) < FRONT_CONE) continue;
if (gap < best_gap) {
best_gap = gap;
best = o;
}
}
return best;
}
/* How much weight a character can move by pushing or pulling. */
static float pushCapacity(const Object *who) {
/* About 240 kg for an average person; heavier people push a bit more */
return 240.0f * (who->strength / 100.0f) * sqrtf(fmaxf(who->weight, 1.0f) / REFERENCE_WEIGHT) * Object_vigor(who);
}
/* Share of the team's effort that actually moves the object (0..1).
Below a quarter of its weight the object does not budge. */
static float teamShare(float capacity, float weight) {
if (capacity < weight * 0.25f) return 0.0f;
return capacity / (capacity + weight);
}
/* Object space: relative to the pivot, before rotation (turns with the object). */
static Vec3 toObject(const Object *o, Vec3 world) {
Vec3 rel = vec3_sub(world, vec3_add(o->position, o->pivot));
return mat4_mul_dir_transposed(mat4_rotate_yxz(o->rotation), rel);
}
static Vec3 fromObject(const Object *o, Vec3 local) {
return vec3_add(vec3_add(o->position, o->pivot), mat4_mul_dir(mat4_rotate_yxz(o->rotation), local));
}
static float yawFacing(Vec3 dir) {
return atan2f(dir.z, dir.x) + PI_F / 2.0f; /* inverse of Object_facing */
}
static bool twoHanded(const Hands *h) {
return h->item[HAND_LEFT] && h->item[HAND_LEFT] == h->item[HAND_RIGHT];
}
/* ---------- Actions ---------- */
void Hands_drop(Object *who, int hand) {
Hands *h = &who->hands;
Object *item = h->item[hand];
if (!item) { if (h->state[hand] == HAND_READY || h->state[hand] == HAND_HOLD) Hands_letGo(who, hand); return; }
if (h->drink_hand == hand) h->drink_hand = -1;
for (int i = 0; i < 2; i++) {
if (h->item[i] == item) {
h->item[i] = NULL;
h->state[i] = HAND_FREE;
}
}
item->is_held = false;
item->velocity = vec3(who->velocity.x, 0.0f, who->velocity.z);
if (item->type == OBJ_CRATE) { /* (set down standing, not tipped over) */
item->rotation = vec3(0.0f, who->rotation.y, 0.0f);
Object_syncColliders(item);
}
if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER) { /* put down flat, where it was */
Vec3 center = Object_colliderCenter(item, 0);
item->rotation = item->type == OBJ_LADDER ? vec3(PI_F / 2.0f, who->rotation.y, 0.0f)
: vec3(0.0f, who->rotation.y - PI_F / 2.0f, PI_F / 2.0f); /* (a tool on its side: the blade flat, not up) */
Object_syncColliders(item);
item->position = vec3_sub(center, item->colliders[0].offset);
}
}
void Hands_setAside(Object *who, Object *out[2]) {
Hands *h = &who->hands;
out[0] = out[1] = NULL;
if (h->grabbed) Hands_release(who);
Vec3 f = Object_facing(who), side = vec3(-f.z, 0.0f, f.x);
for (int i = 0; i < 2; i++) {
Object *item = h->item[i];
if (h->state[i] != HAND_HOLDING || !item) continue;
if (i == HAND_RIGHT && h->item[HAND_LEFT] == item) continue; /* (two-handed: once) */
Hands_drop(who, i);
out[i] = item;
if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER) continue; /* (laid flat) */
item->rotation = vec3(0.0f, who->rotation.y, 0.0f); /* upright on the ground, a step to the side */
Object_syncColliders(item);
float out_by = (i == HAND_RIGHT ? 1.0f : -1.0f) * (0.35f + item->colliders[0].half_extents.x);
Vec3 spot = vec3_add(who->position, vec3_add(vec3_scale(side, out_by), vec3_scale(f, 0.15f)));
spot.y += item->colliders[0].half_extents.y + 0.03f;
item->position = vec3_sub(spot, item->colliders[0].offset);
item->velocity = vec3_zero();
Object_syncColliders(item);
}
}
void Hands_touch(Object *who, Vec3 point) {
who->hands.touching = true;
who->hands.touch_point = point;
}
void Hands_holdEdge(Object *who, Vec3 point, Vec3 across, float crouch) {
who->hands.on_edge = true;
who->hands.edge_crouch = crouch;
who->hands.edge_point = point;
who->hands.edge_across = across;
}
Object *Hands_inFront(const Object *who, Object *const *objects, int count) {
return findInFront(who, objects, count, PICKUP_GAP);
}
int Hands_freeHand(const Object *who) {
const Hands *h = &who->hands;
int main = who->main_hand;
return h->state[main] == HAND_FREE ? main : h->state[1 - main] == HAND_FREE ? 1 - main : -1;
}
/* The hand that throws: the main one if it holds something, else the other. */
static int throwHand(const Object *who) {
const Hands *h = &who->hands;
int main = who->main_hand;
return h->state[main] == HAND_HOLDING ? main : h->state[1 - main] == HAND_HOLDING ? 1 - main : -1;
}
/* Wound up this long, it goes overhand (sooner for the lively); light things only. */
static bool goesOverhand(const Object *who, const Object *item) {
return who->hands.wind >= OVERHAND_AFTER * (1.2f - 0.5f * who->temperament.energy) &&
item->weight < OVERHAND_WEIGHT && !twoHanded(&who->hands);
}
void Hands_windUp(Object *who) {
if (throwHand(who) < 0) { who->hands.wind = 0.0f; return; }
who->hands.winding = true;
if (who->hands.wind <= 0.0f) who->hands.wind = 1e-3f;
}
bool Hands_isWindingUp(const Object *who) {
return who->hands.wind > 0.0f;
}
bool Hands_throw(Object *who, Vec3 dir) {
Hands *h = &who->hands;
int hand = throwHand(who);
if (hand < 0) { h->wind = 0.0f; return false; }
Object *item = h->item[hand];
bool both = twoHanded(h), over = goesOverhand(who, item);
float power = clampf(h->wind / FULL_WIND, 0.0f, 1.0f);
h->wind = 0.0f;
Hands_drop(who, hand);
/* Overhand goes harder and flatter; underhand is a lob. Heavier things don't fly as fast */
float speed = THROW_SPEED * (over ? 0.9f + 0.45f * power : 0.65f + 0.25f * power);
speed = clampf(speed - item->weight * 0.35f, 2.5f, speed);
dir = vec3_normalize(vec3_add(vec3_normalize(dir), vec3(0.0f, over ? 0.1f : 0.22f, 0.0f)));
item->velocity = vec3_add(vec3_scale(dir, speed), vec3(who->velocity.x, 0.0f, who->velocity.z));
item->thrown_by = who;
item->thrown_time = 0.0f;
for (int i = 0; i < 2; i++)
if (i == hand || both) {
h->throw_time[i] = THROW_TIME;
h->overhand[i] = over;
}
return true;
}
/* Starts reaching for item with this hand, or both hands if it is too big. */
static bool startPickUp(Object *who, int hand, Object *item) {
Hands *h = &who->hands;
int other = 1 - hand;
if (liftableOneHand(item)) {
h->state[hand] = HAND_REACHING;
h->item[hand] = item;
h->timer[hand] = 0.0f;
return true;
}
if (liftableTwoHands(item) && h->state[other] != HAND_HOLDING && h->state[other] != HAND_REACHING) {
if (h->state[other] == HAND_GRABBING) Hands_release(who);
for (int i = 0; i < 2; i++) {
h->state[i] = HAND_REACHING;
h->item[i] = item;
h->timer[i] = 0.0f;
}
return true;
}
return false;
}
/* Put down near a table, a counter, a crate (something solid at about hand
height in front): set on top of it, not dropped to the floor. */
#define PLACE_LOWEST 0.3f /* m over the feet: a surface this high and up counts ... */
#define PLACE_HIGHEST 1.5f /* ... up to this */
#define PLACE_HEAVY 20.0f /* kg: loose things this heavy hold something put on them */
static float surfaceUnder(const Object *who, const Object *item, Object *const *objects, int count, Vec3 at, float from_y) {
float best = -1e9f;
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (o == who || o == item || o->is_character || o->is_held || o->collider_count == 0 || o->type == OBJ_DECAL ||
o->type == OBJ_PUDDLE || o->type == OBJ_STINK || o->type == OBJ_STRUCTURE || (!o->is_static && o->weight < PLACE_HEAVY)) continue;
if (fabsf(o->position.x - at.x) > 4.0f || fabsf(o->position.z - at.z) > 4.0f) continue;
for (int c = 0; c < o->collider_count; c++) {
if (fabsf(o->colliders[c].axes[1].y) < 0.95f) continue; /* (a level top only) */
float t = Object_rayToCollider(o, c, vec3(at.x, from_y, at.z), vec3(0.0f, -1.0f, 0.0f), NULL);
if (t >= 0.0f) best = fmaxf(best, from_y - t);
}
}
return best;
}
static bool placeOnSurface(Object *who, Object *item, Object *const *objects, int count) {
if (item->type == OBJ_LADDER || item->weight > 15.0f || item->collider_count == 0) return false;
const Vec3 f = Object_facing(who);
const float feet = who->position.y, from = feet + PLACE_HIGHEST + 0.3f;
const float r = fmaxf(item->colliders[0].half_extents.x, item->colliders[0].half_extents.z);
Vec3 tries[4] = { Object_colliderCenter(item, 0) };
for (int k = 1; k < 4; k++) tries[k] = vec3_add(who->position, vec3_scale(f, 0.25f + 0.2f * (float)k));
int pick = -1;
float pick_top = -1e9f;
for (int k = 0; k < 4; k++) { /* (the highest: a table over the chair in front of it) */
float top = surfaceUnder(who, item, objects, count, tries[k], from);
if (top < feet + PLACE_LOWEST || top > feet + PLACE_HIGHEST || top <= pick_top + 0.05f) continue;
/* room for all of it up there (its edges on the top too) */
bool room = true;
Vec3 side = vec3(-f.z, 0.0f, f.x);
for (int e = 0; e < 4 && room; e++) {
Vec3 d = vec3_scale(e < 2 ? f : side, (e % 2 ? -1.0f : 1.0f) * r * 0.8f);
room = fabsf(surfaceUnder(who, item, objects, count, vec3_add(tries[k], d), from) - top) < 0.03f;
}
if (room) { pick = k; pick_top = top; }
}
if (pick >= 0) {
if (item->type != OBJ_AXE && item->type != OBJ_SHEARS) item->rotation = vec3(0.0f, who->rotation.y, 0.0f); /* (upright; tools lie flat) */
Object_syncColliders(item);
float bottom = Object_bottom(item) - item->position.y;
item->position = vec3(tries[pick].x - item->colliders[0].offset.x, pick_top - bottom + 0.005f, tries[pick].z - item->colliders[0].offset.z);
item->velocity = vec3_zero();
Object_syncColliders(item);
return true;
}
return false;
}
bool Hands_pickUpOrDrop(Object *who, int hand, Object *const *objects, int count) {
Hands *h = &who->hands;
if (h->state[hand] == HAND_HOLDING || h->state[hand] == HAND_REACHING) {
Object *item = h->state[hand] == HAND_HOLDING ? h->item[hand] : NULL;
Hands_drop(who, hand);
if (item && !item->is_held && !placeOnSurface(who, item, objects, count) && Object_isBottle(item)) item->drop_tip = true;
return true;
}
if (h->state[hand] == HAND_GRABBING) Hands_release(who);
Object *item = findInFront(who, objects, count, PICKUP_GAP);
return item && startPickUp(who, hand, item);
}
int Hands_takeInFront(Object *who, Object *const *objects, int count) {
Hands *h = &who->hands;
if (h->grabbed) return 0;
Object *item = findInFront(who, objects, count, PICKUP_GAP);
if (!item) return 0;
if (liftableOneHand(item)) {
int hand = Hands_freeHand(who);
if (hand < 0) return -1;
return startPickUp(who, hand, item) ? 1 : 0;
}
if (liftableTwoHands(item)) {
if (h->state[HAND_LEFT] != HAND_FREE || h->state[HAND_RIGHT] != HAND_FREE) return -1;
return startPickUp(who, HAND_RIGHT, item) ? 1 : 0;
}
/* Too big to carry in the hands: take hold of it instead */
if (h->state[HAND_LEFT] != HAND_FREE && h->state[HAND_RIGHT] != HAND_FREE) return -1;
return Hands_grabObject(who, item) ? 2 : 0;
}
bool Hands_pickUpObject(Object *who, int hand, Object *item) {
const Hands *h = &who->hands;
if (h->state[hand] != HAND_FREE || h->grabbed) return false;
if (!item || item->is_static || item->is_character || item->is_held) return false;
if (gapTo(who, item, NULL) > PICKUP_GAP) return false;
return startPickUp(who, hand, item);
}
static void startHolding(Object *who, int hand);
bool Hands_receive(Object *who, int hand, Object *item) {
Hands *h = &who->hands;
if (h->state[hand] != HAND_FREE || h->grabbed || !item || item->is_held || !liftableOneHand(item)) return false;
h->state[hand] = HAND_REACHING;
h->item[hand] = item;
startHolding(who, hand);
return true;
}
bool Hands_grab(Object *who, Object *const *objects, int count) {
if (who->hands.grabbed) return false;
Object *target = findInFront(who, objects, count, GRAB_GAP);
return target && Hands_grabObject(who, target);
}
bool Hands_grabObject(Object *who, Object *target) {
Hands *h = &who->hands;
if (h->grabbed || !target || target->is_static || target->is_held) return false;
if (h->state[HAND_LEFT] != HAND_FREE && h->state[HAND_RIGHT] != HAND_FREE) return false;
/* Close enough? Touch the nearest point of the surface at a comfortable height. */
const float s = scaleOf(who);
Vec3 nearest;
if (gapTo(who, target, &nearest) > GRAB_GAP) return false;
Vec3 center = itemCenter(target);
float ext_y = extentAlong(target, UP);
float height = clampf(who->position.y + 1.0f * s, center.y - ext_y + 0.05f, center.y + ext_y - 0.05f);
Vec3 contact = closestPoint(target, vec3(nearest.x, height, nearest.z));
Vec3 toward = vec3_sub(who->position, contact); /* from the object towards us */
toward.y = 0.0f;
if (vec3_length(toward) < 1e-3f) toward = vec3_sub(who->position, center), toward.y = 0.0f;
if (vec3_length(toward) < 1e-3f) return false;
toward = vec3_normalize(toward);
Vec3 side = vec3_normalize(vec3_cross(UP, toward)); /* our right, seen facing the object */
bool both = h->state[HAND_LEFT] == HAND_FREE && h->state[HAND_RIGHT] == HAND_FREE;
float spacing = fminf(GRAB_HAND_SPACING, extentAlong(target, side));
for (int i = 0; i < 2; i++) {
if (h->state[i] != HAND_FREE) continue;
float offset = both ? (i == HAND_LEFT ? -spacing : spacing) : 0.0f;
Vec3 point = closestPoint(target, vec3_add(contact, vec3_scale(side, offset)));
h->grab_point[i] = toObject(target, point);
h->state[i] = HAND_GRABBING;
}
/* Face the object; from now on the pair keeps this layout in the world */
h->grabbed = target;
h->grab_heading = vec3_scale(toward, -1.0f);
h->grab_vector = vec3_sub(target->position, who->position);
h->grab_share = teamShare(pushCapacity(who), Object_dragWeight(target));
who->rotation.y = yawFacing(h->grab_heading);
Object_syncColliders(who);
return true;
}
#define MAX_TEAM 8 /* people holding one thing at most */
#define TURN_RATE 2.0f /* rad/s an easy thing turns (less for heavy or big ones) */
#define TURN_SOLID 40.0f /* kg: loose things lighter than this are pushed aside, not in the way */
static Vec3 turnVec(Vec3 v, float a) { /* about the vertical, a: the way the heading angle grows */
float c = cosf(a), s = sinf(a);
return vec3(v.x * c - v.z * s, v.y, v.x * s + v.z * c);
}
/* How deep the object is in solid things around it (the deepest). */
static float solidOverlap(const Object *o, Object *const *objects, int count) {
float most = 0.0f;
for (int i = 0; i < count; i++) {
const Object *b = objects[i];
if (b == o || b->is_character || b->is_held || b->collider_count == 0 || b->type == OBJ_BUSH) continue;
if (!b->is_static && b->weight < TURN_SOLID) continue;
if (fabsf(b->position.x - o->position.x) > 8.0f || fabsf(b->position.z - o->position.z) > 8.0f) {
if (!b->is_static || vec3_length(b->colliders[0].half_extents) < 4.0f) continue; /* (far: only big buildings matter) */
}
most = fmaxf(most, Physics_penetration(o, b));
}
return most;
}
float Hands_turnGrabbed(Object *who, Object *const *objects, int count, float want, float dt) {
Object *o = who->hands.grabbed;
if (!o || o->is_static || dt <= 0.0f || fabsf(want) < 1e-4f) return 0.0f;
/* Everybody holding it, and how strong they are together */
Object *team[MAX_TEAM];
int n = 0;
float capacity = 0.0f;
for (int k = 0; k < count && n < MAX_TEAM; k++)
if (objects[k]->is_character && objects[k]->hands.grabbed == o) {
team[n++] = objects[k];
capacity += pushCapacity(objects[k]);
}
float share = o->lifted ? 1.0f : teamShare(capacity, Object_dragWeight(o));
float reach = fmaxf(extentAlong(o, vec3(1.0f, 0.0f, 0.0f)), extentAlong(o, vec3(0.0f, 0.0f, 1.0f)));
float most = TURN_RATE * share / (0.5f + reach) * dt;
float a = clampf(want, -most, most);
if (fabsf(a) < 1e-5f) return 0.0f;
/* Turn it; if that takes it (deeper) into something solid, it doesn't turn */
float before = solidOverlap(o, objects, count);
o->rotation.y += a;
Object_syncColliders(o);
if (solidOverlap(o, objects, count) > before + 0.01f) {
o->rotation.y -= a;
Object_syncColliders(o);
return 0.0f;
}
/* Everybody holding it walks round with it, still facing it the same way */
for (int k = 0; k < n; k++) {
Hands *h = &team[k]->hands;
h->grab_vector = turnVec(h->grab_vector, a);
h->grab_heading = turnVec(h->grab_heading, a);
Vec3 at = vec3_sub(o->position, h->grab_vector);
team[k]->position.x = at.x;
team[k]->position.z = at.z;
team[k]->rotation.y = yawFacing(h->grab_heading);
Object_syncColliders(team[k]);
}
return a;
}
void Hands_release(Object *who) {
Hands *h = &who->hands;
for (int i = 0; i < 2; i++)
if (h->state[i] == HAND_GRABBING) h->state[i] = HAND_FREE;
h->grabbed = NULL;
}
bool Hands_isGrabbing(const Object *who) {
return who->hands.grabbed != NULL;
}
bool Hands_carries(const Object *who, int type) {
for (int i = 0; i < 2; i++)
if (who->hands.item[i] && who->hands.state[i] == HAND_HOLDING && (int)who->hands.item[i]->type == type) return true;
return false;
}
float Hands_moveFactor(const Object *who) {
const Hands *h = &who->hands;
if (h->grabbed && h->grabbed->lifted) return 0.6f;
if (h->grabbed) return 0.35f + 0.45f * h->grab_share;
if (twoHanded(h)) return 0.7f;
return 1.0f;
}
/* ---------- Drinking ---------- */
int Hands_bottleHand(const Object *who) {
const Hands *h = &who->hands;
for (int k = 0; k < 2; k++) {
int i = k == 0 ? who->main_hand : 1 - who->main_hand;
Object *item = h->item[i];
bool consumable = item && (Object_isBottle(item) || Object_isFood(item));
if (h->state[i] == HAND_HOLDING && consumable && item->sips > 0 && !twoHanded(h))
return i;
}
return -1;
}
bool Hands_isDrinking(const Object *who) {
return who->hands.drink_hand >= 0;
}
bool Hands_drink(Object *who) {
Hands *h = &who->hands;
if (h->drink_hand >= 0) return false;
int hand = Hands_bottleHand(who);
if (hand < 0) return false;
h->drink_hand = hand;
h->drink_time = 0.0f;
h->drink_sipped = false;
return true;
}
/* How far the bottle is tipped back (radians) and how much it is held at its
middle instead of hanging from the hand (0..1) at time t of a sip. */
static float drinkTilt(float t) {
return DRINK_TILT * smoothstepf(0.25f, 0.6f, t) * (1.0f - smoothstepf(1.15f, 1.5f, t));
}
/* Food isn't tipped back, a bottle is. */
static float tiltOf(const Object *item, float t) {
return item && Object_isFood(item) ? 0.0f : drinkTilt(t);
}
static float drinkGrip(float t) {
return smoothstepf(0.0f, 0.25f, t) * (1.0f - smoothstepf(1.35f, DRINK_TIME, t));
}
/* Where along a bottle the hand holds it, from its middle towards the top (m):
low around the body, or (rarely) at the neck. Sliding lower to drink. */
#define FIST_AT 0.6f /* from the wrist to the knuckles: where a held thing sits in the hand */
static float bottleGrip(const Object *who, int hand, const Object *bottle) {
const Hands *h = &who->hands;
float half = bottle->colliders[0].half_extents.y;
float hold = h->by_neck[hand] ? half - 0.035f : -0.45f * half;
if (h->drink_hand != hand) return hold;
return lerpf(hold, -0.2f * half, drinkGrip(h->drink_time));
}
/* Direction the bottle's opening points when tipped back by angle a. */
static Vec3 bottleTop(Vec3 facing, float a) {
return vec3_sub(vec3_scale(UP, cosf(a)), vec3_scale(facing, sinf(a)));
}
static Vec3 mouthOf(const Object *who) {
const float s = scaleOf(who);
const Skeleton *sk = &who->skeleton;
Vec3 local = vec3_add(sk->joints[JOINT_HEAD],
vec3_add(vec3_scale(sk->head_forward, 0.085f * s), vec3_scale(sk->head_up, -0.05f * s)));
return Figure_toWorld(who->position, Object_facing(who), local);
}
/* Hand target for a sip: carry pose, then bottle opening at the mouth. */
static Vec3 drinkTarget(const Object *who, Vec3 carry_target, float t) {
const int hand = who->hands.drink_hand;
const Object *bottle = who->hands.item[hand];
float half = bottle->colliders[0].half_extents.y; /* along the bottle, before tipping */
Vec3 top = bottleTop(Object_facing(who), tiltOf(bottle, t));
Vec3 at_mouth = Object_isFood(bottle)
? vec3_add(mouthOf(who), vec3_scale(Object_facing(who), half + 0.01f)) /* a bite */
: vec3_sub(mouthOf(who), vec3_scale(top, half - bottleGrip(who, hand, bottle) + 0.01f));
float k = smoothstepf(0.0f, 0.35f, t) * (1.0f - smoothstepf(1.3f, DRINK_TIME, t));
return vec3_lerp(carry_target, at_mouth, k);
}
static void advanceDrink(Object *who, float dt) {
Hands *h = &who->hands;
if (h->drink_hand < 0) return;
h->drink_time += dt;
Object *bottle = h->item[h->drink_hand];
if (!h->drink_sipped && h->drink_time >= SIP_AT) {
h->drink_sipped = true;
if (bottle && bottle->sips > 0) {
bottle->sips--;
/* Heavier and more hardened people get drunk slower */
float body = REFERENCE_WEIGHT / fmaxf(who->weight, 30.0f);
who->drunk = fminf(who->drunk + bottle->alcohol * body / fmaxf(who->tolerance, 0.2f), 1.5f);
Needs *n = &who->needs;
n->thirst = fmaxf(0.0f, n->thirst - bottle->quench);
n->hunger = fmaxf(0.0f, n->hunger - bottle->nourish);
n->stamina = fminf(1.0f, n->stamina + bottle->refresh);
n->sleepiness = fmaxf(0.0f, n->sleepiness - bottle->wake);
n->bladder = fminf(1.0f, n->bladder + (bottle->quench * 0.25f + bottle->alcohol * 0.3f) / Object_bladder(who));
}
}
if (h->drink_time >= DRINK_TIME) {
int hand = h->drink_hand;
h->drink_hand = -1;
if (bottle && Object_isFood(bottle) && bottle->sips <= 0) { /* all eaten */
Hands_drop(who, hand);
Object_useUp(bottle);
}
}
}
/* ---------- Per frame ---------- */
/* Crouch needed to bring the hands down to a world height. */
static float crouchFor(const Object *who, float world_y) {
const float s = scaleOf(who);
return clampf((0.95f * s - (world_y - who->position.y)) / (0.75f * s), 0.0f, 1.0f);
}
static void startHolding(Object *who, int hand) {
Hands *h = &who->hands;
Object *item = h->item[hand];
const Vec3 facing = Object_facing(who);
const Mat4 r = mat4_rotate_yxz(item->rotation);
for (int i = 0; i < 2; i++) {
if (h->item[i] != item) continue;
h->state[i] = HAND_HOLDING;
/* Tools are held the right way round; anything else as it was picked up */
bool tool = item->type == OBJ_CHAINSAW;
h->item_yaw[i] = tool ? 0.0f : item->rotation.y - who->rotation.y;
/* From the way it lay, it turns into the hand */
h->settle[i] = 0.0f;
/* Bottles: now and then (more so when drunk) grabbed by the neck */
float dice = fabsf(fmodf(sinf(who->body.clock * 12.9898f + item->position.x * 78.233f) * 43758.547f, 1.0f));
h->by_neck[i] = Object_isBottle(item) && dice < 0.12f + 0.35f * fminf(who->drunk, 1.0f);
h->settle_y[i] = Figure_toLocal(vec3_zero(), facing, mat4_mul_dir(r, UP));
h->settle_z[i] = Figure_toLocal(vec3_zero(), facing, mat4_mul_dir(r, vec3(0.0f, 0.0f, 1.0f)));
}
if (item->type == OBJ_CHAINSAW) item->rotation.x = item->rotation.z = 0.0f;
item->is_held = true;
if (item->type == OBJ_FIREWOOD) item->stored = 0; /* taken off the pile */
item->toppled = false;
item->thrown_by = NULL;
item->velocity = vec3_zero();
item->angular_velocity = vec3_zero();
}
/* What a character can lift with the hands that hold on. */
static float liftCapacity(const Object *who) {
int hands = 0;
for (int i = 0; i < 2; i++) hands += who->hands.state[i] == HAND_GRABBING;
return LIFT_CAPACITY * 0.5f * (float)hands * (who->strength / 100.0f) *
sqrtf(fmaxf(who->weight, 1.0f) / REFERENCE_WEIGHT) * Object_vigor(who);
}
#define END_LIFT_SHARE 0.5f /* one end up: that end's half of the weight */
#define END_DRAG 0.75f /* dragging the other end along: slower */
#define END_REACH 0.8f /* held this close to an end to lift it there */
/* Long things (logs): one end can be lifted, the other drags on the ground. */
static bool endLiftable(const Object *o) {
return o->type == OBJ_LOG && o->collider_count > 0 && o->colliders[0].half_extents.y >= 0.7f;
}
/* One end up at the holder's hip, the other on the ground behind: it pivots
on the end on the ground. Eased up (LIFT_SPEED). */
static void endLift(Object *o, Object *holder, float dt) {
const Collider *c = &o->colliders[0];
const float hl = c->half_extents.y, r = c->half_extents.x;
Vec3 center = itemCenter(o), axis = c->axes[1];
Vec3 e1 = vec3_add(center, vec3_scale(axis, hl)), e2 = vec3_sub(center, vec3_scale(axis, hl));
Vec3 hp = holder->position;
float d1 = vec3_length(vec3(e1.x - hp.x, 0.0f, e1.z - hp.z)), d2 = vec3_length(vec3(e2.x - hp.x, 0.0f, e2.z - hp.z));
Vec3 near = d1 < d2 ? e1 : e2, far = d1 < d2 ? e2 : e1;
far.y = hp.y + r; /* (on the ground: as level as where you stand) */
Vec3 flat = vec3(near.x - far.x, 0.0f, near.z - far.z);
if (vec3_length(flat) < 1e-3f) return;
flat = vec3_normalize(flat);
float goal = hp.y + LIFT_HEIGHT;
float y = near.y + clampf(goal - near.y, -LIFT_SPEED * dt, LIFT_SPEED * dt);
float dh = clampf(y - far.y, 0.0f, 1.8f * hl);
Vec3 u = vec3_normalize(vec3_add(vec3_scale(flat, sqrtf(fmaxf(4.0f * hl * hl - dh * dh, 1e-4f))), vec3(0.0f, dh, 0.0f)));
if (vec3_dot(u, axis) < 0.0f) u = vec3_scale(u, -1.0f); /* (keep its own way round) */
Vec3 z = c->axes[2];
z = vec3_sub(z, vec3_scale(u, vec3_dot(z, u)));
if (vec3_length(z) < 1e-3f) return;
Vec3 new_center = vec3_add(far, vec3_scale(vec3_dot(u, flat) >= 0.0f ? u : vec3_scale(u, -1.0f), hl));
o->rotation = euler_yxz_from_axes(u, vec3_normalize(z));
o->position = vec3_add(o->position, vec3_sub(new_center, center));
Object_syncColliders(o);
Vec3 shift = vec3_sub(new_center, center); /* (tilting it moved its middle: it's held there now) */
holder->hands.grab_vector = vec3_add(holder->hands.grab_vector, vec3(shift.x, 0.0f, shift.z));
}
static float teamLift(const Object *o, Object *const *objects, int count) {
float capacity = 0.0f;
for (int k = 0; k < count; k++)
if (objects[k]->is_character && objects[k]->hands.grabbed == o) capacity += liftCapacity(objects[k]);
return capacity;
}
int Hands_toggleLift(Object *who, Object *const *objects, int count) {
Object *o = who->hands.grabbed;
if (!o) return -1;
if (o->lifted) {
o->lifted = o->end_up = false; /* put it down: gravity takes over */
return 0;
}
float can = teamLift(o, objects, count);
if (can < o->weight) {
/* long: one end up, the other stays on the ground (half the weight) */
if (!endLiftable(o) || can < END_LIFT_SHARE * o->weight) return -1;
int holders = 0;
for (int k = 0; k < count; k++) holders += objects[k]->is_character && objects[k]->hands.grabbed == o;
if (holders != 1) return -1;
/* held near one end: the hands slide to that end, either side of it */
Hands *h = &who->hands;
const Collider *c = &o->colliders[0];
Vec3 center = itemCenter(o), grip = vec3_zero();
int n = 0;
for (int i = 0; i < 2; i++)
if (h->state[i] == HAND_GRABBING) { grip = vec3_add(grip, fromObject(o, h->grab_point[i])); n++; }
if (n == 0) return -1;
float along = vec3_dot(vec3_sub(vec3_scale(grip, 1.0f / (float)n), center), c->axes[1]);
if (c->half_extents.y - fabsf(along) > END_REACH) return -3;
Vec3 end = vec3_add(center, vec3_scale(c->axes[1], (along >= 0.0f ? 1.0f : -1.0f) * (c->half_extents.y - 0.12f)));
Vec3 right = rightOf(Object_facing(who));
for (int i = 0; i < 2; i++)
if (h->state[i] == HAND_GRABBING)
h->grab_point[i] = toObject(o, vec3_add(end, vec3_scale(right, (i == HAND_LEFT ? -1.0f : 1.0f) * c->half_extents.x * 0.9f)));
o->lifted = o->end_up = true;
o->velocity = vec3_zero();
return 2;
}
o->lifted = true;
o->end_up = false;
o->velocity = vec3_zero();
return 1;
}
/* Lowest point of an object's main collider. */
static float bottomOf(const Object *o) {
return itemCenter(o).y - extentAlong(o, UP);
}
/*
* A long thing carried by several people: each end goes up to the height of
* the people holding it there, so it's lifted evenly (and follows a slope).
* Tilts it a little at a time; returns the height its lowest point should go to.
*/
static float levelLift(Object *o, Object *const *team, int n, float dt) {
Vec3 center = itemCenter(o);
Vec3 axis = o->colliders[0].axes[1]; /* logs and trunks run along their local Y */
Vec3 along = vec3(axis.x, 0.0f, axis.z);
if (vec3_length(along) < 1e-3f) return center.y;
along = vec3_normalize(along);
/* Least squares: wanted height of the bottom over the position along the log */
float st = 0.0f, sh = 0.0f, stt = 0.0f, sth = 0.0f;
for (int k = 0; k < n; k++) {
float t = vec3_dot(vec3_sub(team[k]->position, center), along);
float h = team[k]->position.y + LIFT_HEIGHT;
st += t; sh += h; stt += t * t; sth += t * h;
}
float fn = (float)n, var = stt - st * st / fn;
float slope = var > 0.05f ? clampf((sth - st * sh / fn) / var, -0.6f, 0.6f) : 0.0f;
float mean_t = st / fn, base = sh / fn - slope * mean_t; /* height at the center */
/* Tilt towards that slope */
float now = axis.y / fmaxf(vec3_length(vec3(axis.x, 0.0f, axis.z)), 1e-3f);
if (vec3_dot(vec3(axis.x, 0.0f, axis.z), along) < 0.0f) now = -now;
now += (slope - now) * fminf(1.0f, LEVEL_RATE * dt);
Vec3 tilted = vec3_normalize(vec3_add(along, vec3(0.0f, now, 0.0f)));
if (vec3_dot(tilted, axis) < 0.0f) tilted = vec3_scale(tilted, -1.0f);
Vec3 z = o->colliders[0].axes[2];
z = vec3_sub(z, vec3_scale(tilted, vec3_dot(z, tilted)));
if (vec3_length(z) > 1e-3f) {
o->rotation = euler_yxz_from_axes(tilted, vec3_normalize(z));
Object_syncColliders(o);
}
return base - o->colliders[0].half_extents.x; /* its underside at the middle */
}
/* People (not holding it) the load would go into, moved by move: their
weight (they stand against it), and each gets told it shoves them. */
#define BYSTANDER_HOLD 1.0f /* x their weight against the load */
static float inTheWay(Object *o, Object *const *team, int n, Object *const *objects, int count, Vec3 move) {
Vec3 dir = vec3(move.x, 0.0f, move.z);
float len = vec3_length(dir);
if (len < 1e-5f) return 0.0f;
dir = vec3_scale(dir, 1.0f / len);
Vec3 was = o->position;
o->position = vec3_add(was, vec3_scale(dir, fmaxf(len, 0.02f))); /* (a little ahead) */
Object_syncColliders(o);
float weight = 0.0f;
for (int k = 0; k < count; k++) {
Object *c = objects[k];
if (!c->is_character || c->hands.grabbed == o || c->on_ladder || c->climbing) continue;
bool mine = false;
for (int t = 0; t < n; t++) mine = mine || team[t] == c;
if (mine || fabsf(c->position.x - was.x) > 4.0f || fabsf(c->position.z - was.z) > 4.0f) continue;
if (vec3_dot(vec3_sub(c->position, was), dir) <= 0.0f || Physics_penetration(o, c) <= 0.0f) continue;
weight += c->weight;
c->shoved = dir;
c->shove_load = was;
}
o->position = was;
Object_syncColliders(o);
return weight;
}
void Hands_solveGrabs(Object *const *objects, int count, float dt) {
for (int i = 0; i < count; i++) objects[i]->shoved = vec3_zero(); /* (told again below while it goes on) */
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o->is_static || o->is_character) continue;
/* Everybody holding this object */
Object *team[MAX_TEAM];
Vec3 want[MAX_TEAM];
int n = 0;
Vec3 sum = vec3_zero();
float capacity = 0.0f, lift = 0.0f, ground = 0.0f;
for (int k = 0; k < count && n < MAX_TEAM; k++) {
Object *c = objects[k];
if (!c->is_character || c->hands.grabbed != o) continue;
Vec3 d = vec3_sub(vec3_add(c->position, c->hands.grab_vector), o->position);
d.y = 0.0f;
if (o->is_held || vec3_length(d) > GRIP_BREAK_DISTANCE) { /* lost grip */
Hands_release(c);
continue;
}
team[n] = c;
want[n] = d;
sum = vec3_add(sum, d);
capacity += pushCapacity(c);
lift += liftCapacity(c);
ground += c->position.y;
n++;
}
/* Not enough hands left to carry it: it drops */
if (o->lifted && (n == 0 || lift < o->weight * (o->end_up ? END_LIFT_SHARE : 1.0f))) o->lifted = o->end_up = false;
if (o->end_up && (n != 1 || lift >= o->weight)) o->end_up = false; /* (more hands: carried all of it) */
if (n == 0) continue;
/* Lifted: it goes where the team goes, carried at hip height.
On the ground: it follows the team's average wish as far as their
strength allows; whatever it doesn't follow holds each of them back. */
float share = o->end_up ? END_DRAG : o->lifted ? 1.0f : teamShare(capacity, Object_dragWeight(o));
Vec3 move = vec3_scale(sum, share / (float)n);
if (!o->lifted && !o->end_up && share > 0.0f) { /* somebody in the way: they lean against it (and get told) */
float in_way = inTheWay(o, team, n, objects, count, move);
if (in_way > 0.0f) {
share = teamShare(capacity, Object_dragWeight(o) + BYSTANDER_HOLD * in_way);
move = vec3_scale(sum, share / (float)n);
}
}
if (o->end_up) {
move.y = 0.0f;
} else if (o->lifted) {
float goal = ground / (float)n + LIFT_HEIGHT;
if (n >= 2 && (o->type == OBJ_LOG || o->type == OBJ_TREE)) goal = levelLift(o, team, n, dt);
move.y = clampf(goal - bottomOf(o), -LIFT_SPEED * dt, LIFT_SPEED * dt);
}
o->position = vec3_add(o->position, move);
if (o->end_up) endLift(o, team[0], dt);
o->velocity.x = o->velocity.z = 0.0f;
if (o->lifted) o->velocity.y = 0.0f;
o->angular_velocity.y = 0.0f;
for (int k = 0; k < n; k++) {
Vec3 held_back = vec3_sub(want[k], vec3(move.x, 0.0f, move.z));
team[k]->position = vec3_sub(team[k]->position, held_back);
team[k]->hands.grab_share = share;
}
}
}
void Hands_update(Object *who, float dt) {
Hands *h = &who->hands;
Body *b = &who->body;
const float s = scaleOf(who);
const Vec3 facing = Object_facing(who);
const Vec3 right = rightOf(facing);
b->crouch_goal = 0.0f;
b->lean_goal = 0.0f;
b->chin_goal = 0.0f;
b->hand_active[0] = b->hand_active[1] = false;
advanceDrink(who, dt);
bool missed = false;
bool can = (h->item[0] && h->item[0]->type == OBJ_CAN) || (h->item[1] && h->item[1]->type == OBJ_CAN);
float pour_goal = who->spraying && can ? 1.0f : 0.0f; /* lifting the can takes a moment */
who->pour += (pour_goal - who->pour) * fminf(1.0f, 5.0f * dt);
if (h->winding) h->wind = fminf(h->wind + dt, FULL_WIND); /* (held on: harder) */
h->winding = false;
if (throwHand(who) < 0) h->wind = 0.0f;
for (int i = 0; i < 2; i++) {
float sign = i == HAND_LEFT ? -1.0f : 1.0f;
Object *item = h->item[i];
h->settle[i] = fminf(1.0f, h->settle[i] + dt / SETTLE_TIME);
switch (h->state[i]) {
case HAND_FREE:
if (h->throw_time[i] > 0.0f) {
h->throw_time[i] = fmaxf(0.0f, h->throw_time[i] - dt);
float u = 1.0f - h->throw_time[i] / THROW_TIME, k = sinf(PI_F * u);
Vec3 local;
if (h->overhand[i]) { /* over the shoulder, forward and down across the body */
float e = smoothstepf(0.0f, 0.7f, u);
local = vec3_lerp(vec3(sign * 0.2f * s, 1.6f * s, -0.1f * s), vec3(-sign * 0.08f * s, 1.05f * s, 0.55f * s), e);
b->lean_goal = fmaxf(b->lean_goal, 0.35f * k);
} else { /* underhand: arm stretched forward and up */
local = vec3(sign * 0.12f * s, (1.25f + 0.2f * k) * s, 0.62f * s);
}
b->hand_active[i] = h->overhand[i] ? u < 0.85f : k > 0.05f;
b->hand_target[i] = Figure_toWorld(who->position, facing, local);
}
break;
case HAND_REACHING: {
if (!item || item->is_held || item->broken) { /* someone else got it first (or it's gone) */
for (int k = 0; k < 2; k++)
if (h->item[k] == item) { h->state[k] = HAND_FREE; h->item[k] = NULL; }
break;
}
Vec3 c = itemCenter(item);
Vec3 target = twoHanded(h)
? vec3_add(c, vec3_scale(right, sign * (extentAlong(item, right) + 0.01f)))
: vec3_add(c, vec3(0.0f, extentAlong(item, UP), 0.0f));
if (!twoHanded(h) && (item->type == OBJ_LADDER || target.y > who->position.y + 1.5f * s)) /* (long, or up high: where it's in reach) */
target = closestPoint(item, vec3(target.x, who->position.y + 1.0f * s, target.z));
b->hand_active[i] = true;
b->hand_target[i] = target;
b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, target.y));
b->lean_goal = 0.25f;
h->timer[i] += dt;
if (h->timer[i] >= REACH_TIME && (!twoHanded(h) || i == HAND_RIGHT))
startHolding(who, i);
break;
}
case HAND_HOLDING: {
Vec3 local;
if (twoHanded(h)) {
/* Item in front of the chest: palms on the back half of its sides,
just outside the surface, so the forearms run along the outside */
local = vec3(sign * (extentAlong(item, right) + CARRY_PALM_GAP), 1.0f * s,
CARRY_CHEST_GAP * s + CARRY_GRIP_DEPTH);
} else if (Object_isBottle(item) && !h->by_neck[i]) {
local = vec3(sign * 0.22f * s, 0.86f * s, 0.15f * s); /* held up a little, forearm forward */
} else { /* hanging at the side (a wide one, a full bag say: the arm a little out, held by the neck) */
float out = fminf(fmaxf(0.25f * s, BODY_HALF_WIDTH * s + extentAlong(item, right) + 0.01f), 0.42f * s);
local = vec3(sign * out, fmaxf(0.80f * s, 2.0f * extentAlong(item, UP) + 0.14f), 0.08f * s);
}
if (h->wind > 0.0f && (i == throwHand(who) || twoHanded(h))) { /* winding up to throw */
float back = smoothstepf(0.0f, 0.2f, h->wind);
Vec3 low = twoHanded(h) ? vec3(local.x, 0.7f * s, -0.05f * s) : vec3(sign * 0.24f * s, 0.78f * s, -0.24f * s);
local = vec3_lerp(local, low, back);
if (goesOverhand(who, item)) { /* the arm comes back over the shoulder, the other one points ahead */
float over = smoothstepf(0.0f, 0.25f, h->wind - OVERHAND_AFTER * (1.2f - 0.5f * who->temperament.energy));
local = vec3_lerp(local, vec3(sign * 0.24f * s, 1.62f * s, -0.3f * s), over);
b->lean_goal = fminf(b->lean_goal, -0.2f * over);
if (h->state[1 - i] == HAND_FREE) {
b->hand_active[1 - i] = true;
b->hand_target[1 - i] = Figure_toWorld(who->position, facing, vec3(-sign * 0.15f * s, 1.3f * s, 0.45f * s));
}
}
}
if (Ladder_isRaised(item)) { /* a slid-out ladder held up: the hand in front, the other higher up the rail */
local = vec3(sign * 0.1f * s, 0.95f * s, 0.32f * s);
if (h->state[1 - i] == HAND_FREE && item->raised > 0.0f) {
Vec3 rail = vec3_scale(right, -sign * LADDER_WIDTH * 0.5f);
b->hand_active[1 - i] = true;
b->hand_target[1 - i] = vec3_add(Ladder_raisedGrip(item, 1.0f), vec3_add(rail, vec3_scale(right, -sign * 0.02f)));
}
}
if (item->type == OBJ_UMBRELLA && item->umbrella_open) /* the hand up in front of its shoulder, the forearm up (the shaft leans in over the head) */
local = vec3(sign * UMBRELLA_HAND_OUT * s, UMBRELLA_HAND_UP * s, UMBRELLA_HAND_AHEAD * s);
if (item->type == OBJ_CAN) { /* watering: the arm comes up and forward */
Vec3 lifted = vec3(sign * 0.13f * s, 1.02f * s, 0.40f * s);
local = vec3_lerp(local, lifted, smoothstepf(0.0f, 1.0f, who->pour));
}
b->hand_active[i] = true;
b->hand_target[i] = Figure_toWorld(who->position, facing, local);
if (h->drink_hand == i) {
b->hand_target[i] = drinkTarget(who, b->hand_target[i], h->drink_time);
float tipped = tiltOf(item, h->drink_time) / DRINK_TILT;
b->lean_goal = -0.15f * tipped;
b->chin_goal = 0.45f * tipped; /* head back */
}
break;
}
case HAND_READY: { /* out in front, where you look, open */
Vec3 aim = who->work_state.aim;
if (vec3_length(aim) < 0.1f) aim = facing;
aim = vec3_normalize(vec3(aim.x, clampf(aim.y, -0.8f, 0.8f), aim.z));
Vec3 shoulder = Figure_toWorld(who->position, facing, vec3(sign * 0.19f * s, 1.42f * s, 0.0f));
b->hand_active[i] = true;
b->hand_target[i] = vec3_add(shoulder, vec3_scale(aim, READY_OUT * s));
b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, b->hand_target[i].y) * 0.6f);
break;
}
case HAND_HOLD: {
Object *o = h->hold[i];
if (!o || o->broken || o->indoors > 0.0f || o->riding || o->gone_home) { Hands_letGo(who, i); break; }
Vec3 target = Hands_holdPoint(who, i);
b->hand_active[i] = true;
b->hand_target[i] = target;
b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, target.y) * 0.6f);
break;
}
case HAND_GRABBING: {
Vec3 target = fromObject(h->grabbed, h->grab_point[i]);
b->hand_active[i] = true;
b->hand_target[i] = target;
/* The hand couldn't get there last frame: not really holding */
Vec3 palm = Figure_toWorld(who->position, facing, who->skeleton.joints[i == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R]);
if (b->hand_weight[i] > 0.95f && vec3_length(vec3_sub(palm, target)) > GRIP_MISS) missed = true;
b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, target.y) * 0.8f);
float intent = who->move_local.z; /* forward = push, backward = pull */
b->lean_goal = intent > 0.1f ? 0.55f : (intent < -0.1f ? -0.45f : 0.15f);
break;
}
}
}
/* Can't reach the grip for a moment: take hold again from where we stand.
If that's too far, let go (helpers step back, wait and try again). */
h->miss_time = missed ? h->miss_time + dt : fmaxf(0.0f, h->miss_time - 2.0f * dt);
if (h->grabbed && h->miss_time > REGRAB_TIME) {
Object *o = h->grabbed;
h->miss_time = 0.0f;
Hands_release(who);
Hands_grabObject(who, o);
}
/* Touching someone (waking them up): a free hand reaches out */
if (h->touching) {
int hand = Hands_freeHand(who);
if (hand >= 0) {
b->hand_active[hand] = true;
b->hand_target[hand] = h->touch_point;
b->crouch_goal = fmaxf(b->crouch_goal, crouchFor(who, h->touch_point.y));
b->lean_goal = 0.3f;
}
h->touching = false;
}
if (h->rope > 0.0f) { /* on a rope: both hands on it, one behind the other; leaning back to pull */
for (int hand = 0; hand < 2; hand++) {
if (h->state[hand] != HAND_FREE) continue;
b->hand_active[hand] = true;
b->hand_target[hand] = vec3_add(h->rope_at, vec3_scale(h->rope_dir, hand == who->main_hand ? 0.0f : 0.35f));
}
b->lean_goal = fminf(b->lean_goal, -0.6f * fminf(h->rope, 1.0f) + 0.1f);
h->rope = 0.0f;
}
if (!h->on_edge) b->reach_turn = 0.0f;
if (h->on_edge) { /* hanging on, pulling up: both hands on the edge */
for (int hand = 0; hand < 2; hand++) {
if (h->state[hand] != HAND_FREE) continue;
float side = hand == HAND_LEFT ? -1.0f : 1.0f;
b->hand_active[hand] = true;
b->hand_target[hand] = vec3_add(h->edge_point, vec3_scale(h->edge_across, side * 0.2f * s));
if (hand != h->edge_lead && h->edge_trail > 0.0f) /* (one hand reaches up first; the other follows from down by the shoulder) */
b->hand_target[hand] = vec3_add(b->hand_target[hand], vec3(0.0f, -0.5f * s * h->edge_trail, 0.0f));
}
b->reach_turn = h->edge_turn;
b->crouch_goal = fmaxf(b->crouch_goal, h->edge_crouch);
h->on_edge = false;
h->edge_trail = h->edge_turn = 0.0f;
}
}
/* Just taken: the item turns from the way it lay into the carry pose, around the palm. */
/* The middle of the closed hand (between the wrist and the knuckles): what's held is in it. */
static Vec3 fistOf(const Object *who, int hand) {
const Vec3 *j = who->skeleton.joints;
int w = hand == HAND_LEFT ? JOINT_WRIST_L : JOINT_WRIST_R;
return Figure_toWorld(who->position, Object_facing(who), vec3_lerp(j[w], j[w + 1], FIST_AT));
}
static void settleInHand(const Object *who, int hand, Object *item) {
const Hands *h = &who->hands;
if (h->settle[hand] >= 1.0f) return;
float k = smoothstepf(0.0f, 1.0f, h->settle[hand]);
const Vec3 facing = Object_facing(who);
Vec3 palm = fistOf(who, hand);
Mat4 target = mat4_rotate_yxz(item->rotation);
Vec3 y = vec3_normalize(vec3_lerp(Figure_toWorld(vec3_zero(), facing, h->settle_y[hand]), mat4_mul_dir(target, UP), k));
Vec3 z = vec3_lerp(Figure_toWorld(vec3_zero(), facing, h->settle_z[hand]), mat4_mul_dir(target, vec3(0.0f, 0.0f, 1.0f)), k);
z = vec3_sub(z, vec3_scale(y, vec3_dot(z, y)));
if (vec3_length(z) < 1e-4f) return;
z = vec3_normalize(z);
Vec3 origin = vec3_add(item->position, item->pivot);
Vec3 local = mat4_mul_dir_transposed(target, vec3_sub(origin, palm)); /* origin as seen from the palm */
item->rotation = euler_yxz_from_axes(y, z);
Object_syncColliders(item);
origin = vec3_add(palm, mat4_mul_dir(mat4_rotate_yxz(item->rotation), local));
item->position = vec3_sub(origin, item->pivot);
}
/* Carried things never sink into the body: pushed out of a box around the spine
(forward for things in front, sideways for things at the side). */
static void keepOffBody(const Object *who, Object *item) {
const float s = scaleOf(who);
const Vec3 facing = Object_facing(who), right = rightOf(facing);
const Vec3 *j = who->skeleton.joints;
Vec3 local = Figure_toLocal(who->position, facing, itemCenter(item));
float ex = extentAlong(item, right), ey = extentAlong(item, UP), ez = extentAlong(item, facing);
if (local.y - ey > j[JOINT_NECK].y || local.y + ey < 0.1f * s) return;
/* The spine at the item's height (the legs below the hips) */
Vec3 pelvis = j[JOINT_PELVIS], chest = j[JOINT_CHEST];
float k = clampf((local.y - pelvis.y) / fmaxf(chest.y - pelvis.y, 1e-3f), 0.0f, 1.0f);
Vec3 spine = vec3_lerp(pelvis, chest, k);
float dx = local.x - spine.x, dz = local.z - spine.z;
float push_x = BODY_HALF_WIDTH * s + ex - fabsf(dx);
float push_z = BODY_CLEARANCE * s + ez - fabsf(dz);
if (push_x <= 0.0f || push_z <= 0.0f) return;
Vec3 push = push_z <= push_x ? vec3_scale(facing, dz >= 0.0f ? push_z : -push_z)
: vec3_scale(right, dx >= 0.0f ? push_x : -push_x);
item->position = vec3_add(item->position, push);
}
#define CAN_TIP 1.2f /* radians the can tips forward to pour (the wrist turns) */
static void placeCan(const Object *who, int hand, Object *can) {
const Vec3 facing = Object_facing(who);
Vec3 palm = Figure_toWorld(who->position, facing, who->skeleton.joints[hand == HAND_LEFT ? JOINT_HAND_L : JOINT_HAND_R]);
can->rotation = vec3(-CAN_TIP * smoothstepf(0.2f, 1.0f, who->pour), who->rotation.y + PI_F, 0.0f);
Object_syncColliders(can);
Vec3 grip = mat4_mul_dir(mat4_rotate_yxz(can->rotation), vec3(0.0f, 0.19f, -0.02f)); /* the top of the handle */
can->position = vec3_sub(vec3_sub(palm, grip), can->pivot);
can->velocity = vec3_zero();
}
void Hands_placeHeld(Object *who) {
Hands *h = &who->hands;
const Vec3 facing = Object_facing(who);
for (int i = 0; i < 2; i++) {
if (h->state[i] != HAND_HOLDING) continue;
if (twoHanded(h) && i == HAND_RIGHT) continue; /* placed with the left hand */
Object *item = h->item[i];
if (item->type == OBJ_LADDER) { /* on edge at the side */
Ladder_placeHeld(who, i, item);
continue;
}
if (item->type == OBJ_CAN) { /* by the handle, spout ahead; tipped forward to pour */
placeCan(who, i, item);
continue;
}
if (Tools_placeHeld(who, i, item)) {
if (!twoHanded(h)) settleInHand(who, i, item);
else keepOffBody(who, item);
continue;
}
item->rotation.y = who->rotation.y + h->item_yaw[i];
Object_syncColliders(item);
Vec3 center;
if (twoHanded(h)) {
/* Between the palms, pushed forward so the palms grip its back half */
Vec3 l = Figure_toWorld(who->position, facing, who->skeleton.joints[JOINT_HAND_L]);
Vec3 r = Figure_toWorld(who->position, facing, who->skeleton.joints[JOINT_HAND_R]);
float forward = extentAlong(item, facing) - CARRY_GRIP_DEPTH;
center = vec3_add(vec3_scale(vec3_add(l, r), 0.5f), vec3_scale(facing, forward));
} else {
Vec3 hand = fistOf(who, i); /* (in the fist, not at the fingertips) */
if (h->drink_hand == i) {
/* Tipped back towards the mouth */
item->rotation = vec3(-tiltOf(item, h->drink_time), who->rotation.y, 0.0f);
Object_syncColliders(item);
} else if (Object_isBottle(item)) {
item->rotation.x = item->rotation.z = 0.0f;
Object_syncColliders(item);
}
if (item->type == OBJ_UMBRELLA && item->umbrella_open) { /* the handle in the fist, the shaft leaning in so the canopy is over the head (not the shaft in the face) */
const float s = who->look.height / 1.75f;
Vec3 over = Figure_toWorld(who->position, facing, vec3(0.0f, UMBRELLA_TOP * s, 0.03f * s));
const Object *g = who->umbrella_guest;
if (g && who->umbrella_guest_t > 0.0f && hypotf(g->position.x - who->position.x, g->position.z - who->position.z) < 1.2f) { /* somebody under it too: over the two of them */
over.x = 0.5f * (over.x + g->position.x);
over.z = 0.5f * (over.z + g->position.z);
}
Vec3 up = vec3_normalize(vec3_sub(over, hand));
Vec3 side = vec3_cross(up, facing);
Vec3 ahead = vec3_length(side) > 1e-3f ? vec3_normalize(vec3_cross(vec3_normalize(side), up)) : facing;
item->rotation = euler_yxz_from_axes(up, ahead);
Object_syncColliders(item);
center = vec3_add(hand, vec3_scale(up, UMBRELLA_LENGTH * 0.5f - 0.07f));
} else if (item->type == OBJ_UMBRELLA && h->drink_hand != i) { /* folded: in the fist by the middle, the tip ahead and down, the crook behind */
Vec3 along = vec3_normalize(vec3_add(vec3_scale(facing, 0.9f), vec3(0.0f, -0.42f, 0.0f)));
item->rotation = euler_yxz_from_axes(along, vec3_normalize(vec3_cross(vec3_cross(along, facing), along)));
Object_syncColliders(item);
center = vec3_add(hand, vec3_scale(along, 0.06f));
} else if (item->type == OBJ_SCYTHE) { /* carried over the shoulder, the blade up behind; mowing: the blade sweeping the grass in front */
Vec3 along, right = vec3_normalize(vec3_cross(facing, UP));
const float s = who->look.height / 1.75f;
if (who->mowing > 0.0f) {
Vec3 bottom = vec3_add(who->position, vec3_add(vec3_scale(facing, 1.05f * s), vec3_scale(right, 0.7f * s * sinf(who->mowing))));
bottom.y += 0.06f;
along = vec3_normalize(vec3_sub(hand, bottom));
center = vec3_add(bottom, vec3_scale(along, 0.8f));
} else {
along = vec3_normalize(vec3_add(vec3_scale(facing, 0.75f), vec3(0.0f, -0.55f, 0.0f)));
center = vec3_sub(hand, vec3_scale(along, 0.15f));
}
Vec3 side = vec3_cross(along, facing);
Vec3 ahead = vec3_length(side) > 1e-3f ? vec3_normalize(vec3_cross(vec3_normalize(side), along)) : facing;
item->rotation = euler_yxz_from_axes(along, ahead);
Object_syncColliders(item);
} else if (Object_isBottle(item)) {
/* The hand around the bottle where it grips it */
center = vec3_sub(hand, vec3_scale(item->colliders[0].axes[1], bottleGrip(who, i, item)));
} else {
float hang = h->drink_hand == i ? 1.0f - drinkGrip(h->drink_time) : 1.0f;
center = vec3_sub(hand, vec3(0.0f, (extentAlong(item, UP) + 0.01f) * hang, 0.0f)); /* hangs from the hand */
}
}
item->position = vec3_sub(center, item->colliders[0].offset);
item->velocity = vec3_zero();
if (!twoHanded(h)) settleInHand(who, i, item);
if (h->drink_hand != i && !(item->type == OBJ_UMBRELLA && item->umbrella_open) && item->type != OBJ_SCYTHE) keepOffBody(who, item);
}
}
/* ---------- Reaching out, holding on (E / Q) ---------- */
void Hands_reachOut(Object *who, int hand) {
Hands *h = &who->hands;
if (h->state[hand] != HAND_FREE || h->grabbed) return;
h->state[hand] = HAND_READY;
h->hold[hand] = NULL;
}
void Hands_letGo(Object *who, int hand) {
Hands *h = &who->hands;
if (h->state[hand] != HAND_READY && h->state[hand] != HAND_HOLD) return;
h->state[hand] = HAND_FREE;
h->hold[hand] = NULL;
}
int Hands_holding(const Object *who, const Object *whom) {
for (int i = 0; i < 2; i++) if (who->hands.state[i] == HAND_HOLD && who->hands.hold[i] == whom) return i;
return -1;
}
Vec3 Hands_holdPoint(const Object *who, int hand) {
const Hands *h = &who->hands;
const Object *o = h->hold[hand];
if (!o) return who->position;
if (o->is_character) return Figure_toWorld(o->position, Object_facing(o), o->skeleton.joints[h->hold_joint[hand]]);
return fromObject(o, h->hold_local[hand]);
}
/* Where on a person a point is: their nearest joint (the hand, the wrists into it). */
static int nearestJoint(const Object *o, Vec3 p, float *dist) {
Vec3 facing = Object_facing(o);
int best = -1;
float best_d = 1e9f;
for (int j = 0; j < JOINT_COUNT; j++) {
if (j == JOINT_TOE_L || j == JOINT_TOE_R) continue;
float d = vec3_length(vec3_sub(Figure_toWorld(o->position, facing, o->skeleton.joints[j]), p));
if (j == JOINT_HAND_L || j == JOINT_HAND_R) d -= 0.07f; /* (a hand reaching for theirs takes it) */
if (d < best_d) { best_d = d; best = j; }
}
*dist = fmaxf(best_d, 0.0f);
return best;
}
Object *Hands_catch(Object *who, int hand, Object *const *objects, int count, bool *person) {
Hands *h = &who->hands;
*person = false;
if (h->state[hand] != HAND_READY) return NULL;
const Body *b = &who->body;
Vec3 palm = b->hand_target[hand];
Object *best = NULL;
float best_d = CATCH_NEAR;
int best_joint = -1;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (o == who || o->is_held || o->collider_count == 0 || o->broken || o->type == OBJ_DECAL || o->type == OBJ_STRUCTURE) continue;
if (fabsf(o->position.x - palm.x) > 6.0f || fabsf(o->position.z - palm.z) > 6.0f) continue;
if (o->is_character) { /* somebody: at a joint of theirs (the body round it) */
if (o->type == OBJ_DOG || o->riding || o->indoors > 0.0f) continue;
float d;
int j = nearestJoint(o, palm, &d);
if (j >= 0 && d - 0.08f < best_d) { best_d = d - 0.08f; best = o; best_joint = j; }
continue;
}
float d = vec3_length(vec3_sub(closestPoint(o, palm), palm));
if (d < best_d) { best_d = d; best = o; best_joint = -1; }
}
if (!best) return NULL;
if (best->is_character) {
h->state[hand] = HAND_HOLD;
h->hold[hand] = best;
h->hold_joint[hand] = best_joint;
*person = true;
return best;
}
if (!best->is_static) { /* loose: carried if it can be, else held on to (push, pull, lift together) */
h->state[hand] = HAND_FREE;
if (liftableOneHand(best) || liftableTwoHands(best)) { if (startPickUp(who, hand, best)) return best; }
else if (Hands_grabObject(who, best)) return best;
h->state[hand] = HAND_READY;
return NULL;
}
h->state[hand] = HAND_HOLD; /* fixed: the hand holds on where it touched */
h->hold[hand] = best;
h->hold_local[hand] = toObject(best, closestPoint(best, palm));
h->hold_joint[hand] = -1;
return best;
}