#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; }