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 1.95.0: people sit at the tables, find their way into buildings
- People now and then sit down at a table (the meeting table, the kitchen tables) for a while and have a word with whoever sits there; food they pick up they mostly eat there, a beer sometimes too - They find their way into the buildings: the walking map knows floors and steps (before, every floor counted as a wall), and they walk up steps instead of round them - Nobody stands stuck under a small tree's crown or squeezed against a box any more: they step out the way that's free - Sitting at a table no longer ends in a fall (the feet under the table counted as stepping over something) - Felling: look down at the trunk to cut right there, look up the trunk above your head to cut higher (up to the chest); straight ahead stays hip high
entropie/Makefile | 2 +- entropie/README.md | 6 +- entropie/STATE.md | 13 ++- entropie/commit_message.txt | 24 ++--- entropie/src/people/ai.c | 206 ++++++++++++++++++++++++++++++++++++++++++- entropie/src/people/ai.h | 4 + entropie/src/world/nav.c | 67 ++++++++++++-- entropie/src/world/terrain.c | 7 +- entropie/src/world/terrain.h | 2 + entropie/src/world/tools.c | 8 +- entropie/src/world/world.c | 1 + 11 files changed, 308 insertions(+), 32 deletions(-) diff --git a/entropie/Makefile b/entropie/Makefile index c68f953..2633686 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 1.94.0 +VERSION := 1.95.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index 47913ad..5d18828 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -102,6 +102,9 @@ and satellite pictures (about 340 x 340 m). - Hold food close to the fire to grill it: sausages (from the cool box) are only really good grilled; apples and bread get better too - People come over to a burning fire, sit down round it and look into the flames +- Now and then they sit down at a table (the meeting table, the kitchens) for + a while and have a word; food they pick up they mostly eat there, a beer + sometimes too. They find their way in: up the steps, through the doors - C: sit down yourself (and rest faster): on a bench or chair (right on the seat; a chair is pulled out first, shuffled in to the table once you sit, and pushed back in after you leave) if one is next @@ -334,7 +337,8 @@ An axe, pruning shears and a chainsaw are in the tool room of your garages; the | Split a short log or half | 3 | – | – | – | | Chop out a stump | 14 | 7 | – | – | -A tree is cut at about hip height (lower if you look lower), where you started +A tree is cut at about hip height: look down at the trunk to cut right there, +look up the trunk (above your head) to cut higher, up to the chest; where you started its notch; strokes elsewhere start a new notch from scratch. It falls beside its stump and lies on the ground. diff --git a/entropie/STATE.md b/entropie/STATE.md index 313a383..d29d011 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -122,6 +122,13 @@ Round 1.94: - Fallen trees (`forestry.c`): once down (not stopped by something) loose (`is_static` false), weight `Object_treeWeight` (wood + TREE_BRANCH_KG 18 per branch). Grabbing uses `Object_dragWeight`: lying on the ground TREE_DRAG 0.5 x weight + TREE_SNAG_KG 25 per branch. Only the trunk collider meets the ground. Dragged with the crown against something static: `snag` adds up (fades 0.5/s when free), the tree is held; at SNAG_HOLD 1 s (or SNAG_SPEED 0.9 m/s) a branch snaps (`dropBranch`, chips). - Carried ladders and walls (`world.c`): a raised ladder running into something tips down flat (`dipped`) only if that gets it out (a beam, a doorway); else it is stood up straight (`steep`, RAISE_HIGH) until it would stand at 1.36 clear again. Carried at the side, a ladder in a wall slides along in the hand (`grip_shift`, SLIDE_STEP 0.1 m, at least SLIDE_KEEP 0.35 m from its end; back to the middle once clear) before the walk is stopped: taken off a wall, walked into one. +Round 1.95: +- At a table (`ai.c` AI_TABLE): a free chair standing at a table (`chairAtTable`: within 0.8 m of its edge, facing it, same floor; not anyone's `seat` or target) within TABLE_WALK 35 m. Food picked up goes there TABLE_EAT 0.75 of the time, a beer TABLE_BEER 0.3; idle with no fire TABLE_SIT 0.06 a decision. Walk there (`ai_steer`), within TABLE_SEAT 0.5 m `Object_sit`; sitting TABLE_STAY 25-70 s: a bite or sip every 3-5 s (`table_pause`), done: on the table (`Camp_doneWithBottle`); a word with someone sitting within 2.6 m every ~14 s, they answer; looks at them. Up when done and the time is over, or bladder 0.8 / sleepy 0.85 / hungry or thirsty 0.6 with nothing in hand. Chair taken on the way: another one, else eat standing. +- Walking map (`nav.c`): per cell `g_floor`, where one stands: the ground, or the top of a building's level box (OBJ_WALL, not a roof, at least 0.2 m across, up to WALK_ON_MOST 1.2 m over `Terrain_naturalHeight`, the ground without pits). Blocking is measured from there (floors over pits were blocked: no way into any building). A* and the straightening don't go up or down more than NAV_RISE 0.38 m a cell (x1.42 diagonal): stairs yes, floor edges no. +- Steering (`ai.c` ai_blockedBy): a building's floor or step up to STAIR_FIRST 0.3 + STAIR_SLOPE 0.65 x distance over the feet is walked up, not round; a standing tree blocks with its trunk only (the crown boxed people in under small trees). Squeezed (their own spot within clearance of something): out the way that's free 2/3 and 1 x the look ahead (`ai_steerPast`). +- Sitting: no half-over-something falls (`world.c` halfOver clears `crossing` while sitting; feet under the table started a step over). +- Felling cut (`tools.c`): looking lower than hip height, right where you look; between that and AIM_UP_FROM 0.3 m over the eyes, hip height; higher, the cut goes up as much (clamped to the tool's range: axe 0.45-1.35 m). + Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves. Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built); the coat rack (FURN_COATRACK, in your kitchen by the doors, garage coords 20.6 / 2.4) shows the jackets hanging on it (`Shapes_coatRack`, up to 6, a bitmask of whose in `shown`). `jacket_hung` (characters, true at the start): cold (chill 0.6+) and the jacket with them: on it goes (as before); hanging: NPCs `ai.want_jacket` -> `ai.c` fetchJacket (idle / wander: goTo the rack, on with it, "Ah, better."; gives up after 90 s or warm again); you are told once ("your jacket hangs on the rack in your garage") and click the rack (`game_actions.c` coatRack: on, or back onto it). Warm again: it comes off and stays with them. @@ -254,9 +261,7 @@ Most important first. - Joint jumps left in the audit: jog+stop 171, sprint 297 (worst 26 cm) - running into things: a hurried catch step swings through in a few frames (step 0.07 -> 0.34, the ankle up 20 cm). Capping the swing speed helped sprint (278) but made jumps, drunk walk and the NPCs worse - tried and reverted in 1.83; ladder 45: toes on the sloping roof (a landing step's toes snap onto the slope, 16 cm; blending them in made all walking worse), toes turning onto the rungs getting on from the top (14 cm) **From the 15th of October (to do)** -- NPC pathfinding has problems (which ones: to look at with examples) -- NPCs sitting at the meeting table (chairs work for them, nothing sends them there yet); eating at a table -- Felling: aiming could set the cut height more freely +- NPC pathfinding: buildings, steps and squeezes fixed in 1.95; the basement (under a floor) isn't on the walking map yet **Body and moving** - Obstacle approach, still to do: NPCs stepping up onto higher things (now only what they'd stride over anyway); thin round logs (under 0.2 m) are still stepped over; a step in between on top of something unstable (if clumsy); NPCs: pull up on their own for their own reasons (now only following a leader), jumping onto things @@ -279,4 +284,4 @@ Most important first. - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene) --- -State of `entropie 1.94.0 (build 83ab53f)` +State of `entropie 1.95.0 (build f8a9b47)` diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index 6a09c1c..76f77ad 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,12 +1,14 @@ -entropie 1.94.0: Alice fetches, fallen trees drag, ladders stay out of walls +entropie 1.95.0: people sit at the tables, find their way into buildings -- Alice runs after what you throw and brings it back to your feet - (sometimes catching it in the air); a dropped sausage she eats, an apple - she keeps and chews lying somewhere -- Felled trees lying on the ground can be pushed or pulled; their branches - make them heavy and snag on things in the way, and snap off when you - keep pulling. Without branches they move much more easily -- A ladder taken off a wall no longer pokes into it: carried at the side it - slides back in your hand; walking into a wall stops you -- A slid-out ladder walked into a wall is stood up straight instead of - tipping flat through it (it still tips down under a beam or doorway) +- People now and then sit down at a table (the meeting table, the kitchen + tables) for a while and have a word with whoever sits there; food they + pick up they mostly eat there, a beer sometimes too +- They find their way into the buildings: the walking map knows floors and + steps (before, every floor counted as a wall), and they walk up steps + instead of round them +- Nobody stands stuck under a small tree's crown or squeezed against a box + any more: they step out the way that's free +- Sitting at a table no longer ends in a fall (the feet under the table + counted as stepping over something) +- Felling: look down at the trunk to cut right there, look up the trunk + above your head to cut higher (up to the chest); straight ahead stays hip high diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index 3c92f0b..481ede4 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -110,6 +110,9 @@ static bool longToStepOnto(const Object *self, const Object *o) { return longest >= ONTO_LONG && top - self->position.y <= ONTO_HIGH; } +#define STAIR_FIRST 0.3f /* m: a step up right here ... */ +#define STAIR_SLOPE 0.65f /* ... and this much more per m further on: stairs, not a wall */ + bool ai_blockedBy(const Object *self, const Object *ignore, const Object *ignore2, Object *const *objects, int count, Vec3 p) { Object *near[512]; /* (only what's near p, from the cell grid) */ @@ -140,13 +143,18 @@ bool ai_blockedBy(const Object *self, const Object *ignore, const Object *ignore if (flatDistance(o->position, p) < PERSON_RADIUS) return true; continue; } - for (int c = 0; c < o->collider_count; c++) { + int parts = o->type == OBJ_TREE && !o->toppled ? 1 : o->collider_count; /* (a standing tree: its trunk; under the crown you just duck) */ + for (int c = 0; c < parts; c++) { /* Low things (a woodpile's sleepers, a stump) are stepped over */ const Collider *box = &o->colliders[c]; if (flatDistance(Object_colliderCenter(o, c), p) > vec3_length(box->half_extents) + BODY_RADIUS) continue; float top = Object_colliderCenter(o, c).y + fabsf(box->axes[0].y) * box->half_extents.x + fabsf(box->axes[1].y) * box->half_extents.y + fabsf(box->axes[2].y) * box->half_extents.z; if (top < self->position.y + 0.25f) continue; + /* A building's floor or steps: walked up (a stair rises about STAIR_SLOPE) */ + if (o->type == OBJ_WALL && o->variant != 3 && box->axes[1].y > 0.99f && box->half_extents.x >= 0.1f && box->half_extents.z >= 0.1f && + top <= self->position.y + STAIR_FIRST + STAIR_SLOPE * flatDistance(self->position, p)) + continue; /* Knee, waist and head height above the feet */ for (int k = 0; k < 3; k++) { Vec3 q = vec3(p.x, self->position.y + 0.4f + 0.6f * (float)k, p.z); @@ -202,6 +210,16 @@ Vec3 ai_steerPast(Object *self, const Object *ignore, const Object *ignore2, } } } + /* Already too close to something (squeezed in by a box, a trunk): out the way that's free further on */ + if (ai_blockedBy(self, ignore, ignore2, objects, count, self->position)) { + for (unsigned i = 0; i < sizeof(DETOURS) / sizeof(DETOURS[0]); i++) + for (int s = 0; s < (i ? 2 : 1); s++) { + Vec3 d = rotateY(dir, deg2rad(DETOURS[i]) * (s ? -ai->detour : ai->detour)); + if (!ai_blockedBy(self, ignore, ignore2, objects, count, vec3_add(self->position, vec3_scale(d, ahead * 2.0f / 3.0f))) && + !ai_blockedBy(self, ignore, ignore2, objects, count, vec3_add(self->position, vec3_scale(d, ahead)))) + return d; + } + } /* Boxed in: next time try the other side first */ ai->detour = -ai->detour; ai->steer_hold = 0.0f; @@ -289,6 +307,168 @@ static void rest(Ai *ai, float min, float max) { ai->timer = min + (max - min) * random01(ai); } +/* ---------- At a table ---------- */ + +#define TABLE_WALK 35.0f /* m: a free chair at a table this near is worth the walk */ +#define TABLE_EAT 0.75f /* chance food they pick up is eaten sitting at a table (a free chair there) ... */ +#define TABLE_BEER 0.3f /* ... a beer drunk there */ +#define TABLE_SIT 0.06f /* idle (no fire): now and then over to a table to sit a while */ +#define TABLE_STAY_MIN 25.0f +#define TABLE_STAY_MAX 70.0f +#define TABLE_CHAT 14.0f /* s between words with others sitting there */ +#define TABLE_NEAR 2.6f /* m: sitting this close counts as at the same table */ +#define TABLE_SEAT 0.5f /* m: within this of the chair they sit down */ + +#define PICK_ONE(list, ai) (list)[(int)(random01(ai) * (float)(sizeof(list) / sizeof((list)[0]))) % (int)(sizeof(list) / sizeof((list)[0]))] +static const char *const TABLE_OPEN[] = { "Ah, good to sit for a bit.", "Mind if I join you?", "That's better.", "Nice table, this." }; +static const char *const TABLE_EATING[] = { "Mmh. Not bad at all.", "I was starving.", "Good stuff.", "Want a bite?" }; +static const char *const TABLE_REPLY[] = { "Mhm.", "Sure, sit down.", "Ha, yes.", "Enjoy.", "Right?" }; + +/* A chair standing at a table (facing it, on the same floor). */ +static bool chairAtTable(const Object *chair, Object *const *objects, int count) { + Vec3 front = chair->colliders[0].axes[2]; + for (int i = 0; i < count; i++) { + const Object *t = objects[i]; + if (t->type != OBJ_FURNITURE || t->variant != FURN_TABLE || t->collider_count == 0) continue; + if (fabsf(t->position.y - chair->position.y) > 0.4f) continue; + const Collider *c = &t->colliders[0]; + Vec3 rel = vec3_sub(chair->position, Object_colliderCenter(t, 0)); + if (fabsf(vec3_dot(rel, c->axes[0])) < c->half_extents.x + 0.8f && fabsf(vec3_dot(rel, c->axes[2])) < c->half_extents.z + 0.8f && + vec3_dot(front, rel) < 0.0f) + return true; + } + return false; +} + +static bool chairTaken(const Object *self, const Object *chair, Object *const *objects, int count) { + for (int i = 0; i < count; i++) { + const Object *c = objects[i]; + if (c == self || !c->is_character) continue; + if (c->seat == chair || (c->type == OBJ_HUMAN && c->ai.mode == AI_TABLE && c->ai.target == chair)) return true; + } + return false; +} + +/* The nearest free chair at a table (another floor counts as further). */ +static Object *freeTableChair(const Object *self, Object *const *objects, int count, float range) { + Object *best = NULL; + float best_d = range; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o->type != OBJ_FURNITURE || o->variant != FURN_CHAIR || ai_skipped(&self->ai, o)) continue; + float d = flatDistance(o->position, self->position) + 3.0f * fabsf(o->position.y - self->position.y); + if (d >= best_d || chairTaken(self, o, objects, count) || !chairAtTable(o, objects, count)) continue; + best_d = d; + best = o; + } + return best; +} + +static void goToTable(Object *self, Object *chair) { + Ai *ai = &self->ai; + ai->mode = AI_TABLE; + ai->target = chair; + ai->timer = 0.0f; + ai->table_stay = TABLE_STAY_MIN + (TABLE_STAY_MAX - TABLE_STAY_MIN) * random01(ai); + ai->table_pause = DRINK_PAUSE; + ai->table_chat = 2.0f + 4.0f * random01(ai); + setGoal(ai, chair->position); +} + +/* Food or a bottle in the main hand (what they eat or drink at the table). */ +static Object *mealInHand(const Object *self) { + const Hands *h = &self->hands; + Object *m = h->state[self->main_hand] == HAND_HOLDING ? h->item[self->main_hand] : NULL; + return m && (Object_isFood(m) || Object_isBottle(m)) ? m : NULL; +} + +static void leaveTable(Object *self) { + self->sitting = false; /* (gets up: Object_leaveSeat) */ + self->ai.target = NULL; + rest(&self->ai, 1.0f, 3.0f); +} + +/* Somebody else sitting at the same table. */ +static Object *tableMate(const Object *self, Object *const *objects, int count) { + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o != self && o->is_character && o->sitting && o->seat && o->seat->type == OBJ_FURNITURE && + flatDistance(o->position, self->position) < TABLE_NEAR && fabsf(o->position.y - self->position.y) < 0.5f) + return o; + } + return NULL; +} + +static void updateTable(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + Object *chair = ai->target; + Object *meal = mealInHand(self); + const int hand = self->main_hand; + if (self->hands.state[hand] == HAND_REACHING) { stand(self); return; } /* (still picking it up) */ + if (!chair) { leaveTable(self); return; } + + /* Over to the chair, then sit down on it */ + if (!self->sitting || self->seat != chair) { + if (chairTaken(self, chair, objects, count)) { /* somebody was quicker: another one, or eat standing */ + Object *other = freeTableChair(self, objects, count, TABLE_WALK * 0.5f); + if (other) { ai->target = other; setGoal(ai, other->position); return; } + ai->target = NULL; + if (meal) { ai->mode = AI_DRINK; ai->timer = 0.0f; ai->target = meal; } else rest(ai, 1.0f, 2.0f); + return; + } + ai->timer += dt; + if (ai->timer > FETCH_TIMEOUT * 2.0f) { /* can't get there */ + skipTarget(ai, chair); + ai->target = NULL; + if (meal) { ai->mode = AI_DRINK; ai->timer = 0.0f; ai->target = meal; } else rest(ai, 1.0f, 2.0f); + return; + } + float gap = Hands_gapFrom(self->position, self->look.height, chair); + if (gap < TABLE_SEAT && fabsf(chair->position.y - self->position.y) < 0.4f) { + stand(self); + Object_sit(self, chair, self->position); + ai->timer = 0.0f; + return; + } + Vec3 dir = ai_steer(self, chair, objects, count, chair->position); + if (vec3_length(dir) < 0.5f) { stand(self); return; } + ai_walk(self, dir, fminf(WALK_SPEED * 0.7f, gap * 1.5f + 0.3f), dt); + return; + } + + stand(self); + if (self->sit_moving) return; /* (sitting down) */ + ai->timer += dt; + Object *mate = tableMate(self, objects, count); + if (mate) { /* look at them now and then */ + Vec3 to = vec3_sub(vec3_add(mate->position, vec3(0.0f, mate->look.height * 0.9f, 0.0f)), + vec3_add(self->position, vec3(0.0f, self->look.height * 0.6f, 0.0f))); + self->body.look_dir = vec3_normalize(to); + self->body.look_weight = fmodf(ai->timer, 9.0f) < 4.0f ? 0.7f : 0.0f; + } + + /* A bite or a sip now and then */ + if (meal && !Hands_isDrinking(self) && (ai->table_pause -= dt) <= 0.0f) { + ai->table_pause = DRINK_PAUSE * (Object_isFood(meal) ? 2.5f : 4.0f) * (0.7f + 0.6f * random01(ai)); + if (ai->sips_left > 0 && meal->sips > 0 && Hands_drink(self)) ai->sips_left--; + else if (Camp_doneWithBottle(self, hand, objects, count, random01(ai)) == DONE_NONE) Hands_drop(self, hand); /* (on the table, mostly) */ + } + + /* A word with the others */ + if ((ai->table_chat -= dt) <= 0.0f) { + ai->table_chat = TABLE_CHAT * (0.7f + 0.6f * random01(ai)); + if (mate && ai->speech_time <= 0.0f && !mate->ai.talking) { + Ai_say(self, meal && Object_isFood(meal) ? PICK_ONE(TABLE_EATING, ai) : PICK_ONE(TABLE_OPEN, ai)); + if (mate->type == OBJ_HUMAN && mate->ai.speech_time <= 0.0f && random01(ai) < 0.6f) Ai_say(mate, PICK_ONE(TABLE_REPLY, ai)); + } + } + + /* Up again: sat long enough (and finished), or something more pressing */ + const Needs *n = &self->needs; + bool pressing = n->bladder > 0.8f || n->sleepiness > 0.85f || (!meal && (n->hunger > 0.6f || n->thirst > 0.6f)); + if (pressing || (ai->timer > ai->table_stay && !meal)) leaveTable(self); +} + /* Nearest loose bottle (or food) with something left that nobody else is going for. Thirsty people prefer water. */ static Object *findConsumable(const Object *self, Object *const *objects, int count, Want want) { @@ -499,6 +679,16 @@ static void updateIdle(Object *self, Object *const *objects, int count, float dt return; } + /* Over to a table to sit a while */ + if (!fire && random01(ai) < TABLE_SIT) { + Object *chair = freeTableChair(self, objects, count, TABLE_WALK); + if (chair) { + goToTable(self, chair); + ai->sips_left = 0; + return; + } + } + Object *bottle = NULL; if (hands_free && self->drunk < DRINK_MAX_DRUNK && random01(ai) < DRINK_CHANCE) bottle = findConsumable(self, objects, count, WANT_BEER); @@ -554,6 +744,15 @@ static void updateFetch(Object *self, Object *const *objects, int count, float d /* Beer: a sip or two. Thirsty or hungry: until it's better. */ bool need = self->needs.thirst > 0.3f || Object_isFood(bottle); ai->sips_left = need ? bottle->sips : 1 + (random01(ai) < 0.5f ? 1 : 0); + /* food (and now and then a beer) at a table if there's a chair free */ + if (random01(ai) < (Object_isFood(bottle) ? TABLE_EAT : TABLE_BEER)) { + Object *chair = freeTableChair(self, objects, count, TABLE_WALK); + if (chair) { + int sips = ai->sips_left; + goToTable(self, chair); + ai->sips_left = sips; + } + } } return; } @@ -1144,7 +1343,7 @@ static void listen(Object *self, Object *const *objects, int count, float dt) { /* Waiting is fine here: for the leader, by the fire, on a break, for an answer. */ static bool waitingIsFine(const Object *self) { const Ai *ai = &self->ai; - return ai->mode == AI_IDLE || ai->mode == AI_HELP || ai->mode == AI_FOLLOW || ai->mode == AI_DRINK || + return ai->mode == AI_IDLE || ai->mode == AI_HELP || ai->mode == AI_FOLLOW || ai->mode == AI_DRINK || (ai->mode == AI_TABLE && self->sitting) || (ai->mode == AI_APPROACH && ai->ready_to_talk) || ai->talking || self->sitting || self->body.asleep || ai->on_break > 0.0f || ai->ponder > 0.0f || ai->tool_waiting; } @@ -1409,7 +1608,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { smokeBreak(self, objects, count, dt); if (ai_climbing(self, dt)) return; /* (up or down a ladder, over an edge: that first) */ if (stepAside(self, dt)) return; /* (a pushed load coming at us) */ - if (ai->mode != AI_IDLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */ + if (ai->mode != AI_IDLE && ai->mode != AI_TABLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */ smell(self, objects, count, dt); if (ai->talking) { listen(self, objects, count, dt); @@ -1447,6 +1646,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { case AI_WORK: ai_updateWork(self, objects, count, dt); break; case AI_FOLLOW: ai_updateFollow(self, objects, count, dt); break; case AI_APPROACH: updateApproach(self, objects, count, dt); break; + case AI_TABLE: updateTable(self, objects, count, dt); break; } watchdog(self); } diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h index da6e775..413f01f 100644 --- a/entropie/src/people/ai.h +++ b/entropie/src/people/ai.h @@ -62,6 +62,7 @@ typedef enum { AI_WORK, /* on an order: firewood (and more trees) on their own */ AI_FOLLOW, /* being shown something by the player */ AI_APPROACH, /* walks up to the player to say something */ + AI_TABLE, /* over to a free chair at a table: sits a while, eats or drinks what they have there */ } AiMode; typedef enum { ORDER_NONE, ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE, ORDER_COUNT } Order; @@ -122,6 +123,9 @@ typedef struct { float stuck; /* seconds without getting closer to the goal */ float closest; int sips_left; /* AI_DRINK: sips still wanted */ + float table_stay; /* AI_TABLE: seconds they sit there ... */ + float table_pause; /* ... till the next bite or sip ... */ + float table_chat; /* ... and the next word with the others there */ bool resting; /* AI_HELP: worn out, catching breath */ struct Object *tool; /* AI_HELP: tool we are going to fetch for the leader's job */ struct Object *copied; /* AI_HELP: the leader's item we copy (got one like it, do the same with it) */ diff --git a/entropie/src/world/nav.c b/entropie/src/world/nav.c index 2ec2654..85ba462 100644 --- a/entropie/src/world/nav.c +++ b/entropie/src/world/nav.c @@ -1,7 +1,9 @@ /* * Walking map: a grid over the site, a cell blocked where something fixed - * stands between knee and head height. Ways are found with A* (8 directions, - * no cutting corners) and then straightened: only the turns are kept. + * stands between knee and head height over where one stands there (the + * ground, or a building's floor or step). Ways are found with A* (8 + * directions, no cutting corners, no more up or down from cell to cell than a + * step or two) and then straightened: only the turns are kept. */ #include "world/nav.h" @@ -15,8 +17,11 @@ #define HEADROOM 1.8f /* higher than this: walked under */ #define MOST_STEPS 90000 /* cells looked at for one way, at most */ #define FREE_SEARCH 6 /* cells round a blocked start or end to find a free one */ +#define WALK_ON_MOST 1.2f /* building floors and steps (OBJ_WALL) up to this over the natural ground are walked on ... */ +#define NAV_RISE 0.38f /* ... from one cell to the next at most this much up or down (a stair: 1-2 steps) */ static unsigned char *g_blocked; +static float *g_floor; /* where one stands in each cell: the ground, or a floor / step of a building */ static bool g_ready; static double g_signature = -1.0; @@ -33,6 +38,8 @@ static int cellOf(float v) { return (int)floorf((v + TERRAIN_SIZE * 0.5f) / NAV_ static float centerOf(int i) { return ((float)i + 0.5f) * NAV_CELL - TERRAIN_SIZE * 0.5f; } static bool inside(int x, int z) { return x >= 0 && z >= 0 && x < NAV_N && z < NAV_N; } static bool open(int x, int z) { return inside(x, z) && !g_blocked[z * NAV_N + x]; } +/* From one cell to the next: no more up or down than a step or two. */ +static bool canStep(int a, int b, bool diagonal) { return fabsf(g_floor[a] - g_floor[b]) <= (diagonal ? NAV_RISE * 1.42f : NAV_RISE); } /* Point in the collider, with clearance sideways only (not up and down). */ static bool inCollider(const Object *o, int i, Vec3 p) { @@ -76,7 +83,38 @@ static CellRect boundsOf(const Object *o) { static bool overlaps(CellRect a, CellRect b) { return a.x0 <= b.x1 && b.x0 <= a.x1 && a.z0 <= b.z1 && b.z0 <= a.z1; } -static void blockCollider(const Object *o, int i, const Terrain *terrain, CellRect clip) { +/* A building's floor slab or step (level, its top within reach of the natural ground): stood on. */ +static bool walkOn(const Object *o, int i, const Terrain *terrain, float *top) { + if (o->type != OBJ_WALL || o->variant == 3) return false; /* (building parts; not roofs) */ + const Collider *c = &o->colliders[i]; + if (fabsf(c->axes[1].y) < 0.99f || c->half_extents.x < 0.1f || c->half_extents.z < 0.1f) return false; + Vec3 center = Object_colliderCenter(o, i); + *top = center.y + c->half_extents.y; + return *top <= Terrain_naturalHeight(terrain, center.x, center.z) + WALK_ON_MOST; +} + +static void raiseFloor(const Object *o, int i, const Terrain *terrain, CellRect clip) { + float top; + if (!walkOn(o, i, terrain, &top)) return; + const Collider *c = &o->colliders[i]; + Vec3 center = Object_colliderCenter(o, i); + float ex = fabsf(c->axes[0].x) * c->half_extents.x + fabsf(c->axes[2].x) * c->half_extents.z; + float ez = fabsf(c->axes[0].z) * c->half_extents.x + fabsf(c->axes[2].z) * c->half_extents.z; + int x0 = cellOf(center.x - ex), x1 = cellOf(center.x + ex), z0 = cellOf(center.z - ez), z1 = cellOf(center.z + ez); + if (x0 < clip.x0) x0 = clip.x0; + if (z0 < clip.z0) z0 = clip.z0; + if (x1 > clip.x1) x1 = clip.x1; + if (z1 > clip.z1) z1 = clip.z1; + for (int z = z0; z <= z1; z++) + for (int x = x0; x <= x1; x++) { + Vec3 rel = vec3_sub(vec3(centerOf(x), center.y, centerOf(z)), center); + if (fabsf(vec3_dot(rel, c->axes[0])) > c->half_extents.x || fabsf(vec3_dot(rel, c->axes[2])) > c->half_extents.z) continue; + float *f = &g_floor[z * NAV_N + x]; + if (top > *f) *f = top; + } +} + +static void blockCollider(const Object *o, int i, CellRect clip) { const Collider *c = &o->colliders[i]; Vec3 center = Object_colliderCenter(o, i); float ex = 0.0f, ey = 0.0f, ez = 0.0f; @@ -96,7 +134,7 @@ static void blockCollider(const Object *o, int i, const Terrain *terrain, CellRe for (int x = x0; x <= x1; x++) { if (g_blocked[z * NAV_N + x]) continue; float px = centerOf(x), pz = centerOf(z); - float ground = Terrain_height(terrain, px, pz); + float ground = g_floor[z * NAV_N + x]; if (center.y + ey < ground + STEP_OVER || center.y - ey > ground + HEADROOM) continue; for (int k = 0; k < 3; k++) if (inCollider(o, i, vec3(px, ground + 0.4f + 0.6f * (float)k, pz))) { @@ -126,11 +164,19 @@ static NavEntry *slotFor(NavEntry *table, const Object *o, bool *found) { /* Clears the cells in r and blocks them again from every fixed thing over them. */ static void redo(Object *const *objects, int count, const Terrain *terrain, CellRect r) { - for (int z = r.z0; z <= r.z1; z++) memset(&g_blocked[z * NAV_N + r.x0], 0, (size_t)(r.x1 - r.x0 + 1)); + for (int z = r.z0; z <= r.z1; z++) { + memset(&g_blocked[z * NAV_N + r.x0], 0, (size_t)(r.x1 - r.x0 + 1)); + for (int x = r.x0; x <= r.x1; x++) g_floor[z * NAV_N + x] = Terrain_height(terrain, centerOf(x), centerOf(z)); + } + for (int i = 0; i < count; i++) { /* floors and steps first: what's in the way is from there up */ + const Object *o = objects[i]; + if (!o->is_static || o->type != OBJ_WALL || !overlaps(boundsOf(o), r)) continue; + for (int c = 0; c < o->collider_count; c++) raiseFloor(o, c, terrain, r); + } for (int i = 0; i < count; i++) { const Object *o = objects[i]; if (!o->is_static || o->collider_count == 0 || !overlaps(boundsOf(o), r)) continue; - for (int c = 0; c < o->collider_count; c++) blockCollider(o, c, terrain, r); + for (int c = 0; c < o->collider_count; c++) blockCollider(o, c, r); } } @@ -147,13 +193,14 @@ void Nav_update(Object *const *objects, int count, const Terrain *terrain) { if (!g_blocked) { size_t n = (size_t)NAV_N * NAV_N; g_blocked = calloc(n, 1); + g_floor = calloc(n, sizeof *g_floor); g_cost = malloc(n * sizeof *g_cost); g_from = malloc(n * sizeof *g_from); g_seen = calloc(n, sizeof *g_seen); g_done = calloc(n, sizeof *g_done); g_heap_cap = MOST_STEPS * 8; g_heap = malloc((size_t)g_heap_cap * sizeof *g_heap); - if (!g_blocked || !g_cost || !g_from || !g_seen || !g_done || !g_heap) return; + if (!g_blocked || !g_floor || !g_cost || !g_from || !g_seen || !g_done || !g_heap) return; } if (!g_table) { g_table = calloc(NAV_TABLE, sizeof *g_table); @@ -263,11 +310,14 @@ static float guess(int x, int z, int gx, int gz) { /* Straight line between two cells without touching a blocked one. */ static bool clearLine(int ax, int az, int bx, int bz) { float dx = (float)(bx - ax), dz = (float)(bz - az); - int steps = (int)(fmaxf(fabsf(dx), fabsf(dz)) * 3.0f) + 1; + int steps = (int)(fmaxf(fabsf(dx), fabsf(dz)) * 3.0f) + 1, last = az * NAV_N + ax; for (int s = 1; s <= steps; s++) { float t = (float)s / (float)steps; float fx = (float)ax + 0.5f + dx * t, fz = (float)az + 0.5f + dz * t; if (!open((int)floorf(fx), (int)floorf(fz))) return false; + int here = (int)floorf(fz) * NAV_N + (int)floorf(fx); + if (here != last && !canStep(last, here, true)) return false; /* (not off the edge of a floor) */ + last = here; /* (a little to each side too: no grazing corners) */ if (!open((int)floorf(fx + 0.3f), (int)floorf(fz + 0.3f)) || !open((int)floorf(fx - 0.3f), (int)floorf(fz - 0.3f))) return false; @@ -306,6 +356,7 @@ int Nav_findPath(Vec3 from, Vec3 to, Vec3 *out, int max) { if (!open(nx, nz)) continue; if (k >= 4 && (!open(x + DX[k], z) || !open(x, z + DZ[k]))) continue; /* no cutting corners */ int n = nz * NAV_N + nx; + if (!canStep(cell, n, k >= 4)) continue; /* (up or down a floor's edge: no) */ if (g_done[n] == g_stamp) continue; float cost = g_cost[cell] + (k < 4 ? 1.0f : 1.41421356f); if (g_seen[n] == g_stamp && cost >= g_cost[n]) continue; diff --git a/entropie/src/world/terrain.c b/entropie/src/world/terrain.c index 922f9b6..523c6bc 100644 --- a/entropie/src/world/terrain.c +++ b/entropie/src/world/terrain.c @@ -348,10 +348,13 @@ static float wearAt(const Terrain *t, float x, float z) { } float Terrain_height(const Terrain *t, float x, float z) { - if (x < -HALF || x > HALF || z < -HALF || z > HALF) return OUTSIDE_HEIGHT; float pit; - if (t->pit_count && Terrain_inPit(t, x, z, &pit)) return pit; + if (t->pit_count && x >= -HALF && x <= HALF && z >= -HALF && z <= HALF && Terrain_inPit(t, x, z, &pit)) return pit; + return Terrain_naturalHeight(t, x, z); +} +float Terrain_naturalHeight(const Terrain *t, float x, float z) { + if (x < -HALF || x > HALF || z < -HALF || z > HALF) return OUTSIDE_HEIGHT; float gx = fminf((x + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f); float gz = fminf((z + HALF) / GRID_CELL, (float)TERRAIN_GRID - 0.001f); int ix = (int)gx, iz = (int)gz; diff --git a/entropie/src/world/terrain.h b/entropie/src/world/terrain.h index 21127c3..6421b53 100644 --- a/entropie/src/world/terrain.h +++ b/entropie/src/world/terrain.h @@ -64,6 +64,8 @@ void Terrain_destroy(Terrain *t); Returns a very low value outside the terrain (objects fall off). */ float Terrain_height(const Terrain *t, float x, float z); +/* The ground as if no pit were dug there (what a building stands on). */ +float Terrain_naturalHeight(const Terrain *t, float x, float z); /* Ground height without the trampled paths (as built). */ float Terrain_baseHeight(const Terrain *t, float x, float z); /* The meadow's own color here (before trampling). */ diff --git a/entropie/src/world/tools.c b/entropie/src/world/tools.c index cfff10f..424e1ad 100644 --- a/entropie/src/world/tools.c +++ b/entropie/src/world/tools.c @@ -48,6 +48,8 @@ static const ToolSpec SPECS[] = { static const Vec3 UP = { 0.0f, 1.0f, 0.0f }; +#define AIM_UP_FROM 0.3f /* m over the eyes on the trunk: looking higher than that raises the felling cut */ + static float workFor(ToolKind tool, Job job) { switch (job) { case JOB_FELL: return SPECS[tool].fell; @@ -214,8 +216,10 @@ static Plan planFor(const Object *who, ToolKind tool, Object *const *objects, in /* The cut goes in where the worker looks, within the tool's range */ Vec3 c = Object_colliderCenter(o, 0); float distance = vec3_length(flat(vec3_sub(c, who->position))); - /* (about hip high; lower only if they look lower: looking straight at the tree isn't aiming up the trunk) */ - float h = clampf(fminf(lookHeight(who, distance, spec->height * s), spec->height * s), spec->low * s, spec->high * s); + /* About hip high. Looking lower: right where you look. Looking straight ahead isn't aiming + up the trunk; looking up past AIM_UP_FROM over the eyes, the cut goes up that much */ + float look = lookHeight(who, distance, spec->height * s), hip = spec->height * s, from = (EYE_HEIGHT + AIM_UP_FROM) * s; + float h = clampf(look <= hip ? look : look >= from ? hip + (look - from) : hip, spec->low * s, spec->high * s); float top = o->position.y + 2.0f * o->colliders[0].half_extents.y - 0.3f; float y = who->position.y + h; /* A cut already started: keep hitting the same notch, whoever swings, as long as it's in reach */ diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index 0031d4a..f7fd015 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -455,6 +455,7 @@ static void runInto(World *world, Object *o, float into) { apart: over you go (the body carries on); can't get over: down you go. */ static void halfOver(Object *o, float dt) { Body *b = &o->body; + if (b->crossing && o->sitting) { b->crossing = b->cross_force = false; b->cross_held = 0.0f; } /* (feet shuffled under a table: no getting over anything) */ if (!b->crossing || o->type == OBJ_DOG) return; const float s = o->look.height / 1.75f; const Foot *lead = &b->feet[b->cross_foot], *other = &b->feet[1 - b->cross_foot];