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.92.0: careful on roofs, breaks of all kinds, out of the climber's way
- Feet on the course: a step over or onto something lands the feet flat on
it (they stood 12-22 cm above logs); firewood lying on its side is walked
onto instead of being pushed along
- Holding a ladder: from the side, hands on one rail, a foot on the bottom
rung; the climber gets on and off in front
- Up on a roof: NPCs walk slowly and carefully, arms out; afraid of heights
(Ida) they don't follow up the ladder; drunk, the sharp ones are extra
careful, the dim ones stroll about and stumble. New trait: afraid of
heights (dev menu)
- Breaks: mostly their own kind, now and then another (a coffee at the
stove, a walk, a chat, a sit), short or long; the tool goes down first
- Called away from a job to help, they go back to it after; the forgetful
wait to be reminded ("Back to the firewood, please")
- Worn from the days before: hard work wears people down, only sleep takes
it off, a hard day leaves some for the next; shown on Tab
- Woodpile: "Find a good place for it" - they pick a spot themselves (a pile
nearby, else clear, level, off the roads, by a wall)entropie/Makefile | 2 +- entropie/README.md | 23 ++++++--- entropie/RULES.md | 4 +- entropie/STATE.md | 18 ++++--- entropie/commit_message.txt | 40 +++++++--------- entropie/src/game/game.c | 1 + entropie/src/game/game_dev.c | 4 +- entropie/src/people/ai.c | 48 +++++++++++++++++++ entropie/src/people/ai.h | 13 ++++- entropie/src/people/ai_help.c | 14 +++++- entropie/src/people/ai_work.c | 96 ++++++++++++++++++++++++++++++------- entropie/src/people/body.h | 3 ++ entropie/src/people/body_internal.h | 2 +- entropie/src/people/condition.c | 5 ++ entropie/src/people/talk.c | 79 +++++++++++++++++++++++++++--- entropie/src/world/ladder.c | 30 +++++++----- entropie/src/world/ladder.h | 2 +- entropie/src/world/object.c | 21 +++++++- entropie/src/world/object.h | 11 ++++- entropie/src/world/physics.c | 6 +-- entropie/src/world/scene.c | 4 ++ entropie/src/world/world.c | 2 + 22 files changed, 347 insertions(+), 81 deletions(-) diff --git a/entropie/Makefile b/entropie/Makefile index 720cebd..0e2ae0e 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 1.91.0 +VERSION := 1.92.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index d4b0c8b..96e5945 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -357,7 +357,10 @@ Hold the left button to keep working. - Everyone works their own way: Jonas is quick, gets right to it and takes no breaks; Ida thinks things over first and takes a little walk now and then; Mila stops for a chat with someone; Ben is slow, ponders and sits - down a lot + down a lot. Now and then they take another kind of break (a coffee at the + stove, a walk, a chat, a sit), short or long, and put the tool down first +- Called away from a job to help you, they go back to it after; the + forgetful wait to be reminded ("Back to the firewood, please") - Helpers (G) understand what the wood is for: they fell along, take the branches off, cut and split, fetching fitting tools. Without an axe they say so - Using or taking something shows a short line about it @@ -398,13 +401,15 @@ A hand that can't reach its grip lets go and takes hold again. what holds it (a bush, something light). The message says when it's wobbly - An unsteady ladder may slip when you climb it: a wobble first (it rocks), then it falls over and you with it (the higher, the harder you land) -- Somebody holding it (hands on the rails, a foot on the bottom rung) keeps - it steady: helpers (G) hold it when you climb, helpful people come over - on their own when it looks wobbly +- Somebody holding it (beside its foot, hands on a rail, a foot on the bottom + rung: out of the climber's way) keeps it steady: helpers (G) hold it when you + climb, helpful people come over on their own when it looks wobbly - The others can slip off a wobbly ladder too - Helpers follow you up a leaning ladder onto a roof or wall and down again; without a ladder they pull themselves up after you (if they reach) and - jump down when you're down + jump down when you're down. Ida is afraid of heights and stays down. Up + there they go slowly and carefully, arms out; drunk, the sharp ones are + extra careful, the dim ones stroll about and stumble - Click a leaning ladder to climb: W / S up and down, at the top W gets you over the edge, Space jumps off. From up there, click its top to climb down - One hand free is enough (something up to 5 kg in the other), but you may @@ -442,6 +447,9 @@ say no more often and don't come to you for a bit. may answer "In a minute!" - "The firewood goes somewhere else now": show them a new place; the old stack gets carried over to the new woodpile +- Asked where the firewood goes: show them, let them find a good place (an + existing pile nearby, else a clear, level spot off the roads, by a wall), + or leave it lying - The work place: things lying where they'd stand to split or cut, the tidy clear away first; no room to swing (a wall, the car), they take the piece somewhere with room - carried, or dragged with somebody called to help @@ -533,8 +541,9 @@ say no more often and don't come to you for a bit. - Fresh ones stink: stink lines rise, faces scrunch up, people remark on it and walk off - Sleep with Z, hold Space to sleep faster -- Tab shows your stats: energy, hunger, thirst, bladder, sleepiness, drink, - dirt, how fast you sprint and how high you jump; the arrow keys turn to +- Tab shows your stats: energy, hunger, thirst, bladder, sleepiness, how worn + out you are from the days' work (only sleep takes it off; a hard day leaves + some for the next), drink, dirt, how fast you sprint and how high you jump; the arrow keys turn to your skills. Only pressing needs show otherwise - The others live the same day: they eat, drink, pee and sleep at night diff --git a/entropie/RULES.md b/entropie/RULES.md index d1503f6..080be8a 100644 --- a/entropie/RULES.md +++ b/entropie/RULES.md @@ -18,7 +18,9 @@ Structure: [ARCHITECTURE.md](ARCHITECTURE.md). - Rebuild a mesh only when what it shows changes. - Compile without warnings: `-Wall -Wextra -Wpedantic -Wshadow`. - Characters moved by script (stepping over, onto, sitting down) use the set - moves, never a jump of the position. + moves, never a jump of the position. Feet set down by script: the surface + plus `BODY_ANKLE_Y` (scaled), not the ankle's height of the moment. +- A collider's "up" face can be any of its axes (things lie on their side). - Every version: bump `VERSION`, update `STATE.md` (done items out of the to-do list, version line at the end), keep `README.md` short, write `commit_message.txt` with this version's changes only. diff --git a/entropie/STATE.md b/entropie/STATE.md index 8648275..fa5b9de 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -21,6 +21,8 @@ Updated with every version (see the version line at the end). | Lighter in the pocket (lights) | 20 | 40 | – | – | 40 | | Smokes (craving speed) / pack / roll-ups | .3 / 10 / 0 | .6 / 0 / 12 | – | .25 / 0 / 0 | .9 / 6 / 20 | | Wits | .6 | .45 | .8 | .6 | .25 | +| Afraid of heights | .2 | .1 | .85 | .15 | .5 | +| Worn from the days before (start) | .15 | .2 | .1 | .25 | .55 | Alice the dog: follows, can be petted and called. @@ -93,6 +95,15 @@ Trees and stumps (1.91): a tree's foot flares out (FOOT_FLARE 0.12 up to 0.45 m) Shared code (1.91): one box ray (`Collider_ray`, `Object_rayToCollider`; the ladder's copy is gone, world.c's down ray uses it), `World_rayCast`, `Object_reach`. ARCHITECTURE.md (modules, the frame, conventions) and RULES.md (do's and don'ts), kept current. +Round 1.92: +- Feet on the course (`physics.c` beginStepOver): the landing spots of a step over / onto are the surface plus the ankle's own height (`BODY_ANKLE_Y`), not the ankle as it was (up on the toes: it stood 12-22 cm over a log). Planted feet over 3 cm off the surface now at most 5-9 cm, rarely (all three lanes). `tryStepUp` takes whichever collider axis points up (firewood lies on its side: you were bulldozing a half log 2.5 m down the course instead of walking onto it). +- Holding a ladder (`ladder.c`): from the side (HOLD_OUT 0.3 out, HOLD_BESIDE 0.48 aside, `hold_side`: the side they came from), both hands on that rail (one higher), the near foot on the end of the bottom rung; the climber gets on and off in front. `ai_help.c` ai_holdLadder: the last 0.7 m straight to the spot, there within 0.1 m, walks back once 0.2 m off (`hold_there`). +- Up high (`Object_upHigh`: on a roof slab, or 1.6 m+ over the ground): `Object_caution` = 0.35 + 0.5 height_fear - 0.15 fitness + drunk x (wits - 0.45) x 0.8. NPCs walk at most HIGH_PACE 1.5 m/s x (1 - 0.5 caution), arms out (balance 0.5 x caution); careful ones say "Don't look down...", drunk and careless "Woo! Look at me up here!" and stumble (HIGH_STUMBLE 0.12/s x drunk). `height_fear` (dev menu row): over 0.6 (less when drunk) they don't follow up a ladder ("No way - I'm not going up there"). +- Breaks (`ai_work.c` takeBreak): each break their own kind (BREAK_OWN 0.6) or another (sit, walk, chat, BREAK_COFFEE: to the nearest stove within 70 m, sips, back); 30 % long (x1.8), else short (x0.7), longer and sooner when worn; the tool goes down first (`Hands_setAside`, taken again after). A walk back early: the rest sitting. +- Called away (`Ai_help` keeps `paused_order` / `paused_area`): `Ai_stopHelping` sends them back to it when 0.6 wits + 0.4 willing - 0.3 drunk > 0.42; the others forget it, and "Back to the <job>, please" (talk, first choice) reminds them (`Ai_resumePaused`). +- Worn from the days before (`worn`, condition.c): + WORN_PER_EFFORT 0.012 per stamina used, - WORN_SLEEP_HOUR 0.075 per game hour asleep; adds up to WORN_FEEL 0.6 to `Object_tiredness`. Tab shows it ("Worn (days)"). +- Woodpile: "Find a good place for it" (instead of "Just pile it up here"): a woodpile within 40 m of the work, else the best spot 3-9 m round it (clear of things, level, off roads, by something big and fixed); they think it over first. + 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. @@ -226,11 +237,6 @@ Most important first. **From the 15th of October (to do)** - Trees: push / pull fallen trees; branches break or snag on obstacles (with enough momentum); much easier to carry without branches -- Steps on the course (middle lane): check the feet land on what they stand on (flat ground checked: planted feet within 1 cm) -- Holding a ladder: don't get in the way of whoever's climbing (wait a moment, or hold it from the side). NPCs on a roof: slow and careful; afraid of heights: don't go up; drunk: more careful, or dumber -- Breaks: put the tool down, a coffee, a walk, a chat; short and long ones; called away, they go back to the old task afterwards (or you remind them) -- Tired from the day before, by what they did -- Woodpiles: drop "show me where" (you can place it yourself), maybe "find a good place"; people think between tasks - Alice: doesn't fit the art style, and dumb; she tries to catch things thrown, eats a sausage dropped on the ground, may catch a thrown apple (and not bring it back). NPC pathfinding has problems - NPCs sitting at the meeting table (chairs work for them, nothing sends them there yet) @@ -255,4 +261,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.91.0 (build 286f693)` +State of `entropie 1.92.0 (build 9eb26b9)` diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index 8445eb3..8000583 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,23 +1,19 @@ -entropie 1.91.0: a basement, chairs you can sit on, the camera stays out of walls +entropie 1.92.0: careful on roofs, breaks of all kinds, out of the climber's way -- Main building: the porch only along the right part of the front; the - right half's roof turned (ridge front to back); chairs, counter, stove - and shelf face the right way (one yaw convention: yaw_facing / yaw_along - / yaw_front in linalg.h); kitchen with a shelf and two chairs -- Basement: stairs down on the right of the entrance, old loose brick - walls, open rooms, shelves, loose bricks; the ground has a hole there - (Terrain_addPit, not drawn inside); brick courses on the shell's walls -- Camera: pulled in by walls between your head and it, eases back out; - your own body is hidden when it's right behind your head -- Chairs: pulled out to sit (a hand on the backrest), pushed back in after - you leave; sitting down no longer gets stuck on the table -- Ground contact: loose things rest on their real lowest points and lie - along the slope; logs on their round side; things slide down steeper - slopes (round ones sooner) -- Walking: the hips rise and fall with the steps; strafing no longer - swings the pelvis out (springs following a moving goal lagged behind it) -- Edges higher than a stair (the ramp's side, crates): a step up, no jolt -- Trees have a flared foot that becomes the stump: no pop when felled; the - felled trunk shows its sawn end instead of being hollow -- One shared box ray (Collider_ray, World_rayCast) -- ARCHITECTURE.md and RULES.md: how the code is built, do's and don'ts +- Feet on the course: a step over or onto something lands the feet flat on + it (they stood 12-22 cm above logs); firewood lying on its side is walked + onto instead of being pushed along +- Holding a ladder: from the side, hands on one rail, a foot on the bottom + rung; the climber gets on and off in front +- Up on a roof: NPCs walk slowly and carefully, arms out; afraid of heights + (Ida) they don't follow up the ladder; drunk, the sharp ones are extra + careful, the dim ones stroll about and stumble. New trait: afraid of + heights (dev menu) +- Breaks: mostly their own kind, now and then another (a coffee at the + stove, a walk, a chat, a sit), short or long; the tool goes down first +- Called away from a job to help, they go back to it after; the forgetful + wait to be reminded ("Back to the firewood, please") +- Worn from the days before: hard work wears people down, only sleep takes + it off, a hard day leaves some for the next; shown on Tab +- Woodpile: "Find a good place for it" - they pick a spot themselves (a pile + nearby, else clear, level, off the roads, by a wall) diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c index c09147e..2be9401 100644 --- a/entropie/src/game/game.c +++ b/entropie/src/game/game.c @@ -174,6 +174,7 @@ int Game_stats(const Game *game, StatBar *bars, int max, const char **title, cha { "Thirst", n->thirst, true }, { "Bladder", n->bladder, true }, { "Sleepy", n->sleepiness, true }, + { "Worn (days)", p->worn, true }, { "Drunk", fminf(p->drunk, 1.0f), true }, { "Dirty", p->dirt, true }, { "Sporty", p->fitness, false }, diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c index 4145e7c..0cb7d6d 100644 --- a/entropie/src/game/game_dev.c +++ b/entropie/src/game/game_dev.c @@ -11,7 +11,7 @@ typedef enum { DEV_WHO, DEV_TELEPORT, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY, DEV_BLADDER, DEV_HUNGER, DEV_THIRST, DEV_SLEEPY, DEV_STRENGTH, DEV_ANNOYED, - DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_WILLING, + DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING, DEV_LIKES, /* likes for each job ... */ DEV_SKILLS = DEV_LIKES + 4, /* ... skill at each job */ DEV_ROWS = DEV_SKILLS + 4 @@ -122,6 +122,7 @@ void game_dev(Game *game, const Input *input) { case DEV_NERVES: o->temperament.nerves = step01(o->temperament.nerves, dir, 0.1f); break; case DEV_CLUMSY: o->body.quirk[3] = stepSigned(o->body.quirk[3], dir, 0.2f); break; /* (clumsiness = 0.5 + 0.5 x this) */ case DEV_WITS: o->wits = step01(o->wits, dir, 0.05f); break; + case DEV_HEIGHTS: o->height_fear = step01(o->height_fear, dir, 0.05f); break; case DEV_WILLING: c->willing = step01(c->willing, dir, 0.1f); break; default: break; } @@ -158,6 +159,7 @@ static void devLine(const Object *o, int row, char *out) { case DEV_NERVES: snprintf(out, 96, "Nerves (calm .. restless): %.1f", o->temperament.nerves); break; case DEV_CLUMSY: snprintf(out, 96, "Clumsy: %.0f%%", (0.5f + 0.5f * o->body.quirk[3]) * 100.0f); break; case DEV_WITS: snprintf(out, 96, "Wits: %.2f", o->wits); break; + case DEV_HEIGHTS: snprintf(out, 96, "Afraid of heights: %.2f", o->height_fear); break; case DEV_WILLING: snprintf(out, 96, "Willing: %.0f%%", c->willing * 100.0f); break; default: out[0] = '\0'; break; } diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index 433eaae..3c92f0b 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -9,13 +9,40 @@ #include "world/physics.h" #include "world/nav.h" #include "world/grid.h" +#include "world/world.h" #include "render/shapes.h" /* ---------- Small helpers ---------- */ /* Walks along dir (unit, horizontal) at speed; the body turns after it. */ +/* Up on a roof: careful steps (the fearful creep, the drunk and dim stroll about and stumble). */ +#define HIGH_PACE 1.5f /* m/s up there at most (nobody runs on a roof) ... */ +#define HIGH_SLOW 0.5f /* ... slower by this much at most (all caution) */ +#define HIGH_STUMBLE 0.12f /* stumbles per second, drunk and careless */ + +static float upHighSpeed(Object *o, float speed, float dt) { + Ai *ai = &o->ai; + if (!g_world || !Object_upHigh(o, Terrain_height(&g_world->terrain, o->position.x, o->position.z))) { + ai->high_time = fmaxf(0.0f, ai->high_time - dt); + if (ai->high_time <= 0.0f) ai->high_said = false; + return speed; + } + ai->high_time = fminf(ai->high_time + dt, 5.0f); + float c = Object_caution(o), d = fminf(o->drunk, 1.0f); + if (!ai->high_said && ai->high_time > 1.0f && ai->speech_time <= 0.0f) { + ai->high_said = true; + if (c > 0.6f) Ai_say(o, "Don't look down... don't look down..."); + else if (c < 0.2f && d > 0.5f) Ai_say(o, "Woo! Look at me up here!"); + } + if (c < 0.25f && d > 0.5f && o->body.stumble <= 0.0f && dt > 0.0f && + random01(ai) < HIGH_STUMBLE * d * dt) + Body_trip(&o->body, 0, Object_facing(o), 0.4f + 0.3f * d); + return fminf(speed, HIGH_PACE * (1.0f - HIGH_SLOW * c)); +} + void ai_walk(Object *o, Vec3 dir, float speed, float dt) { speed *= Object_vigor(o) * (o->tempo > 0.0f ? 0.8f + 0.2f * o->tempo : 1.0f); /* (their own pace) */ + speed = upHighSpeed(o, speed, dt); /* Drunk people wobble off course */ float wobble = sinf(o->body.clock * 0.7f) + 0.6f * sinf(o->body.clock * 1.9f + 1.3f); dir = rotateY(dir, wobble * 0.5f * fminf(o->drunk, 1.0f)); @@ -955,6 +982,10 @@ void Ai_help(Object *self, Object *leader) { if (self->hands.state[hand] == HAND_HOLDING && !toolOf(self->hands.item[hand])) Hands_drop(self, hand); Vec3 right = vec3_normalize(vec3_cross(Object_facing(leader), UP)); + if (ai->order != ORDER_NONE && (ai->mode == AI_WORK || ai->before == AI_WORK)) { /* (called away from a job: remembered) */ + ai->paused_order = ai->order; + ai->paused_area = ai->work_area; + } ai->mode = AI_HELP; ai->order = ORDER_NONE; ai->leader = leader; @@ -974,6 +1005,22 @@ void Ai_stopHelping(Object *self) { ai->home = self->position; /* hang around where we helped */ stand(self); rest(ai, PAUSE_MIN, PAUSE_MAX); + /* Called away from a job: back to it - unless they forget (then they wait to be reminded) */ + if (ai->paused_order != ORDER_NONE && 0.6f * self->wits + 0.4f * ai->character.willing - 0.3f * fminf(self->drunk, 1.0f) > 0.42f) { + Order back = ai->paused_order; + Ai_order(self, back, ai->paused_area); + Ai_say(self, back == ORDER_FIRE ? "I'll get back to the fire then." : back == ORDER_TRASH ? "Back to the tidying then." + : back == ORDER_TREES ? "Back to the trees then." : "Right, back to the firewood."); + } +} + +bool Ai_resumePaused(Object *self) { + Ai *ai = &self->ai; + if (ai->paused_order == ORDER_NONE || ai->mode == AI_WORK) return false; + Order back = ai->paused_order; + if (ai->mode == AI_HELP) Ai_stopHelping(self); + if (ai->mode != AI_WORK) Ai_order(self, back, ai->paused_area); + return true; } bool Ai_isHelping(const Object *self) { @@ -992,6 +1039,7 @@ void Ai_order(Object *self, Order order, Vec3 area) { dropEverything(self); ai->mode = AI_WORK; ai->order = order; + ai->paused_order = ORDER_NONE; ai->pending = ORDER_NONE; ai->showing = SHOW_NONE; ai->leader = NULL; diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h index 48c2780..da6e775 100644 --- a/entropie/src/people/ai.h +++ b/entropie/src/people/ai.h @@ -67,7 +67,7 @@ typedef enum { typedef enum { ORDER_NONE, ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE, ORDER_COUNT } Order; /* How someone takes a break from work */ -typedef enum { BREAK_NONE, BREAK_SIT, BREAK_WALK, BREAK_CHAT } BreakKind; +typedef enum { BREAK_NONE, BREAK_SIT, BREAK_WALK, BREAK_CHAT, BREAK_COFFEE } BreakKind; /* Who someone is */ typedef struct { @@ -188,8 +188,14 @@ typedef struct { float prep_time; bool chat_near; /* on a chat break: close enough (left again only well beyond) */ float hold_idle; + bool hold_there; /* ... at the spot (stands there till pushed well off it) */ float ladder_dir; /* on a ladder: +1 going up, -1 down */ float ladder_seek; /* until they look for a way up / down again */ + Order paused_order; /* called away from this job (to help): back to it after, or when reminded ... */ + Vec3 paused_area; /* ... there */ + float high_time; /* seconds up high (a roof) ... */ + bool high_said; /* ... said something about it */ + bool heights_said; /* told they won't go up there (once each time they're asked) */ float ladder_climb_t; /* seconds up a ladder that leads nowhere */ /* holding a ladder nobody is on: seconds of that (then they let go) */ float sun_heat; /* seconds of hard work in the midday sun (the shirt comes off) */ float sniff_cooldown; /* until the next remark about a smell */ @@ -202,6 +208,9 @@ typedef struct { float on_break; /* on a break (seconds left) */ Vec3 break_from; /* BREAK_WALK: where they set out from (and come back to) */ bool break_back; /* ... on the way back */ + BreakKind break_now; /* this break: mostly their own kind, now and then another */ + struct Object *break_at; /* BREAK_COFFEE: the stove (a coffee there) */ + float break_sip; /* ... until the next sip */ struct Object *chat_with; /* BREAK_CHAT: who they're chatting with */ float chat_line; /* ... until the next thing said */ int chat_said; @@ -211,6 +220,8 @@ typedef struct { void Ai_help(struct Object *self, struct Object *leader); void Ai_stopHelping(struct Object *self); bool Ai_isHelping(const struct Object *self); +/* Called away from a job and didn't go back to it on their own: back to it now (reminded). False: no such job. */ +bool Ai_resumePaused(struct Object *self); /* Says something (a speech bubble for a few seconds). */ void Ai_say(struct Object *self, const char *text); diff --git a/entropie/src/people/ai_help.c b/entropie/src/people/ai_help.c index 43d13d8..e95491b 100644 --- a/entropie/src/people/ai_help.c +++ b/entropie/src/people/ai_help.c @@ -700,12 +700,16 @@ bool ai_holdLadder(Object *self, Object *const *objects, int count, float dt) { } Vec3 facing, spot = Ladder_holdSpot(l, &facing); float d = flatDistance(spot, self->position); - if (d > 0.35f) { - Vec3 dir = ai_steer(self, NULL, objects, count, spot); + if (d > (ai->hold_there ? 0.2f : 0.1f)) { /* (right up to it; once there, a little shove doesn't send them walking) */ + ai->hold_there = false; + Vec3 to = vec3_sub(spot, self->position); + to.y = 0.0f; + Vec3 dir = d < 0.7f ? vec3_scale(to, 1.0f / d) : ai_steer(self, NULL, objects, count, spot); /* (the last bit straight there) */ if (vec3_length(dir) < 0.5f) stand(self); else ai_walk(self, dir, fminf(WALK_SPEED, d * 2.5f + 0.3f), dt); return true; } + ai->hold_there = true; stand(self); turnTowards(self, facing, dt * 2.0f); return true; @@ -776,6 +780,12 @@ bool ai_followByLadder(Object *self, Object *const *objects, int count, const Ob if (fabsf(up) < LADDER_UP_FROM) return false; if (ai->ladder_seek > 0.0f) { ai->ladder_seek -= dt; return false; } bool going_up = up > 0.0f; + if (going_up && self->height_fear * (1.0f - 0.5f * fminf(self->drunk, 1.0f)) > 0.6f) { /* afraid of heights: not up there */ + if (!ai->heights_said) { ai->heights_said = true; Ai_say(self, "No way - I'm not going up there. I'll wait down here."); } + ai->ladder_seek = 20.0f; + return false; + } + if (!going_up) ai->heights_said = false; Object *l = going_up ? ladderBetween(self, objects, count, self->position, leader->position, false) : ladderBetween(self, objects, count, leader->position, self->position, true); if (!l && going_up && up < CLIMB_REACH * self->look.height / 1.75f + 0.2f) { /* no ladder, but in reach: pull up */ diff --git a/entropie/src/people/ai_work.c b/entropie/src/people/ai_work.c index eb3fc64..0d73ec3 100644 --- a/entropie/src/people/ai_work.c +++ b/entropie/src/people/ai_work.c @@ -827,53 +827,117 @@ static Object *chatPartner(const Object *self, Object *const *objects, int count return best; } -static void nextBreak(Ai *ai) { - ai->streak = ai->character.break_every * (0.7f + 0.6f * random01(ai)); +#define BREAK_OWN 0.6f /* their own kind of break this often, else another */ +#define BREAK_LONG 0.3f /* a long one this often (BREAK_LONGER x), else short (BREAK_SHORTER x) */ +#define BREAK_LONGER 1.8f +#define BREAK_SHORTER 0.7f +#define COFFEE_RANGE 70.0f /* m: a stove this near is worth a coffee */ + +static void nextBreak(Object *self) { + Ai *ai = &self->ai; + ai->streak = ai->character.break_every * (0.7f + 0.6f * random01(ai)) * (1.0f - 0.4f * self->worn); /* (worn out: sooner) */ } static void endBreak(Object *self) { Ai *ai = &self->ai; ai->on_break = 0.0f; ai->chat_with = NULL; + ai->break_at = NULL; ai->task_seen = NULL; /* (whoever thinks things over does so again) */ self->sitting = false; - nextBreak(ai); + nextBreak(self); Ai_say(self, random01(ai) < 0.5f ? "Right, back to it!" : "Okay, back to work."); } -/* Now and then a break, their way: sitting down, a little walk, a chat. True while on it. */ +static Object *stoveNear(const Object *self, Object *const *objects, int count) { + Object *best = NULL; + float best_d = COFFEE_RANGE; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o->type != OBJ_FURNITURE || o->variant != FURN_STOVE) continue; + float d = flatDistance(o->position, self->position); + if (d < best_d) { best = o; best_d = d; } + } + return best; +} + +/* Which break this time: mostly their own way, now and then a coffee, a walk, a chat or a sit. */ +static BreakKind pickBreak(Object *self, Object *const *objects, int count) { + Ai *ai = &self->ai; + if (random01(ai) < BREAK_OWN) return ai->character.break_kind; + static const BreakKind KINDS[] = { BREAK_SIT, BREAK_WALK, BREAK_CHAT, BREAK_COFFEE }; + BreakKind k = KINDS[(int)(random01(ai) * 4.0f) % 4]; + if (k == BREAK_COFFEE && !stoveNear(self, objects, count)) k = BREAK_SIT; + if (k == BREAK_CHAT && !chatPartner(self, objects, count)) k = BREAK_SIT; + return k; +} + +/* Now and then a break, short or long, their way or another: sitting down, + a little walk, a chat, a coffee at the stove. The tool goes down first. True while on it. */ static bool takeBreak(Object *self, Object *const *objects, int count, float dt) { Ai *ai = &self->ai; const Character *c = &ai->character; if (c->break_kind == BREAK_NONE || c->break_every <= 0.0f) return false; if (ai->on_break <= 0.0f) { - if (ai->streak <= 0.0f) nextBreak(ai); + if (ai->streak <= 0.0f) nextBreak(self); if ((ai->streak -= dt) > 0.0f || Tools_isWorking(self) || firewoodInHands(self) > 0) return false; - ai->on_break = c->break_length * (0.7f + 0.6f * random01(ai)); + ai->break_now = pickBreak(self, objects, count); + bool long_one = random01(ai) < BREAK_LONG; + ai->on_break = c->break_length * (long_one ? BREAK_LONGER : BREAK_SHORTER) * (0.8f + 0.4f * random01(ai)) * (1.0f + 0.5f * self->worn); ai->break_from = self->position; ai->break_back = false; - ai->chat_with = c->break_kind == BREAK_CHAT ? chatPartner(self, objects, count) : NULL; + ai->break_at = NULL; + ai->break_sip = 0.0f; + ai->chat_with = ai->break_now == BREAK_CHAT ? chatPartner(self, objects, count) : NULL; ai->chat_line = 0.0f; ai->chat_said = 0; - if (c->break_kind == BREAK_WALK) { - float a = random01(ai) * 2.0f * PI_F, d = 7.0f + 5.0f * random01(ai); + if (self->hands.item[HAND_LEFT] || self->hands.item[HAND_RIGHT]) Hands_setAside(self, ai->set_aside); /* (the tool down: taken again after) */ + if (self->worn > 0.45f && random01(ai) < 0.5f) Ai_say(self, "Ugh, I'm still beat from yesterday."); + else if (ai->break_now == BREAK_WALK) Ai_say(self, long_one ? "I need some air. Back in a bit." : "I'll stretch my legs for a bit."); + else if (ai->break_now == BREAK_COFFEE) Ai_say(self, "Coffee time."); + else if (!ai->chat_with) Ai_say(self, long_one ? "I need a proper break." : "Time for a little sit-down."); + if (ai->break_now == BREAK_WALK) { + float a = random01(ai) * 2.0f * PI_F, d = (long_one ? 12.0f : 7.0f) + 5.0f * random01(ai); setGoal(ai, vec3_add(self->position, vec3(cosf(a) * d, 0.0f, sinf(a) * d))); - Ai_say(self, "I'll stretch my legs for a bit."); - } else if (!ai->chat_with) { - Ai_say(self, "Time for a little sit-down."); + } else if (ai->break_now == BREAK_COFFEE) { + ai->break_at = stoveNear(self, objects, count); + if (ai->break_at) setGoal(ai, ai->break_at->position); + else ai->break_now = BREAK_SIT; } } ai->on_break -= dt; - if (c->break_kind == BREAK_WALK) { /* out a bit, and back */ + if (ai->break_now == BREAK_COFFEE && ai->break_at) { /* over to the stove, a coffee there, and back */ + Object *stove = ai->break_at; + Vec3 front = vec3_add(stove->position, vec3_scale(stove->colliders[0].axes[2], stove->size.z * 0.5f + 0.5f)); + Vec3 goal = ai->break_back ? ai->break_from : front; + float d = flatDistance(goal, self->position); + if (!ai->break_back && d < 0.6f) { /* there: drinking it, a sip now and then */ + stand(self); + turnTowards(self, vec3_sub(stove->position, self->position), dt); + if ((ai->break_sip -= dt) <= 0.0f) { + ai->break_sip = 6.0f + 6.0f * random01(ai); + if (ai->speech_time <= 0.0f && random01(ai) < 0.4f) Ai_say(self, random01(ai) < 0.5f ? "Ahh. Good coffee." : "Mm."); + } + if (ai->on_break <= 0.0f) { ai->break_back = true; setGoal(ai, ai->break_from); } + return true; + } + if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) { endBreak(self); return false; } + if (!ai->break_back && isStuck(ai, d, dt)) { ai->break_back = true; setGoal(ai, ai->break_from); } + Vec3 dir = ai_steer(self, NULL, objects, count, goal); + if (vec3_length(dir) < 0.5f) stand(self); + else ai_walk(self, dir, WALK_SPEED * 0.6f, dt); + return true; + } + if (ai->break_now == BREAK_WALK) { /* out a bit, and back */ Vec3 goal = ai->break_back ? ai->break_from : ai->goal; float d = flatDistance(goal, self->position); - if (!ai->break_back && (d < 1.0f || isStuck(ai, d, dt) || ai->on_break < c->break_length * 0.4f)) { + if (!ai->break_back && (d < 1.0f || isStuck(ai, d, dt) || ai->on_break < c->break_length * 0.3f)) { ai->break_back = true; setGoal(ai, ai->break_from); } else if (ai->break_back && (d < 1.2f || isStuck(ai, d, dt))) { - endBreak(self); - return false; + if (ai->on_break > 3.0f) ai->break_now = BREAK_SIT; /* (back early: the rest of it sitting) */ + else { endBreak(self); return false; } } Vec3 dir = ai_steer(self, NULL, objects, count, goal); if (vec3_length(dir) < 0.5f) stand(self); diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h index e45315c..0744e53 100644 --- a/entropie/src/people/body.h +++ b/entropie/src/people/body.h @@ -26,7 +26,10 @@ enum { HAND_LEFT = 0, HAND_RIGHT = 1 }; +#define BODY_ANKLE_Y 0.075f /* the ankle over the sole, standing flat (a 1.75 m person; scale with height) */ + /* How a person carries themselves. All -1..1 (nerves 0..1); 0 is neutral. */ + typedef struct { float pride; /* -1 = shy, closed up .. 1 = proud, chest out */ float energy; /* -1 = tired, slumped .. 1 = lively */ diff --git a/entropie/src/people/body_internal.h b/entropie/src/people/body_internal.h index 8a0f3f5..0f5ac50 100644 --- a/entropie/src/people/body_internal.h +++ b/entropie/src/people/body_internal.h @@ -16,7 +16,7 @@ #define HIP_DROP 0.05f #define THIGH 0.44f #define SHIN 0.42f -#define ANKLE_Y 0.075f +#define ANKLE_Y BODY_ANKLE_Y #define FOOT_LENGTH 0.15f #define FOOT_X 0.10f #define SPINE 0.36f diff --git a/entropie/src/people/condition.c b/entropie/src/people/condition.c index a264738..d7d67be 100644 --- a/entropie/src/people/condition.c +++ b/entropie/src/people/condition.c @@ -33,6 +33,9 @@ #define REST_STANDING 0.030f #define REST_WALKING 0.008f #define REST_SLEEPING 0.050f +/* Worn out over the days (see Object.worn) */ +#define WORN_PER_EFFORT 0.012f /* per stamina used up (an afternoon of hard work: about 0.7) */ +#define WORN_SLEEP_HOUR 0.075f /* taken off per game hour asleep (a night: about 0.6) */ static unsigned rng = 0x2545F491u; @@ -107,6 +110,8 @@ static void needs(Object *self, float dt, float game_dt) { effort *= 1.0f + TIRED_EFFORT * tired; if (!asleep) rest *= 1.0f - TIRED_REST * tired; n->stamina = clampf(n->stamina + (effort > 0.0f ? -effort : rest) * dt, 0.0f, fmaxf(cap, 0.0f)); + if (effort > 0.0f) self->worn = fminf(1.0f, self->worn + WORN_PER_EFFORT * effort * dt * (1.15f - 0.3f * self->fitness)); + if (asleep) self->worn = fmaxf(0.0f, self->worn - WORN_SLEEP_HOUR * hours * (1.0f + 0.25f * self->temperament.energy)); if (n->stamina > cap) n->stamina = cap; float hard = effort > 0.0f ? WORK_THIRST : 1.0f; diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c index 9bb8c82..d2ff540 100644 --- a/entropie/src/people/talk.c +++ b/entropie/src/people/talk.c @@ -2,6 +2,7 @@ #include "world/camp.h" #include "world/tools.h" #include "people/pockets.h" +#include "world/site.h" #include <stdio.h> #include <string.h> @@ -43,7 +44,7 @@ static const char *const CHOICES[NODE_COUNT][TALK_CHOICES] = { [NODE_ASK] = { TASK_CHOICES, "No, thanks" }, [NODE_SUGGEST] = { "Good idea, go ahead", "Help me instead", "Not now" }, [NODE_HOW] = { "I'll show you", "Never mind" }, - [NODE_WHERE] = { "I'll show you where", "Just pile it up here", "Leave it where it is" }, + [NODE_WHERE] = { "I'll show you where", "Find a good place for it", "Leave it where it is" }, [NODE_PLACE] = { "Yes, stack it here", "Not yet, keep following", "Forget it" }, [NODE_WATCHING] = { "Watch closely", "Never mind" }, [NODE_ASK_HAND] = { "Sure", "Not now" }, @@ -93,6 +94,12 @@ static const char *lineFor(const Object *o, int node) { } } +/* A job they were called away from and haven't gone back to (ORDER_NONE: none). */ +static const char *const JOB_WORD[ORDER_COUNT] = { "", "firewood", "trees", "tidying", "fire" }; +static Order pausedJob(const Object *npc) { + return npc->ai.mode != AI_WORK ? npc->ai.paused_order : ORDER_NONE; +} + static void go(Talk *talk, int node) { talk->node = node; talk->waited = 0.0f; @@ -143,6 +150,54 @@ static Object *woodpileHere(World *world, const Object *player) { return World_add(world, pile) ? pile : NULL; } +/* Where they'd stack it themselves: a woodpile there already (within PILE_SEEK), else a + spot near the work: clear, level, off the roads, by a wall if there's one. */ +#define PILE_SEEK 40.0f +#define PILE_RING0 3.0f /* m from the work: the ring searched */ +#define PILE_RING1 9.0f +static float pileSpotScore(World *world, Vec3 p, Vec3 work) { + float g = Terrain_height(&world->terrain, p.x, p.z); + if (World_groundHeight(world, p.x, p.z, g + 3.0f) > g + 0.05f) return -1.0f; /* (something stands there) */ + float slope = fabsf(Terrain_height(&world->terrain, p.x + 0.8f, p.z) - Terrain_height(&world->terrain, p.x - 0.8f, p.z)) + + fabsf(Terrain_height(&world->terrain, p.x, p.z + 0.8f) - Terrain_height(&world->terrain, p.x, p.z - 0.8f)); + if (slope > 0.25f || Site_road(p.x, p.z) > 0.2f) return -1.0f; + float score = 1.0f - slope - 0.03f * flatDistance(p, work); + for (int i = 0; i < world->count; i++) { + const Object *o = world->objects[i]; + if (o->collider_count == 0 || o->is_character || o->type == OBJ_DECAL || o->type == OBJ_PUDDLE || o->type == OBJ_STINK) continue; + float d = flatDistance(o->position, p) - Object_reach(o); + if (d > 2.0f) continue; + if (!o->is_static && d < 0.8f) return -1.0f; /* (loose things lying there) */ + if (o->is_static && d < 0.6f) return -1.0f; + if (o->is_static && Object_reach(o) > 1.5f) score += 0.5f; /* (a wall to stack it against) */ + } + return score; +} + +static Object *pileOwnPlace(World *world, const Object *npc) { + Vec3 work = npc->ai.work_area; + for (int i = 0; i < world->count; i++) { + Object *o = world->objects[i]; + if (o->type == OBJ_WOODPILE && flatDistance(o->position, work) < PILE_SEEK) return o; + } + Vec3 best = work; + float best_score = -1.0f; + for (int r = 0; r < 4; r++) + for (int k = 0; k < 16; k++) { + float rad = PILE_RING0 + (PILE_RING1 - PILE_RING0) * (float)r / 3.0f, a = (float)k * PI_F / 8.0f; + Vec3 p = vec3_add(work, vec3(cosf(a) * rad, 0.0f, sinf(a) * rad)); + float sc = pileSpotScore(world, p, work); + if (sc > best_score) { best_score = sc; best = p; } + } + if (best_score < 0.0f) return NULL; + best.y = Terrain_height(&world->terrain, best.x, best.z); + const Vec3 size = WOODPILE_SIZE; + Object *pile = Object_create(OBJ_WOODPILE, PALETTE_WARM, best, size, 0.0f); + if (!pile) return NULL; + Object_setRotation(pile, vec3(0.0f, yaw_along(vec3_sub(best, work)) + PI_F * 0.5f, 0.0f)); /* (lengthwise across the way to it) */ + return World_add(world, pile) ? pile : NULL; +} + static bool woodJob(Order what) { return what == ORDER_FIREWOOD || what == ORDER_TREES; } /* A job: they may say no. Wood jobs: unless they need to be shown how, or @@ -254,7 +309,13 @@ static void answer(Talk *talk, World *world, Object *player, int choice) { Object *npc = talk->with; switch (talk->node) { case NODE_HELLO: - if (choice == 1) go(talk, NODE_TASKS); + if (choice == 1 && pausedJob(npc)) { + Order job = pausedJob(npc); + Ai_resumePaused(npc); + char reply[64]; + snprintf(reply, sizeof reply, shy(npc) ? "Oh... sorry. The %s." : "Oh right - the %s!", JOB_WORD[job]); + wrapUp(talk, reply); + } else if (choice == 1) go(talk, NODE_TASKS); else if (choice == 2) { if (npc->ai.character.willing < 0.3f && npc->needs.stamina < 0.5f) wrapUp(talk, "Maybe later, I'm beat."); else { Ai_help(npc, player); wrapUp(talk, shy(npc) ? "Okay..." : "Sure, I'm with you."); } @@ -311,10 +372,11 @@ static void answer(Talk *talk, World *world, Object *player, int choice) { break; case NODE_WHERE: if (choice == 1) { Ai_show(npc, SHOW_PLACE, player); wrapUp(talk, "Lead the way. Click where it should go."); } - else if (choice == 2) { - Object *pile = woodpileHere(world, player); - if (pile) Ai_setWoodpile(npc, pile); - wrapUp(talk, "Got it."); + else if (choice == 2) { /* they think it over, then pick a place */ + Object *pile = pileOwnPlace(world, npc); + Body_think(&npc->body, 1.0f + npc->ai.character.ponder); + if (pile) { Ai_setWoodpile(npc, pile); wrapUp(talk, pile->stored ? "There's a pile already - I'll put it there." : "Hm... I'll stack it over there."); } + else { npc->ai.no_pile = true; wrapUp(talk, "Hm, no good spot here. I'll leave it lying."); } } else { npc->ai.no_pile = true; wrapUp(talk, "Okay, I'll leave it there."); } break; case NODE_PLACE: @@ -445,6 +507,11 @@ int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) { for (int k = 0; k < TALK_CHOICES; k++) if (CHOICES[talk->node][k]) out[n++] = CHOICES[talk->node][k]; if (talk->node == NODE_WORKER && !woodJob(talk->with->ai.order)) out[1] = "Do something else instead"; + if (talk->node == NODE_HELLO && pausedJob(talk->with)) { /* called away from a job, and forgot it: a reminder */ + static char line[64]; + snprintf(line, sizeof line, "Back to the %s, please", JOB_WORD[pausedJob(talk->with)]); + out[0] = line; + } if (talk->node == NODE_HELLO && g_talk_player) { static const char *const LIGHTER_LINE[] = { NULL, "Can I have my lighter back?", "Here's your lighter back", "Can I borrow your lighter?", "Can I have a cigarette?", "Want a cigarette?" }; diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c index 2af8047..eda10c4 100644 --- a/entropie/src/world/ladder.c +++ b/entropie/src/world/ladder.c @@ -36,7 +36,8 @@ #define STEP_WALL_GAP 0.08f /* ... its back feet this far from that wall */ #define STEP_OFF_UP 0.9f /* ... from its top you get onto that if it's no higher than this over the top */ #define FREE_AHEAD 0.6f /* stood up on its own (nothing to lean on): this far in front of you */ -#define HOLD_OUT 0.6f /* somebody holding it stands this far out from its foot */ +#define HOLD_OUT 0.3f /* somebody holding it stands this far out from its foot ... */ +#define HOLD_BESIDE 0.48f /* ... and this far to the side of its middle (the climber gets on and off in front) */ static const Vec3 UP = { 0.0f, 1.0f, 0.0f }; @@ -226,12 +227,18 @@ float Ladder_steadiness(const Object *l) { void Ladder_hold(Object *who, Object *l) { if (who->holding_ladder && who->holding_ladder->held_by == who) who->holding_ladder->held_by = NULL; who->holding_ladder = l; - if (l) l->held_by = who; + if (!l) return; + l->held_by = who; + Vec3 across = vec3_normalize(vec3_cross(l->lean_normal, UP)); + l->hold_side = vec3_dot(vec3_sub(who->position, l->position), across) >= 0.0f ? 1.0f : -1.0f; /* (the side they come from) */ } Vec3 Ladder_holdSpot(const Object *l, Vec3 *facing) { - *facing = vec3_scale(l->lean_normal, -1.0f); - Vec3 at = vec3_add(l->position, vec3_scale(l->lean_normal, HOLD_OUT)); + Vec3 across = vec3_normalize(vec3_cross(l->lean_normal, UP)); + float side = l->hold_side != 0.0f ? l->hold_side : 1.0f; + /* beside its foot, a little out, turned to it (half facing the wall) */ + *facing = vec3_normalize(vec3_add(vec3_scale(across, -side), vec3_scale(l->lean_normal, -0.6f))); + Vec3 at = vec3_add(l->position, vec3_add(vec3_scale(l->lean_normal, HOLD_OUT), vec3_scale(across, side * HOLD_BESIDE))); at.y = l->position.y; return at; } @@ -720,12 +727,12 @@ void Ladder_poseHands(Object *who, float dt) { if (!l && atHold(who)) { /* holding it steady: both hands on the rails at chest height */ const Object *hl = who->holding_ladder; Vec3 across = vec3_normalize(vec3_cross(hl->lean_normal, UP)); - float s = who->look.height / 1.75f; - for (int h = 0; h < 2; h++) { - float side = h == HAND_LEFT ? 1.0f : -1.0f; - Vec3 at = pointAt(hl, 1.15f * s); + float s = who->look.height / 1.75f, side = hl->hold_side != 0.0f ? hl->hold_side : 1.0f; + for (int h = 0; h < 2; h++) { /* both on the rail on their side, one higher (the hand nearer the wall) */ + bool high = (h == HAND_LEFT) == (side > 0.0f); + Vec3 at = pointAt(hl, (high ? 1.3f : 0.95f) * s); who->body.hand_active[h] = true; - who->body.hand_target[h] = vec3_add(at, vec3_add(vec3_scale(across, side * LADDER_WIDTH * 0.5f), vec3_scale(hl->lean_normal, 0.03f))); + who->body.hand_target[h] = vec3_add(at, vec3_add(vec3_scale(across, side * (LADDER_WIDTH * 0.5f + 0.03f)), vec3_scale(hl->lean_normal, 0.02f))); } return; } @@ -755,9 +762,10 @@ void Ladder_poseFeet(Object *who, float dt) { if (!l && atHold(who) && who->body.pose == POSE_UP) { /* holding it: a foot on the bottom rung */ const Object *hl = who->holding_ladder; float s = who->look.height / 1.75f; - Foot *foot = &who->body.feet[HAND_RIGHT]; Vec3 across = vec3_normalize(vec3_cross(hl->lean_normal, UP)); - Vec3 goal = vec3_add(pointAt(hl, rung(hl, 0)), vec3_add(vec3_scale(hl->lean_normal, 0.06f * s), vec3_scale(across, -0.06f * s))); + float side = hl->hold_side != 0.0f ? hl->hold_side : 1.0f; + Foot *foot = &who->body.feet[side > 0.0f ? HAND_RIGHT : HAND_LEFT]; /* (the foot nearer it: on the end of the bottom rung) */ + Vec3 goal = vec3_add(pointAt(hl, rung(hl, 0)), vec3_add(vec3_scale(hl->lean_normal, 0.06f * s), vec3_scale(across, side * (LADDER_WIDTH * 0.5f - 0.08f)))); goal.y += 0.06f * s; /* (ankle over the rung) */ float k = fminf(1.0f, 6.0f * dt); foot->ankle = vec3_lerp(foot->ankle, goal, k); diff --git a/entropie/src/world/ladder.h b/entropie/src/world/ladder.h index 4a07f5b..5073734 100644 --- a/entropie/src/world/ladder.h +++ b/entropie/src/world/ladder.h @@ -51,7 +51,7 @@ float Ladder_steadiness(const Object *ladder); void Ladder_topple(World *world, Object *ladder, Vec3 dir); /* Holding it steady at its foot (both hands on the rails, a foot on the bottom rung), or not (NULL). */ void Ladder_hold(Object *who, Object *ladder); -/* Where somebody stands to hold it (behind its foot), and which way they face. */ +/* Where somebody stands to hold it (beside its foot, out of the climber's way), and which way they face. */ Vec3 Ladder_holdSpot(const Object *ladder, Vec3 *facing); /* Climbing: hands on the rungs (after Hands_update) and feet on them (after Body_update); diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index 75ffbfb..bb64bb9 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -102,6 +102,7 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size obj->top_speed = SPRINT_SPEED; obj->fitness = 0.5f; obj->wits = 0.5f; + obj->height_fear = 0.3f; obj->jump_speed = JUMP_SPEED; obj->tap_timer = 1e3f; obj->position = position; @@ -551,6 +552,23 @@ float Object_reach(const Object *obj) { return r; } +#define HIGH_UP 1.6f /* m over the ground: up high */ + +bool Object_upHigh(const Object *obj, float ground_y) { + const Object *on = obj->standing_on; + if (on && on->type == OBJ_WALL && on->variant == 3) return true; /* (a roof) */ + return obj->is_grounded && obj->position.y - ground_y > HIGH_UP; +} + +float Object_caution(const Object *obj) { + float d = fminf(obj->drunk, 1.0f); + float c = 0.35f + 0.5f * obj->height_fear - 0.15f * obj->fitness; /* (sporty: surer-footed) */ + c += d * (obj->wits - 0.45f) * 0.8f; /* drunk: the sharp ones extra careful, the dim ones careless */ + return clampf(c, 0.0f, 1.0f); +} + +#define WORN_FEEL 0.6f /* worn out from the days before: this much tireder at most */ + /* Worn down by the day: how sleepy, felt more by the sluggish, the unfit and the drunk, hardly by the lively and sporty (0 = fresh .. 1 = done in). */ float Object_tiredness(const Object *obj) { @@ -558,7 +576,8 @@ float Object_tiredness(const Object *obj) { float sleepy = clampf((obj->needs.sleepiness - 0.25f) / 0.75f, 0.0f, 1.0f); float feels = (1.0f - 0.35f * clampf(obj->temperament.energy, -1.0f, 1.0f)) * (1.2f - 0.4f * obj->fitness) + 0.4f * fminf(obj->drunk, 1.0f); - return clampf(sleepy * feels, 0.0f, 1.0f); + float worn = WORN_FEEL * obj->worn * (1.15f - 0.3f * obj->fitness); /* (yesterday still in the bones) */ + return clampf(sleepy * feels + worn, 0.0f, 1.0f); } float Object_vigor(const Object *obj) { diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index bd1a75e..c1c8dca 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -273,7 +273,9 @@ typedef struct Object { float chill; /* characters: 0..1 cold (night, away from a fire): a jacket on over 0.6 */ float shake; /* ladders: wobbling (seconds left) */ float stability; /* ladders: how steady it stands, leaning (0..1; somebody holding it: steady) */ - struct Object *held_by; /* ladders: somebody holding it steady at its foot ... */ + struct Object *held_by; /* ladders: somebody holding it steady at its foot (beside it, hold_side: +1 / -1 along + cross(lean_normal, up), out of the climber's way) ... */ + float hold_side; struct Object *holding_ladder; /* characters: ... this one */ float tip, tip_v; /* ladders: falling over: tipped this far from where it stood (rad), this fast ... */ Vec3 tip_from, tip_dir; /* ... from that axis, towards that (horizontal) way */ @@ -311,6 +313,8 @@ typedef struct Object { float top_speed; /* sprinting flat out, fresh and sober (m/s) */ float fitness; /* characters: how sporty (0..1), whatever the build: quicker, surer, smoother over things */ float wits; /* characters: 0 = slow on the uptake .. 1 = sharp (sees what's wrong and sorts it out) */ + float worn; /* characters: worn by the work of the days (0..1): sleep takes it off, a hard day leaves some for the next */ + float height_fear; /* characters: 0 = no head for heights needed .. 1 = won't go up a ladder at all */ float jump_speed; /* take-off speed of a standing jump, fresh and sober (m/s) */ float run_speed; /* player: current speed, eased (m/s) */ float sprint_boost; /* player: 0 = jogging .. 1 = flat out (tapping Shift) */ @@ -338,6 +342,11 @@ float Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir /* How far obj's colliders reach from its position at most (a bounding sphere). */ float Object_reach(const Object *obj); +/* Characters: up high (on a roof, or well over the ground)? */ +bool Object_upHigh(const Object *obj, float ground_y); +/* Characters up high: how careful (0 not at all .. 1 creeping): fear, drink and wits (drunk and dim: careless). */ +float Object_caution(const Object *obj); + /* Characters: 1 when fresh, down to 0.5 when exhausted (speed, strength, work). */ float Object_vigor(const Object *obj); /* Worn down by the day (sleepiness, felt by personality, fitness and drink): 0 fresh .. 1 done in. */ diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c index fbe5e19..f760c95 100644 --- a/entropie/src/world/physics.c +++ b/entropie/src/world/physics.c @@ -248,7 +248,7 @@ static bool tryStepUp(Object *obj, int io, const Object *other, int ic, Vec3 nor if (vec3_length(mv) < 0.1f || vec3_length(out) < 1e-4f) return false; if (vec3_dot(vec3_normalize(mv), vec3_normalize(out)) > -0.4f) return false; const Collider *c = &other->colliders[ic]; - if (c->axes[1].y < WALKABLE_SLOPE) return false; /* top face not walkable */ + if (fmaxf(fabsf(c->axes[0].y), fmaxf(fabsf(c->axes[1].y), fabsf(c->axes[2].y))) < WALKABLE_SLOPE) return false; /* no face up flat enough (whichever axis: firewood lies on its side) */ float top = Object_colliderCenter(other, ic).y + projectRadius(c, WORLD_UP); if (fmaxf(fabsf(c->axes[0].y), fmaxf(fabsf(c->axes[1].y), fabsf(c->axes[2].y))) < 0.98f) { @@ -880,8 +880,8 @@ static void beginStepOver(Object *obj, const Terrain *terrain, Vec3 dir, float n if (fabsf(lateral) < 0.08f * s) lateral = (k == 0 ? -0.1f : 0.1f) * s; /* (feet apart) */ float at = k == obj->over_lead ? land_at : land_at + 0.08f * s; Vec3 land = vec3_add(obj->position, vec3_add(vec3_scale(dir, at), vec3_scale(right, lateral))); - land.y = up || !terrain ? obj->position.y + top + (b->feet[k].ankle.y - obj->position.y) - : Terrain_height(terrain, land.x, land.z) + (b->feet[k].ankle.y - Terrain_height(terrain, b->feet[k].ankle.x, b->feet[k].ankle.z)); + /* (standing flat there: the ankle its own height over the surface - not lifted as it may be now, on the toes) */ + land.y = (up || !terrain ? obj->position.y + top : Terrain_height(terrain, land.x, land.z)) + BODY_ANKLE_Y * s; obj->over_land[k] = land; } obj->movement = vec3_zero(); diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c index 1fefce5..4a7caab 100644 --- a/entropie/src/world/scene.c +++ b/entropie/src/world/scene.c @@ -87,6 +87,8 @@ static const bool LIGHTER[] = { true, true, false, false, true }; /* (a lighter /* Smoking: how much (0 = not at all), a pack, tobacco to roll (Mila smokes now and then, and bums them). */ static const float SMOKER[] = { 0.3f, 0.6f, 0.0f, 0.25f, 0.9f }; static const float WITS[] = { 0.6f, 0.45f, 0.8f, 0.6f, 0.25f }; /* (Ben is slow on the uptake) */ +static const float HEIGHT_FEAR[] = { 0.2f, 0.1f, 0.85f, 0.15f, 0.5f }; /* (Ida won't go up a ladder) */ +static const float YESTERDAY[] = { 0.15f, 0.2f, 0.1f, 0.25f, 0.55f }; /* worn from the days before (Ben: a long one) */ static const int CIGARETTES[] = { 10, 0, 0, 0, 6 }; static const int TOBACCO[] = { 0, 12, 0, 0, 20 }; static const int GARMENT[] = { GARMENT_TROUSERS, GARMENT_SHORTS, GARMENT_SKIRT, GARMENT_DRESS, GARMENT_TROUSERS }; @@ -195,6 +197,8 @@ Object *Scene_load(World *world) { obj->lighter_uses = obj->lighter ? LIGHTER_USES * (1 + characters % 3) / 3 : 0; /* (some nearly empty) */ obj->smoker = SMOKER[characters % TEMPERAMENT_COUNT]; obj->wits = WITS[characters % TEMPERAMENT_COUNT]; + obj->height_fear = HEIGHT_FEAR[characters % TEMPERAMENT_COUNT]; + obj->worn = YESTERDAY[characters % TEMPERAMENT_COUNT]; obj->jacket_hung = true; /* (on the rack in your garage) */ obj->cigarettes = CIGARETTES[characters % TEMPERAMENT_COUNT]; obj->tobacco = TOBACCO[characters % TEMPERAMENT_COUNT]; diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index be54359..1073da4 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -811,6 +811,8 @@ static void animateCharacter(World *world, Object *o, float dt) { const Object *on = o->standing_on; float w = on && o->is_grounded && !o->on_ladder && !o->sitting ? Physics_footing(on) : 1e9f; o->body.balance_goal = 1.0f - smoothstepf(BALANCE_NARROW, BALANCE_WIDE, w); + if (o->type == OBJ_HUMAN && !o->sitting && Object_upHigh(o, Terrain_height(&world->terrain, o->position.x, o->position.z))) + o->body.balance_goal = fmaxf(o->body.balance_goal, 0.5f * Object_caution(o)); /* (up on a roof: arms out a little, the careful more) */ } BodyFrame frame = { .position = o->position,