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.75.0: pockets (Urinellas, lighter), hands free to pee, landings, ladders
- Urinellas: one-time use, up to 4 in the pocket; a box by the toilet refills it (the others fetch more when they run out, you click it) - Peeing: what's in the hands is put down first (the others take it up again); standing, the fly is open or the skirt or dress lifted - Lighter: needed to light the fire, runs empty; borrowed when there's none, given back (or not), owners ask for it; talk: borrow, give back, ask for yours back - Working hard in the midday sun the shirt comes off (women keep a top) - Dropping from high up: knees pulled up, legs reach for the ground, deep into the knees, a roll or a crash - Ladders: one-handed climbing (you may slip), a lying ladder is stepped over, kicked it hardly moves and your toe hurts - Dev menu: teleport to the parkour / yard; the player spawns 1 m up Includes 1.74.0 (if not committed separately): - Fix: legs showed through a skirt or dress standing normally; the thighs under it are cloth now and it's wider over the hips - Mila's Urinellas come in a pack of 3 (then she squats); now and then a mishap (on the shoes); used ones go into the bag, are dropped or flung away (drunk) as trash Includes 1.73.0 (if not committed separately): - Clothes: trousers, shorts, skirt, dress; squatting to pee trousers come down, a skirt or dress is gathered up Includes 1.72.0 (if not committed separately): - Fix: walking off the other way from something half stepped over left one foot planted behind, sliding along Includes 1.71.0 (if not committed separately): - Two-handed fidgets: rubbing the hands, hands behind the head, both hands on the hips Includes 1.70.0 (if not committed separately): - More one-hand gestures (neck, nose, ear, hair, thumb in the belt, hand on the chest, shading the eyes); mood and personal habits pick them Includes 1.69.0 (if not committed separately): - Faster with a crowd: 750 people 22 -> 14 ms a frame (kind index, cell grid for company, no foot checks far from you) Includes 1.68.0 (if not committed separately): - The walking map is updated in place when fixed things change (0.3 ms instead of 10 ms) Includes 1.67.0 (if not committed separately): - Pour your drink on the fire (C): a small fire goes out, a big one hisses, vodka flares up Includes 1.66.0 (if not committed separately): - Careful people sort the woodpile by length (front even) and put the branches lying about onto a heap at the end of the woodpile Includes 1.65.0 (if not committed separately): - Smooth off the ladder (a step back, one foot after the other) and taking it down (the hand reaches for the rail at chest height) Includes 1.64.0 (if not committed separately): - Smooth ladder and climbs: getting on, over the edge at the top, feet from where they are; the easy 1 m climb no longer drops you off again Includes 1.63.0 (if not committed separately): - The hose melts in the fire (leaks, burns through); duct tape and the shears mend it Includes 1.62.0 (if not committed separately): - Smooth pull-ups: no hop at the end, feet and knees ease over the edge, elbows and hands taking hold blend in (audit walk: joint jumps 161 -> 58) Includes 1.61.0 (if not committed separately): - A step ladder in the tool room: stands on its own, at a wall with its back to it; climb onto the wall from its top Includes 1.60.0 (if not committed separately): - A folding ladder (two 2.5 m halves on a hinge, 5 m open) beside the telescopic one; one click at a wall unfolds it and leans it there Includes 1.59.0 (if not committed separately): - Stepping up onto blocks above knee high; set moves and jumps only for things standing on the ground in front of you Includes 1.58.0 (if not committed separately): - Walking over knee-high things is one smooth set move that always finishes; at a run: jump up onto blocks, catch climbable edges
entropie/Makefile | 2 +- entropie/README.md | 26 ++++++++++-- entropie/STATE.md | 28 +++++++++---- entropie/assets/shaders/figure.frag | 34 +++++++++------ entropie/commit_message.txt | 19 ++++++++- entropie/src/game/game_actions.c | 68 +++++++++++++++++++++++++++--- entropie/src/game/game_dev.c | 30 ++++++++++++- entropie/src/people/ai.c | 84 ++++++++++++++++++++++++++++++++++++- entropie/src/people/ai.h | 4 ++ entropie/src/people/ai_internal.h | 2 + entropie/src/people/ai_work.c | 48 ++++++++++++++++++++- entropie/src/people/body.c | 43 ++++++++++++++++++- entropie/src/people/body.h | 5 +++ entropie/src/people/body_pose.c | 8 +++- entropie/src/people/hands.c | 23 ++++++++++ entropie/src/people/hands.h | 3 ++ entropie/src/people/pockets.c | 44 +++++++++++++++++++ entropie/src/people/pockets.h | 25 +++++++++++ entropie/src/people/talk.c | 39 ++++++++++++++++- entropie/src/render/figure.h | 5 ++- entropie/src/render/renderer.c | 2 + entropie/src/render/renderer.h | 2 +- entropie/src/world/ladder.c | 1 + entropie/src/world/object.c | 7 +++- entropie/src/world/object.h | 15 +++++-- entropie/src/world/physics.c | 18 +++++--- entropie/src/world/scene.c | 7 +++- entropie/src/world/site.c | 5 +++ entropie/src/world/world.c | 30 +++++++++++++ 29 files changed, 571 insertions(+), 56 deletions(-) diff --git a/entropie/Makefile b/entropie/Makefile index 9b20a4c..ec6f7aa 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 1.74.0 +VERSION := 1.75.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index 5277aa2..f499ce0 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -64,6 +64,7 @@ and satellite pictures (about 340 x 340 m). round it and some wood beside it. Three long iron rods meet over it; a grill hangs from them on a chain and swings a little - Click on it with firewood or a branch in hand to put it in, click again to light it + (with the lighter in your pocket; none, or empty: ask somebody for theirs) - Click on it with food in hand to put it on the grill. Looking at the fire shows how far it is (raw, % done, done, getting black, burnt). Click the fire with a free hand to take the most done piece off @@ -161,6 +162,9 @@ and satellite pictures (about 340 x 340 m). the toes, that leg swung out). Once started it always finishes - Walking at a block a bit higher than that (up to about the hip) with room on top: you step up onto it the same way, one leg after the other +- Dropping from high up: the knees come up to the belly, the legs reach + down just before the ground. Over about 1.3 m you go deep into the knees; + over about 2.5 m the fit roll it off, the others crash (energy, dirt) - Running into something sober: low, you trip or go headlong over it; high, you stagger back or go down to the side you came in from. How badly depends on speed, fitness, energy and clumsiness @@ -350,6 +354,11 @@ A hand that can't reach its grip lets go and takes hold again. gutter of a roof); too short for that, it leans against the face - 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 + slip: a wobble first, then off you go. Fitness, practice, clumsiness, + energy and drink decide +- A ladder lying on the ground is stepped over. Kicked, it hardly moves, + but your toe hurts - E / Q (or right click, hands empty) at its foot takes it down again; a hint shows when you stand there @@ -401,7 +410,12 @@ say no more often and don't come to you for a bit. ground shows the spot, and a left click puts the woodpile there - Trash: they take a garbage bag, bag piece after piece and empty full bags in the container; a bag is carried by its neck, the arm a little out -- The fire: they keep wood in it, light it when it gets dark and sit by it +- The fire: they keep wood in it, light it when it gets dark and sit by it. + No lighter (Ida, Mila): they borrow one from somebody near, maybe you +- Pockets: you, Jonas and Ben have a lighter. It runs empty over time. A + borrowed one goes back when they think of it; owners ask for theirs +- Working hard in the midday sun the shirt comes off (women keep a top on; + the shy keep it all on); it goes back on when they've cooled down - Ask them what they're doing, send them back to helping you, or give them a break ## Day and needs @@ -417,8 +431,12 @@ say no more often and don't come to you for a bit. Aim with the mouse, hold Shift to push harder (further, quicker done). Next to the toilet (the blue cabin by the barn) P takes you inside; the others use it too when it's not too far, and wait if it's occupied -- Women squat to pee, unless they have a Urinella (Mila has a pack of 3). - Squatting, trousers come down to the knees, a skirt or dress is gathered up. +- Women squat to pee, unless they have a Urinella: one-time use, up to 4 + in the pocket. Out of them, they fetch more from the box by the toilet + (you too: click it). Squatting, trousers come down to the knees, a skirt + or dress is gathered up; standing, the fly is open or the skirt lifted +- Before peeing, what's in the hands is put down beside you (the others + take it up again after) A used Urinella goes into the bag (tidy), is dropped (careless) or flung away (drunk); now and then it ends up on the shoes - Clothes: trousers, shorts (Jonas), a skirt (Ida) or a dress (Mila) @@ -474,7 +492,7 @@ say no more often and don't come to you for a bit. | V | Third / first person | | I | Show all keys in game | | Tab | Your stats (Left / Right: other page) | -| F1 | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right) | +| F1 | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right); teleport you to the parkour or the yard | | Esc | Menu: resume, keys, quit (the game waits meanwhile) | The game starts in third person. diff --git a/entropie/STATE.md b/entropie/STATE.md index d2e8229..43b82b9 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -17,7 +17,8 @@ Updated with every version (see the version line at the end). | Tempo / thinks first (s) | 1 / 0 | 1.25 / .2 | .95 / 1.6 | 1.1 / .6 | .75 / 2.4 | | Break | – | none | a little walk | a chat | sits down | | Main hand | right | right | left | right | right | -| Pees | standing | standing | squatting | standing (Urinella) | standing | +| Pees | standing | standing | squatting | standing (Urinella, 3 in the pocket) | standing | +| Lighter in the pocket (lights) | 20 | 40 | – | – | 40 | Alice the dog: follows, can be petted and called. @@ -47,15 +48,19 @@ Tools they shouldn't use (`ai_work.c`, Ai_shouldntUse): the chainsaw with tree s |---|---| | Idle / wander | stand about (fidgets, gestures), stroll near home | | Drink | fetch a bottle, a few sips; empty: onto a table or crate in reach, with other empties, into the container, down at the feet, or tossed (careless, drunk, wild) | -| Supply | eat or drink at the apple crate / water barrel | -| Pee | the toilet if free and within 40 m (waits a while, less when urgent); else a hidden spot within 40 m: behind a bush, tree, wall, building or car, away from the others; used spots welcome; not where someone else is going; standing: face it, aim at it; squatting: back to it | +| Supply | eat or drink at the apple crate / water barrel; out of Urinellas (bladder under 0.6): fill the pocket at the box by the toilet | +| Pee | hands free first (`Hands_setAside`: down beside them, taken up again after if still there); the toilet if free and within 40 m (waits a while, less when urgent); else a hidden spot within 40 m: behind a bush, tree, wall, building or car, away from the others; used spots welcome; not where someone else is going; standing: face it, aim at it; squatting: back to it | | Wake | shake someone asleep or passed out awake | | Help (G) | push or lift along with you, copy what you do | -| Work (an order) | firewood (fetch tools, fell, delimb, cut, split, stack), trees, trash (bag it, empty bags), fire (feed, light at dusk, sit by it) | +| Work (an order) | firewood (fetch tools, fell, delimb, cut, split, stack), trees, trash (bag it, empty bags), fire (feed, light at dusk, sit by it; no working lighter: borrow one from somebody within 30 m - walk over, "Lend me your lighter"; nobody has one: ask out loud every 45 s) | | Follow | being shown how, or where the woodpile goes | | Approach | come to you with an offer, a question, a suggestion; left without an answer they ask again, then give up (patience: 12 s x (1 - .35 energy) x (1 - .3 pride), 5..25 s: Jonas, Mila ~8 s, Ida, Ben ~15 s) and are annoyed (+0.4, fades in 3 min: frown, less willing, no approaching while > 0.3) | | Tidy up | tidy ones pick up trash on the way | +Pockets (`src/people/pockets.c`): Urinellas (POCKET_URINELLAS 4, one-time use), a lighter (`lighter`, `lighter_uses`, LIGHTER_USES 60 full; `lighter_of` = whose it is when borrowed). Lighter manners (`ai.c` lighterManners, every 3-7 s): a borrower near the owner (2.5 m, not lighting the fire) gives it back with a chance (0.45 to people, 0.12 to you; less drunk, more careful); an owner near the borrower (4 m) asks for it - another person gives it back (drunk over 0.6: "What lighter?" half the time), you get asked. Talk (hello, choice 3 instead of "Who are you?"): "Can I have my lighter back?" / "Here's your lighter back" / "Can I borrow your lighter?" (annoyed over 0.6: "Get your own"). + +Shirt off (`ai_work.c` ai_sunShirt): `sun_heat` grows 11:00-17:30 while felling or making firewood (or helping with a tool), faster when tired; at 90 s the shirt comes off (`look.top`: TOP_BARE men, TOP_CROP women; not the shy, pride under -0.3), under 40 it goes back on (cools fast after 17:30). Policy "no shirt no problem" for now; maybe "no shirt no service" later. + Getting about: straight at it, round things in the way (local look-ahead); leads into a corner: a way round from the walking map (`src/world/nav.c`, 0.4 m grid of fixed things, A*). When fixed things change (cheap signature over all of them) only the cells round what moved, turned, came or went are done again (a table of each fixed thing as last built, by pointer; more than 32 changes at once: the whole map): a felled tree 0.3 ms instead of 10 ms, the same cells as a whole rebuild. They remember 4 things they couldn't get at. Watchdog (every behaviour except waiting ones: idle, help, follow, drink, breaks, talking, sitting): nothing happening for 25 s (150 s while working on something): let go of the target and look again; again: give the job up ("I'm stuck here"). @@ -93,6 +98,12 @@ Talk topics: offer, ask, suggest, where (new woodpile spot), ask for a hand, ask ## Things +Ladder kicks (`physics.c` push / noteBump): a lying ladder is stepped over (only a leaning one is never), walked into it moves 0.15 of what a loose thing would, at most 0.25 m/s; walking into it counts as a bump: "Ow..." / "Ouch! My toe!" (drunk: a stagger). + +One-handed ladder (`game_actions.c` oneHandSlip): climbing with something up to 5 kg in the other hand. Slip risk a second while moving: 0.16 x (1.3 - fitness) x (0.6 + clumsy) x (1 + 3 drunk) x (1.5 - energy) x (1 - 0.9 practice); `ladder_skill` grows 0.01 a second of it. First slip a wobble (3 s), the next one a fall (on the bottom rung: you just slip off). + +Landing from high up (`world.c` landFromHigh, `body.c` Body_land): the fastest fall speed in the air (`air_fall`) at touchdown. In the air, falling over 2 m/s with the ground 0.5 m+ below: feet pulled up (knees to the belly), 0.12-0.28 s before touchdown the legs reach down. Landing: under 5 m/s nothing extra; 5-7 m/s into the knees (`land_squat`); 7-9.5 a roll (chance: sure = fitness, energy, sober, sure-footed) or a crash (Body_fall, -0.25 energy, dirt); over 9.5 always a crash ("my ankle"). The player now spawns 1 m up (was 4: that would be a crash). + Ladder: one click at a wall slides it out to fit (top on the edge, 0.9 m over it; up to its 6 m) and leans it; slid out by hand, it keeps that length. Putting it up is a 1.1 s animation (`ladder.c`, putUp): stood upright, slid out, tipped onto the wall; you stand still with both hands on it (`game.c`, puttingUp); it can only be climbed or taken once it leans. Drink on the fire (`camp.c` Camp_pourOnFire, C right at a burning fire with a bottle): 0.1 l a sip left goes on as water (beer, water); enough for the fire (Camp_waterNeeded: 0.2 l + 0.25 l per kg) puts it out, else it hisses; vodka flares (the water on it is gone). Hint shows when you stand there with a drink. @@ -129,7 +140,7 @@ Ground: meadow wears into paths; tar, concrete, gravel and tracks are part of th - Backspace: call off showing someone how / where (they say "never mind" and go back to what they did) - Esc: menu (the game waits): Resume, Keys (show / hide), Quit; mouse or Up / Down + Enter -- Dev menu (F1): pick anyone (Left / Right on the first line) and change: female, Urinella, sporty, drunk, energy, bladder, hunger, thirst, sleepy, strength, annoyed, pride, lively, nerves, clumsy, willing, likes and skill for each job (Up / Down: line, the list scrolls; Left / Right: value) +- Dev menu (F1), second line: teleport you to the parkour (Right) or the yard (Left). Then pick anyone (Left / Right on the first line) and change: female, Urinella, sporty, drunk, energy, bladder, hunger, thirst, sleepy, strength, annoyed, pride, lively, nerves, clumsy, willing, likes and skill for each job (Up / Down: line, the list scrolls; Left / Right: value) - `START_AT_COURSE` in `src/game/game.c`: start at the obstacle course (1) or in the courtyard (0, default); the course is there either way ## Speed @@ -151,7 +162,10 @@ Most important first. - More gestures and feelings to pick personalities from: the lists in Body / Fidgets and Gestures can grow **People** -- Peeing, still to do: open the fly standing; some women don't like the Urinella +- Smoking: tobacco to roll or a pack of cigarettes per person (shared, asked for, offered), needs a lighter, used up over time; rolling, lighting, in hand or mouth, smoke +- Peeing, still to do: some women don't like the Urinella; a Urinella in the hand (not drawn); the player-woman picks up her things again herself +- Shirt off for you (now only the others); "no shirt no service" policy (decision open) +- NPCs on ladders (only you climb them) - Clothing, still to do: more outfits for extra people (they all wear trousers), jackets at night **Tools for us** @@ -161,4 +175,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.74.0 (build 6c0e954)` +State of `entropie 1.75.0 (build 2ada6fc)` diff --git a/entropie/assets/shaders/figure.frag b/entropie/assets/shaders/figure.frag index 5948b73..7e16967 100644 --- a/entropie/assets/shaders/figure.frag +++ b/entropie/assets/shaders/figure.frag @@ -38,7 +38,8 @@ uniform vec3 stream[4]; // pee or sick: an arc from the body to the ground ( uniform int streamKind; // 0 none, 1 pee, 2 sick, 3 water uniform float streamWidth; uniform int garment; // 0 trousers, 1 shorts, 2 skirt, 3 dress -uniform float undress; // squatting to pee: trousers down / skirt up (0..1) +uniform float undress; // to pee: squatting trousers down / skirt up (0..1); standing fly open / skirt lifted in front (-1..0) +uniform int top; // 0 a shirt, 1 bare-chested, 2 just a top (shirt off in the sun) uniform mat4 viewProj; uniform mat4 view; @@ -87,20 +88,21 @@ void addPart(vec3 a, vec3 b, float r1, float r2, float blend, float depth, int m void buildBody() { vec3 pelvis = joints[J_PELVIS], chest = joints[J_CHEST], neck = joints[J_NECK]; vec3 waist = mix(pelvis, chest, 0.45) + waistBend; - int forearm = longSleeves ? M_SHIRT : M_SKIN; + int forearm = longSleeves && top == 0 ? M_SHIRT : M_SKIN; + int sleeve = top == 0 ? M_SHIRT : M_SKIN; - // Torso: belly into chest, shoulders across - addPart(pelvis + spineDir * 0.05, waist, 0.130, 0.135, 0.04, 0.70, M_SHIRT); - addPart(waist, chest + spineDir * 0.03 - chestForward * 0.005, 0.135, 0.160, 0.04, 0.66, M_SHIRT); - addPart(joints[J_SHOULDER_L], joints[J_SHOULDER_R], 0.050, 0.050, 0.05, 0.75, M_SHIRT); + // Torso: belly into chest, shoulders across (the shirt off: skin, or just a top) + addPart(pelvis + spineDir * 0.05, waist, 0.130, 0.135, 0.04, 0.70, sleeve); + addPart(waist, chest + spineDir * 0.03 - chestForward * 0.005, 0.135, 0.160, 0.04, 0.66, top == 1 ? M_SKIN : M_SHIRT); + addPart(joints[J_SHOULDER_L], joints[J_SHOULDER_R], 0.050, 0.050, 0.05, 0.75, sleeve); // What's on the legs: trousers, shorts, a skirt or a dress (squatting to pee: // trousers pulled down to the knees, a skirt or dress gathered up) - bool down = undress > 0.5; + bool down = undress > 0.5, lifted = undress < -0.5; int lower = garment == 3 ? M_SHIRT : M_PANTS; int hips = (garment == 0 && down) ? M_SKIN : lower; // (under a skirt or dress the thighs wear it too: where a leg presses out, it's cloth, not leg) - int thigh = garment >= 2 ? (down ? M_SKIN : lower) : (garment == 0 && !down) || garment == 1 ? M_PANTS : M_SKIN; + int thigh = garment >= 2 ? (down || lifted ? M_SKIN : lower) : (garment == 0 && !down) || garment == 1 ? M_PANTS : M_SKIN; int shin = garment == 0 ? M_PANTS : M_SKIN; // Pelvis spans the hips, so the thighs grow out of it @@ -110,10 +112,12 @@ void buildBody() { // A skirt or dress: a cone from the waist down to the knees (wider as they part) if (garment >= 2) { vec3 knees = 0.5 * (joints[J_KNEE_L] + joints[J_KNEE_R]); - vec3 top = pelvis + spineDir * 0.07; - vec3 hem = mix(knees + vec3(0.0, -0.06, 0.0), top, 0.6 * undress); - float wide = 0.5 * length(joints[J_KNEE_L] - joints[J_KNEE_R]) + 0.08; - addPart(top, hem, 0.165, max(wide, 0.165), 0.03, 0.8, lower); // (over the hips and thighs, wider as the knees part) + vec3 waistline = pelvis + spineDir * 0.07; + float lift = max(-undress, 0.0); // (standing: gathered up in front, held at the hips) + vec3 hem = mix(knees + vec3(0.0, -0.06, 0.0), waistline, 0.6 * max(undress, 0.0) + 0.45 * lift) + + chestForward * 0.05 * lift; + float wide = (0.5 * length(joints[J_KNEE_L] - joints[J_KNEE_R]) + 0.08) * (1.0 - 0.3 * lift); + addPart(waistline, hem, 0.165, max(wide, 0.165), 0.03, 0.8, lower); // (over the hips and thighs, wider as the knees part) } // Legs and feet @@ -130,7 +134,7 @@ void buildBody() { // Arms and hands for (int s = 0; s < 2; s++) { int o = s * 4; - addPart(joints[J_SHOULDER_L + o], joints[J_ELBOW_L + o], 0.055, 0.045, 0.035, 1.0, M_SHIRT); + addPart(joints[J_SHOULDER_L + o], joints[J_ELBOW_L + o], 0.055, 0.045, 0.035, 1.0, sleeve); addPart(joints[J_ELBOW_L + o], joints[J_WRIST_L + o], 0.044, 0.034, 0.02, 1.0, forearm); addPart(joints[J_WRIST_L + o], joints[J_HAND_L + o], 0.033, 0.036, 0.015, 1.0, M_SKIN); } @@ -275,6 +279,10 @@ vec3 clothDetail(int mat, vec3 base, vec3 p) { vec3 side = cross(spineDir, chestForward); if (belt < 0.018) base = vec3(0.25, 0.17, 0.12); if (belt < 0.012 && abs(dot(rel, side)) < 0.02 && dot(rel, chestForward) > 0.0) base = vec3(0.75, 0.70, 0.50); + // standing to pee: the fly open (underwear showing in the slit) + float along = dot(rel, spineDir); + if (undress < -0.5 && garment <= 1 && along < 0.04 && along > -0.07 && dot(rel, chestForward) > 0.03 && + abs(dot(rel, side)) < 0.012 + 0.012 * smoothstep(-0.07, 0.03, along)) base = vec3(0.90, 0.90, 0.87); } else if (mat == M_SHIRT) { // Collar: a band around the base of the neck vec3 rel = p - joints[J_CHEST]; diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index 769d497..e0084eb 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,5 +1,20 @@ -entropie 1.74.0: no legs through the dress; Urinella pack and disposal - +entropie 1.75.0: pockets (Urinellas, lighter), hands free to pee, landings, ladders + +- Urinellas: one-time use, up to 4 in the pocket; a box by the toilet + refills it (the others fetch more when they run out, you click it) +- Peeing: what's in the hands is put down first (the others take it up + again); standing, the fly is open or the skirt or dress lifted +- Lighter: needed to light the fire, runs empty; borrowed when there's + none, given back (or not), owners ask for it; talk: borrow, give back, + ask for yours back +- Working hard in the midday sun the shirt comes off (women keep a top) +- Dropping from high up: knees pulled up, legs reach for the ground, deep + into the knees, a roll or a crash +- Ladders: one-handed climbing (you may slip), a lying ladder is stepped + over, kicked it hardly moves and your toe hurts +- Dev menu: teleport to the parkour / yard; the player spawns 1 m up + +Includes 1.74.0 (if not committed separately): - Fix: legs showed through a skirt or dress standing normally; the thighs under it are cloth now and it's wider over the hips - Mila's Urinellas come in a pack of 3 (then she squats); now and then a diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c index 5da9518..cb92e14 100644 --- a/entropie/src/game/game_actions.c +++ b/entropie/src/game/game_actions.c @@ -4,10 +4,12 @@ */ #include "game/game_internal.h" +#include "people/pockets.h" #include <stdlib.h> #define SUPPLY_REACH 0.8f +#define ONE_HAND_LADDER_KG 5.0f /* up a ladder one-handed with this much in the other hand at most */ #define APPLE_REACH 1.6f /* from the trunk of an apple tree */ #define BAG_INTERVAL 0.35f /* holding the button: the next piece into the bag */ @@ -338,6 +340,31 @@ void game_climb(Game *game, const Input *in, float dt) { } /* On a ladder: W / S climb, at the top W gets you over the edge, Space jumps off. */ +/* Climbing a ladder with one hand (something in the other): the unpractised, + clumsy, unfit, tired or drunk may slip - a wobble first, then off they go. + Every climb like that is practice. */ +static bool oneHandSlip(Game *game, Object *p, Object *l, bool moving, float dt) { + p->ladder_wobble = fmaxf(0.0f, p->ladder_wobble - dt); + bool one_hand = (p->hands.state[HAND_LEFT] == HAND_HOLDING) != (p->hands.state[HAND_RIGHT] == HAND_HOLDING); + if (!one_hand || !moving || p->ladder_off > 0.0f) return false; + p->ladder_skill = fminf(1.0f, p->ladder_skill + 0.01f * dt); + float risk = 0.16f * (1.3f - p->fitness) * (0.6f + Body_clumsiness(&p->body)) * (1.0f + 3.0f * fminf(p->drunk, 1.0f)) + * (1.5f - p->needs.stamina) * (1.0f - 0.9f * p->ladder_skill); + if ((float)rand() / (float)RAND_MAX >= risk * dt) return false; + if (p->ladder_wobble <= 0.0f) { /* first a wobble: caught it */ + p->ladder_wobble = 3.0f; + say(game, "Whoa - you nearly slip!"); + return false; + } + Vec3 n = l->lean_normal; + bool low = p->ladder_at < 0.6f; + Ladder_getOff(p); + p->velocity = vec3(n.x * 1.5f, 0.5f, n.z * 1.5f); + if (!low) Body_fall(&p->body, n); + say(game, low ? "You slip off the bottom rung" : "You slip and fall off the ladder!"); + return true; +} + void game_climbLadder(Game *game, const Input *in, float dt) { Object *p = game->player; Object *l = p->on_ladder; @@ -349,6 +376,7 @@ void game_climbLadder(Game *game, const Input *in, float dt) { say(game, "You jump off the ladder"); return; } + if (oneHandSlip(game, p, l, in->move_forward != 0.0f, dt)) return; if (Ladder_climb(p, in->move_forward, dt) != CLIMB_AT_EDGE) return; /* Over the edge onto whatever it leans on, if there's room to stand */ Vec3 n = l->lean_normal, edge = Ladder_edge(l), over = edge; @@ -422,12 +450,18 @@ static bool useLadder(Game *game) { bool at_top = false; ladder = Ladder_near(&game->world, p, &at_top); if (!ladder) return false; - if (p->hands.state[HAND_LEFT] != HAND_FREE || p->hands.state[HAND_RIGHT] != HAND_FREE) { - say(game, "You need both hands free to climb"); + /* one hand free is enough for the skilled (the other holds something small): the others may slip */ + Object *in_hand = p->hands.item[HAND_LEFT] ? p->hands.item[HAND_LEFT] : p->hands.item[HAND_RIGHT]; + bool free_l = p->hands.state[HAND_LEFT] == HAND_FREE, free_r = p->hands.state[HAND_RIGHT] == HAND_FREE; + bool one_hand = free_l != free_r && in_hand && !p->hands.grabbed && + p->hands.state[free_l ? HAND_RIGHT : HAND_LEFT] == HAND_HOLDING && in_hand->weight <= ONE_HAND_LADDER_KG; + if (!(free_l && free_r) && !one_hand) { + say(game, in_hand && in_hand->weight > ONE_HAND_LADDER_KG ? "Too heavy to take up the ladder" : "You need a hand free to climb"); return true; } Ladder_mount(p, ladder, at_top); - say(game, at_top ? "Down the ladder: S" : "Up the ladder: W, down: S, Space: jump off"); + if (one_hand) say(game, "One-handed up the ladder - careful! W / S, Space: jump off"); + else say(game, at_top ? "Down the ladder: S" : "Up the ladder: W, down: S, Space: jump off"); return true; } @@ -467,6 +501,17 @@ static bool takeFromSupply(Game *game) { to.y = 0.0f; if (vec3_length(to) > 1e-3f && vec3_dot(vec3_normalize(to), facing) < 0.3f) continue; + if (o->dispenses == OBJ_URINELLA) { /* into the pocket, not the hand */ + if (!p->female) { say(game, "You pee standing anyway"); return true; } + int got = Pockets_refill(p); + p->urinella = true; + if (got == 0) say(game, "Your pocket is full of Urinellas already"); + else { + snprintf(game->info, sizeof(game->info), "%d Urinella%s into your pocket (%d now)", got, got == 1 ? "" : "s", p->urinellas); + say(game, game->info); + } + return true; + } int hand = p->hands.state[HAND_RIGHT] == HAND_FREE ? HAND_RIGHT : p->hands.state[HAND_LEFT] == HAND_FREE ? HAND_LEFT : -1; if (hand < 0) { say(game, "Your hands are full"); return true; } @@ -711,8 +756,13 @@ static bool tendFire(Game *game) { return true; } if (!fire->burning) { - if (Camp_light(fire)) { say(game, "You light the fire"); Ai_practice(p, ORDER_FIRE, 0.03f); } - else say(game, "Put firewood or branches in first"); + if (fire->fuel <= 0.0f) say(game, "Put firewood or branches in first"); + else if (!p->lighter) say(game, "No lighter in your pocket - ask somebody (talk to them)"); + else if (!Pockets_light(p)) say(game, "Your lighter is empty - click, click, nothing"); + else if (Camp_light(fire)) { + say(game, p->lighter_uses == 0 ? "You light the fire - that was the lighter's last flame" : "You light the fire"); + Ai_practice(p, ORDER_FIRE, 0.03f); + } return true; } tell(game, fire); @@ -972,6 +1022,12 @@ void game_handleActions(Game *game, const Input *input) { if (p->needs.bladder <= 0.15f) say(game, "No need right now"); else if (toilet && toilet->dog.mode) say(game, "Occupied!"); else if (toilet) { Object_useToilet(p, toilet); say(game, "*in the toilet*"); } - else Body_pee(&p->body, Object_peeTime(p)); + else { + Object *aside[2]; + bool full = p->hands.item[HAND_LEFT] || p->hands.item[HAND_RIGHT]; + Hands_setAside(p, aside); /* (hands free: the dress up, the fly down) */ + Body_pee(&p->body, Object_peeTime(p)); + if (full) say(game, "You put your things down first"); + } } } diff --git a/entropie/src/game/game_dev.c b/entropie/src/game/game_dev.c index 8432f63..a6431ed 100644 --- a/entropie/src/game/game_dev.c +++ b/entropie/src/game/game_dev.c @@ -9,7 +9,7 @@ #include "game/game_internal.h" typedef enum { - DEV_WHO, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_DRUNK, DEV_ENERGY, + 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_WILLING, DEV_LIKES, /* likes for each job ... */ @@ -46,6 +46,27 @@ static float stepSigned(float v, int dir, float step) { return fminf(1.0f, fmaxf(-1.0f, v + (float)dir * step)); } +/* Dev only: you to the parkour (Right) or back to the yard (Left). */ +static void teleport(Game *game, int dir) { + Object *p = game->player; + Vec3 to, along; + if (dir > 0) Site_courseFrame(&to, &along); + else { to = Site_yard(); along = vec3_normalize(vec3_sub(Site_landmark(), to)); } + if (p->on_ladder) Ladder_getOff(p); + for (int h = 0; h < 2; h++) if (p->hands.state[h] != HAND_FREE) Hands_drop(p, h); + Hands_release(p); + p->climbing = p->sitting = false; + p->position = vec3(to.x, World_groundHeight(&game->world, to.x, to.z, to.y + 50.0f) + 0.05f, to.z); + p->velocity = p->movement = vec3_zero(); + p->rotation.y = atan2f(along.z, along.x) + PI_F / 2.0f; + p->body.replant = true; + Object_syncColliders(p); + game->camera.yaw = atan2f(along.z, along.x) * 180.0f / PI_F; + game->camera.pitch = 0.0f; + Camera_rotate(&game->camera, 0.0f, 0.0f); + say(game, dir > 0 ? "Teleported to the parkour" : "Teleported to the yard"); +} + /* Sporty changes top speed and jump too (as when the person was made). */ static void setFitness(Object *o, float fit) { float was = 1.0f + 0.25f * (o->fitness - 0.5f), now = 1.0f + 0.25f * (fit - 0.5f); @@ -65,6 +86,10 @@ void game_dev(Game *game, const Input *input) { game->dev_who += dir; return; } + if (game->dev_row == DEV_TELEPORT) { + teleport(game, dir); + return; + } Object *o = picked(game); if (!o) return; Needs *n = &o->needs; @@ -82,7 +107,7 @@ void game_dev(Game *game, const Input *input) { } switch ((DevRow)row) { case DEV_FEMALE: o->female = !o->female; break; - case DEV_URINELLA: o->urinella = !o->urinella; o->urinellas = o->urinella ? URINELLA_PACK : 0; break; + case DEV_URINELLA: o->urinella = !o->urinella; o->urinellas = o->urinella ? POCKET_URINELLAS : 0; break; case DEV_SPORTY: setFitness(o, step01(o->fitness, dir, 0.05f)); break; case DEV_DRUNK: o->drunk = fminf(1.5f, fmaxf(0.0f, o->drunk + (float)dir * 0.1f)); break; case DEV_ENERGY: n->stamina = step01(n->stamina, dir, 0.1f); break; @@ -115,6 +140,7 @@ static void devLine(const Object *o, int row, char *out) { } switch ((DevRow)row) { case DEV_WHO: snprintf(out, 96, "Who: %s%s", o->name, o->type == OBJ_PLAYER ? " (you)" : ""); break; + case DEV_TELEPORT: snprintf(out, 96, "Teleport you: <- yard parkour ->"); break; case DEV_FEMALE: snprintf(out, 96, "Female: %s", o->female ? "yes" : "no"); break; case DEV_URINELLA: snprintf(out, 96, "Urinella: %s (%d left)", o->urinella ? "yes" : "no", o->urinellas); break; case DEV_SPORTY: snprintf(out, 96, "Sporty: %.2f", o->fitness); break; diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index ab22fb5..57ae80f 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -5,6 +5,7 @@ */ #include "people/ai_internal.h" +#include "people/pockets.h" #include "world/nav.h" #include "world/grid.h" #include "render/shapes.h" @@ -271,7 +272,7 @@ static Object *findSupply(const Object *self, Object *const *objects, int count, float best_dist = 1e9f; for (int i = 0; i < count; i++) { Object *o = objects[i]; - if (o->type != OBJ_SUPPLY || (o->dispenses == OBJ_APPLE) != (want == WANT_FOOD)) continue; + if (o->type != OBJ_SUPPLY || o->dispenses != (want == WANT_FOOD ? OBJ_APPLE : OBJ_WATER)) continue; float d = flatDistance(o->position, self->position); if (d < best_dist) { best_dist = d; @@ -402,6 +403,16 @@ static void updateIdle(Object *self, Object *const *objects, int count, float dt } if (hands_free && n->thirst > 0.5f && seekNeed(self, objects, count, WANT_DRINK)) return; if (hands_free && n->hunger > 0.5f && seekNeed(self, objects, count, WANT_FOOD)) return; + if (self->urinella && self->urinellas == 0 && n->bladder < 0.6f) { /* out of Urinellas: more from the box */ + Object *box = ai_nearestOf(self, objects, count, OBJ_SUPPLY, OBJ_URINELLA); + if (box && flatDistance(box->position, self->position) < 80.0f) { + ai->mode = AI_SUPPLY; + ai->target = box; + ai->timer = 0.0f; + ai->sips_left = 0; + return; + } + } /* Their own idea: a job they love that needs doing */ if (hands_free && self->needs.stamina > 0.55f && self->drunk < 0.3f && random01(ai) < 0.06f) { @@ -549,7 +560,16 @@ static void updateSupply(Object *self, Object *const *objects, int count, float turnTowards(self, vec3_sub(supply->position, self->position), dt); if (ai->sips_left == 0) { /* start eating / drinking */ ai->sips_left = 1; - ai->timer = 4.0f; + ai->timer = supply->dispenses == OBJ_URINELLA ? 1.2f : 4.0f; + } + if (supply->dispenses == OBJ_URINELLA) { /* a handful into the pocket */ + Hands_touch(self, Object_colliderCenter(supply, 0)); + if ((ai->timer -= dt) > 0.0f) return; + Pockets_refill(self); + Ai_say(self, "Some Urinellas for later."); + ai->target = NULL; + rest(ai, 1.0f, 2.0f); + return; } ai->timer -= dt; float s = self->look.height / 1.75f; @@ -722,6 +742,7 @@ static void updatePee(Object *self, Object *const *objects, int count, float dt) if (!squat) self->body.pee_aim = vec3_normalize(vec3_add(to, vec3(0.0f, -0.6f, 0.0f))); } } + Hands_setAside(self, ai->set_aside); /* hands free first (taken again after) */ Body_pee(&self->body, Object_peeTime(self)); ai->pee_hidden = false; rest(ai, 2.0f, 4.0f); @@ -1107,6 +1128,62 @@ static void watchdog(Object *self) { } } +/* Put down for a pee: taken up again (if it's still there and nobody took it). */ +static bool takeBack(Object *self) { + Ai *ai = &self->ai; + for (int k = 0; k < 2; k++) { + Object *item = ai->set_aside[k]; + if (!item) continue; + if (item->is_held || flatDistance(item->position, self->position) > 1.6f || self->hands.grabbed) { ai->set_aside[k] = NULL; continue; } + int hand = self->hands.state[k] == HAND_FREE ? k : Hands_freeHand(self); + if (hand < 0) { ai->set_aside[k] = NULL; continue; } + if (self->hands.state[HAND_LEFT] == HAND_REACHING || self->hands.state[HAND_RIGHT] == HAND_REACHING) { stand(self); return true; } + ai->set_aside[k] = NULL; + turnTowards(self, vec3_sub(item->position, self->position), 1.0f); + if (Hands_pickUpObject(self, hand, item)) { stand(self); return true; } + } + return false; +} + +/* Lighters: a borrowed one goes back (when they think of it), and the owner + asks for theirs. */ +static const char *withName(const char *format, const Object *o) { + static char line[AI_SPEECH_MAX]; + snprintf(line, sizeof line, format, o->name); + return line; +} + +static void lighterManners(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + ai->lighter_ask = fmaxf(0.0f, ai->lighter_ask - dt); + if ((ai->lighter_time -= dt) > 0.0f) return; + ai->lighter_time = 3.0f + 4.0f * random01(ai); + if (ai->speech_time > 0.0f) return; + Object *owner = self->lighter ? self->lighter_of : NULL; + bool needs_it = ai->mode == AI_WORK && ai->order == ORDER_FIRE; + if (owner && !owner->lighter && !needs_it && flatDistance(owner->position, self->position) < 2.5f) { + /* the careful think of it themselves; the player has to ask them (mostly) */ + float chance = (owner->type == OBJ_PLAYER ? 0.12f : 0.45f) * (1.0f - fminf(self->drunk, 1.0f)) * (0.6f + 0.6f * ai_stackCare(self)); + if (random01(ai) < chance && Pockets_giveBack(self)) { + Ai_say(self, owner->type == OBJ_PLAYER ? "Oh - your lighter. Thanks!" : withName("Oh - your lighter, %s. Thanks!", owner)); + return; + } + } + if (self->lighter) return; + for (int i = 0; i < count; i++) { /* somebody near with mine */ + Object *o = objects[i]; + if (o == self || !o->lighter || o->lighter_of != self || flatDistance(o->position, self->position) > 4.0f) continue; + if (o->type == OBJ_PLAYER) { + if (random01(ai) < 0.35f) Ai_say(self, "Hey - can I have my lighter back?"); + } else if (o->type == OBJ_HUMAN && !o->ai.talking && o->ai.speech_time <= 0.0f && random01(ai) < 0.5f) { + Ai_say(self, withName("%s, my lighter, please?", o)); + if (o->drunk > 0.6f && random01(ai) < 0.5f) Ai_say(o, "Lighter? What lighter?"); + else if (Pockets_giveBack(o)) Ai_say(o, "Oh, sorry - here."); + } + return; + } +} + void Ai_update(Object *self, Object *const *objects, int count, float dt) { ai_ensureReady(self); Ai *ai = &self->ai; @@ -1137,6 +1214,9 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { } return; } + ai_sunShirt(self, dt); + if (takeBack(self)) return; + lighterManners(self, objects, count, dt); if (ai->mode != AI_IDLE && !(ai->mode == AI_WORK && ai->order == ORDER_FIRE)) self->sitting = false; /* up again to do anything */ smell(self, objects, count, dt); if (ai->talking) { diff --git a/entropie/src/people/ai.h b/entropie/src/people/ai.h index bc5f7b8..907e9ff 100644 --- a/entropie/src/people/ai.h +++ b/entropie/src/people/ai.h @@ -165,6 +165,10 @@ typedef struct { unsigned char warned; /* tools they already said something about (using one they shouldn't) */ float risky; /* seconds until the next close call with a tool they shouldn't use */ float speech_time; + float lighter_time; /* until they next think about lighters (give one back, ask for theirs) */ + float lighter_ask; /* until they ask anybody for a lighter again */ + struct Object *set_aside[2]; /* put down to pee: picked up again after */ + 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 */ AiMode before; /* mode to go back to after being shown something */ diff --git a/entropie/src/people/ai_internal.h b/entropie/src/people/ai_internal.h index 0fbc962..9b32056 100644 --- a/entropie/src/people/ai_internal.h +++ b/entropie/src/people/ai_internal.h @@ -84,6 +84,8 @@ Object *ai_nearestOf(const Object *self, Object *const *objects, int count, Obje void ai_updateWork(Object *self, Object *const *objects, int count, float dt); /* Stacking firewood: how neatly (0 = thrown on .. 1 = exactly), and a word when the top comes down. */ float ai_stackCare(const Object *self); +/* Hard work in the midday sun: the shirt comes off (not for the shy), on again when it cools down. */ +void ai_sunShirt(Object *self, float dt); void ai_pileSlid(Object *self, int slid); void ai_updateFollow(Object *self, Object *const *objects, int count, float dt); bool ai_tidyUp(Object *self, Object *const *objects, int count, float dt); diff --git a/entropie/src/people/ai_work.c b/entropie/src/people/ai_work.c index 981049f..eedccae 100644 --- a/entropie/src/people/ai_work.c +++ b/entropie/src/people/ai_work.c @@ -4,9 +4,11 @@ */ #include "people/ai_internal.h" +#include "people/pockets.h" /* ---------- Wood work ---------- */ +#define LIGHTER_ASK 30.0f /* m: they go and borrow a lighter from somebody this near */ #define UNEVEN_CUT 0.2f /* m a piece may come out longer or shorter, cut by someone unpractised */ /* Where to stand for a job on o, and what to face. */ @@ -443,6 +445,25 @@ void Ai_setHour(float hour) { g_hour = hour; } static bool evening(void) { return g_hour >= 18.0f || g_hour < 3.0f; } +#define SUN_SHIRT_OFF 90.0f /* s of hard work in the sun before the shirt comes off ... */ +#define SUN_SHIRT_ON 40.0f /* ... and on again when it's cooled down to this */ + +void ai_sunShirt(Object *self, float dt) { + Ai *ai = &self->ai; + bool sunny = g_hour >= 11.0f && g_hour < 17.5f; + bool hard = (ai->mode == AI_WORK && (ai->order == ORDER_FIREWOOD || ai->order == ORDER_TREES)) || + (ai->mode == AI_HELP && Tools_held(self, NULL) >= TOOL_AXE); + float warm = sunny && hard ? 1.0f + 0.5f * (1.0f - self->needs.stamina) : sunny ? -0.5f : -3.0f; + ai->sun_heat = clampf(ai->sun_heat + warm * dt, 0.0f, SUN_SHIRT_OFF * 1.5f); + if (self->look.top == TOP_SHIRT && ai->sun_heat >= SUN_SHIRT_OFF && self->temperament.pride > -0.3f) { + self->look.top = self->female ? TOP_CROP : TOP_BARE; + if (ai->speech_time <= 0.0f) Ai_say(self, "Phew, it's hot. Off with the shirt."); + } else if (self->look.top != TOP_SHIRT && ai->sun_heat < SUN_SHIRT_ON) { + self->look.top = TOP_SHIRT; + } +} + + Object *ai_nearestOf(const Object *self, Object *const *objects, int count, ObjectType type, ObjectType dispenses) { Object *best = NULL; float best_d = 1e9f; @@ -607,13 +628,38 @@ static void workFire(Object *self, Object *const *objects, int count, float dt) } ai->job_target = NULL; if (!fire->burning && evening()) { + if (!self->lighter || self->lighter_uses <= 0) { /* a lighter first: who's got one? */ + Object *lender = Pockets_lighterNear(self, objects, count, LIGHTER_ASK); + if (!lender) { + if (ai->lighter_ask <= 0.0f) { + Ai_say(self, self->lighter ? "My lighter's empty. Has anyone got a light?" : "Has anyone got a lighter?"); + ai->lighter_ask = 45.0f; + } + stand(self); + return; + } + ai->job_target = lender; + if (!goTo(self, lender, objects, count, 1.2f, dt)) return; + ai->job_target = NULL; + static char line[AI_SPEECH_MAX]; + if (lender->type == OBJ_PLAYER) snprintf(line, sizeof line, "I'll borrow your lighter - thanks!"); + else snprintf(line, sizeof line, "Lend me your lighter, %s? Thanks!", lender->name); + Ai_say(self, line); + if (lender->type == OBJ_HUMAN && lender->ai.speech_time <= 0.0f) Ai_say(lender, "Sure, here."); + if (self->lighter) { self->lighter = false; self->lighter_of = NULL; } /* (the empty one is thrown away) */ + Pockets_passLighter(lender, self); + return; /* (back to the fire) */ + } if (flatDistance(fire->position, self->position) > near) { Vec3 dir = ai_steer(self, fire, objects, count, fire->position); if (vec3_length(dir) < 0.5f) stand(self); else ai_walk(self, dir, WALK_SPEED * 0.8f, dt); return; } - if (Camp_light(fire)) { Ai_say(self, "There we go!"); Ai_practice(self, ORDER_FIRE, 0.03f); } + if (Pockets_light(self) && Camp_light(fire)) { + Ai_say(self, self->lighter_uses == 0 ? "There we go! ...and that was the last of the lighter." : "There we go!"); + Ai_practice(self, ORDER_FIRE, 0.03f); + } } /* Nothing to do: wait by the fire */ float d = flatDistance(fire->position, self->position); diff --git a/entropie/src/people/body.c b/entropie/src/people/body.c index 5688a9e..bcb85b8 100644 --- a/entropie/src/people/body.c +++ b/entropie/src/people/body.c @@ -257,6 +257,26 @@ void Body_dropAndRoll(Body *b, Vec3 dir) { b->roll_side = 0.0f; } +#define LAND_EASY 5.0f /* m/s down (about 1.3 m): the legs take it as they are */ +#define LAND_DEEP_ 7.0f /* (2.5 m) deep into the knees; faster: roll or crash */ +#define LAND_BREAK 9.5f /* (4.6 m) nobody lands that on their feet */ + +int Body_land(Body *b, Vec3 dir, float speed, float sure) { + if (b->pose != POSE_UP || speed < LAND_EASY) return LAND_SOFT; + float hard = (speed - LAND_EASY) / (LAND_DEEP_ - LAND_EASY); /* 0 .. 1 at a deep landing */ + b->land_squat = clampf(0.35f + 0.5f * hard, 0.0f, 1.1f); + b->crouch_v += 3.0f + 2.0f * hard; /* (down at once, not eased) */ + if (speed < LAND_DEEP_) return LAND_DEEP; + float risk = (speed - LAND_DEEP_) / (LAND_BREAK - LAND_DEEP_); /* 0 .. 1 */ + if (speed < LAND_BREAK && random01(b) < sure * (1.0f - 0.7f * risk)) { /* over the shoulder, and up again */ + Body_dropAndRoll(b, dir); + b->down_time = ROLL_REST * 0.5f; + return LAND_ROLL; + } + Body_fall(b, dir); + return LAND_CRASH; +} + float Body_clumsiness(const Body *b) { return 0.5f + 0.5f * b->quirk[3]; } @@ -747,10 +767,20 @@ static void inTheAir(Body *b, const BodyFrame *f, float s, float speed) { const float dt = f->dt; Vec3 fwd = speed > 0.6f ? vec3_normalize(flat(b->velocity)) : f->facing; bool rising = b->vy > 0.0f; + /* Dropping from high up: knees pulled up to the belly, then (timed) the legs + reach down for the ground */ + float tuck = 0.0f, reach = 0.0f; + if (b->vy < -2.0f && f->ground) { + float h = f->position.y - f->ground(f->ground_ctx, f->position.x, f->position.z, f->position.y + 0.1f); + float v = -b->vy, t = (-v + sqrtf(v * v + 2.0f * 9.81f * fmaxf(h, 0.0f))) / 9.81f; /* (till the feet are down) */ + float high = smoothstepf(0.5f * s, 1.2f * s, h + 0.1f * v * v); + reach = high * (1.0f - smoothstepf(0.12f, 0.28f, t)); + tuck = high * smoothstepf(2.0f, 4.0f, v) * (1.0f - reach); + } for (int i = 0; i < 2; i++) { Foot *foot = &b->feet[i]; foot->stepping = false; - if (i == b->push_foot && b->air_time < b->push_time) continue; /* still pushing off */ + if (i == b->push_foot && b->air_time < b->push_time && tuck <= 0.0f) continue; /* still pushing off */ float sign = i == 0 ? -1.0f : 1.0f; float ahead, lift, pitch; if (b->lead_foot >= 0) { @@ -764,10 +794,15 @@ static void inTheAir(Body *b, const BodyFrame *f, float s, float speed) { lift = (rising ? 0.20f : 0.11f) + (b->push_foot < 0 ? 0.0f : (i == b->push_foot ? -0.04f : 0.04f)); pitch = -0.35f; } + if (tuck + reach > 0.0f) { /* (knees up, then legs down, feet flat to land) */ + ahead = ahead + (0.12f - ahead) * tuck + (0.05f - ahead) * reach; + lift = lift + (0.40f - lift) * tuck - lift * reach; + pitch = pitch + (-0.15f - pitch) * fmaxf(tuck, reach); + } Vec3 hang = vec3_add(f->position, vec3_add(vec3_scale(rightOf(f->facing), sign * FOOT_X * s), vec3_scale(fwd, ahead * s))); hang.y = f->position.y + lift * s; /* on a spring, from the way it was moving when it left the ground */ - float omega = i == b->push_foot ? 10.0f : 14.0f; + float omega = reach > 0.3f ? 22.0f : i == b->push_foot ? 10.0f : 14.0f; /* (reaching for the ground: quick) */ if (!b->airborne) foot->air_v = foot->vel; Vec3 was = vec3_sub(foot->toe, foot->ankle); /* (how the foot pointed, before it moves on) */ Vec3 carried = vec3(b->velocity.x, b->vy, b->velocity.z); /* (it goes along with the body) */ @@ -886,6 +921,10 @@ void Body_update(Body *b, const BodyFrame *f) { /* Action goals */ if (b->squat_pee && b->pee > 0.0f) b->crouch_goal = fmaxf(b->crouch_goal, 1.0f); /* squatting to pee */ + if (b->land_squat > 0.0f) { /* landed from high up: into the knees, up again */ + b->crouch_goal = fmaxf(b->crouch_goal, b->land_squat); + b->land_squat = fmaxf(0.0f, b->land_squat - 1.6f * dt); + } springCritical(&b->crouch, &b->crouch_v, b->crouch_goal, POSE_OMEGA, dt); /* bending down speeds up, slows down */ b->crouch = clampf(b->crouch, 0.0f, 1.2f); b->sit = ease(b->sit, b->pose == POSE_UP ? b->sit_goal : 0.0f, 2.5f, dt); diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h index b5d05e1..f207e71 100644 --- a/entropie/src/people/body.h +++ b/entropie/src/people/body.h @@ -109,6 +109,7 @@ typedef struct { Vec3 last_velocity; Vec3 velocity; /* smoothed world velocity */ float crouch, crouch_v; + float land_squat; /* just landed from high up: down into the knees this far (eases off) */ float lean, lean_v; float hand_weight[2]; /* 0 = free swing .. 1 = at target */ float hand_wvel[2]; /* ... how fast that changes (it eases in and out) */ @@ -261,6 +262,10 @@ Vec3 Body_takeShift(Body *body); void Body_stagger(Body *body, Vec3 push); /* Falls over towards dir (world), lies a moment and gets up again. */ void Body_fall(Body *body, Vec3 dir); +/* Landed from high up at this speed (m/s down); sure 0..1 (fit, sober, fresh, sure-footed). + Soft: into the knees; harder: deep, a roll (the sure) or a crash. Returns LAND_*. */ +enum { LAND_SOFT, LAND_DEEP, LAND_ROLL, LAND_CRASH }; +int Body_land(Body *body, Vec3 dir, float speed, float sure); /* On fire: quickly down to the ground, stretched out, rolling from side to side. */ void Body_dropAndRoll(Body *body, Vec3 dir); /* Tripped over something with this foot (0 left, 1 right): a lurch that way, diff --git a/entropie/src/people/body_pose.c b/entropie/src/people/body_pose.c index f0c4fbf..a327817 100644 --- a/entropie/src/people/body_pose.c +++ b/entropie/src/people/body_pose.c @@ -931,8 +931,12 @@ static Vec3 arcAt(const StreamArc *a, float t) { static void solveStream(const Body *b, Vec3 facing, Skeleton *out, StreamArc *arc) { const float GRAVITY = 9.81f; out->stream_kind = STREAM_NONE; - /* squatting to pee: trousers down, skirt or dress gathered up (eased with the pee) */ - out->undress = b->squat_pee && b->pose == POSE_UP ? smoothstepf(0.0f, 0.5f, peeing(b)) * smoothstepf(0.4f, 0.8f, b->crouch) : 0.0f; + /* squatting to pee: trousers down, skirt or dress gathered up; standing + (a man, a woman with a Urinella): the fly open, the skirt or dress lifted + in front (negative). Eased with the pee. */ + out->undress = b->pose != POSE_UP ? 0.0f + : b->squat_pee ? smoothstepf(0.0f, 0.5f, peeing(b)) * smoothstepf(0.4f, 0.8f, b->crouch) + : -smoothstepf(0.0f, 0.6f, peeing(b)); if (b->pose != POSE_UP) return; const Vec3 *j = out->joints; float pee = peeing(b), heave = heaving(b); diff --git a/entropie/src/people/hands.c b/entropie/src/people/hands.c index c4a2568..dbb66a6 100644 --- a/entropie/src/people/hands.c +++ b/entropie/src/people/hands.c @@ -175,6 +175,29 @@ void Hands_drop(Object *who, int hand) { } } +void Hands_setAside(Object *who, Object *out[2]) { + Hands *h = &who->hands; + out[0] = out[1] = NULL; + if (h->grabbed) Hands_release(who); + Vec3 f = Object_facing(who), side = vec3(-f.z, 0.0f, f.x); + for (int i = 0; i < 2; i++) { + Object *item = h->item[i]; + if (h->state[i] != HAND_HOLDING || !item) continue; + if (i == HAND_RIGHT && h->item[HAND_LEFT] == item) continue; /* (two-handed: once) */ + Hands_drop(who, i); + out[i] = item; + if (item->type == OBJ_AXE || item->type == OBJ_SHEARS || item->type == OBJ_LADDER) continue; /* (laid flat) */ + item->rotation = vec3(0.0f, who->rotation.y, 0.0f); /* upright on the ground, a step to the side */ + Object_syncColliders(item); + float out_by = (i == HAND_RIGHT ? 1.0f : -1.0f) * (0.35f + item->colliders[0].half_extents.x); + Vec3 spot = vec3_add(who->position, vec3_add(vec3_scale(side, out_by), vec3_scale(f, 0.15f))); + spot.y += item->colliders[0].half_extents.y + 0.03f; + item->position = vec3_sub(spot, item->colliders[0].offset); + item->velocity = vec3_zero(); + Object_syncColliders(item); + } +} + void Hands_touch(Object *who, Vec3 point) { who->hands.touching = true; who->hands.touch_point = point; diff --git a/entropie/src/people/hands.h b/entropie/src/people/hands.h index bd14157..a1ddd2b 100644 --- a/entropie/src/people/hands.h +++ b/entropie/src/people/hands.h @@ -65,6 +65,9 @@ typedef struct { /* Picks up the item in front with this hand (or both), or drops what it holds. */ bool Hands_pickUpOrDrop(struct Object *who, int hand, struct Object *const *objects, int count); void Hands_drop(struct Object *who, int hand); +/* Puts whatever is in the hands down on the ground beside them (lets go of + what they hold on to): the hands are free, for a pee. out: what was put down. */ +void Hands_setAside(struct Object *who, struct Object *out[2]); /* * Takes whatever is in front: small things with a free hand, bigger ones with * both, and anything too heavy to carry is taken hold of (push, pull, or lift diff --git a/entropie/src/people/pockets.c b/entropie/src/people/pockets.c new file mode 100644 index 0000000..d4033b1 --- /dev/null +++ b/entropie/src/people/pockets.c @@ -0,0 +1,44 @@ +#include "people/pockets.h" + +#include <math.h> + +bool Pockets_light(Object *who) { + if (!who->lighter || who->lighter_uses <= 0) return false; + who->lighter_uses--; + return true; +} + +Object *Pockets_lighterNear(Object *who, Object *const *objects, int count, float range) { + Object *best = NULL; + for (int i = 0; i < count; i++) { + Object *o = objects[i]; + if (o == who || !o->is_character || o->type == OBJ_DOG || !o->lighter || o->lighter_uses <= 0) continue; + float d = hypotf(o->position.x - who->position.x, o->position.z - who->position.z); + if (d < range) { range = d; best = o; } + } + return best; +} + +void Pockets_passLighter(Object *from, Object *to) { + if (!from->lighter || to->lighter) return; + to->lighter = true; + to->lighter_uses = from->lighter_uses; + to->lighter_of = from->lighter_of == to ? NULL : from->lighter_of ? from->lighter_of : from; /* (back home: their own again) */ + from->lighter = false; + from->lighter_of = NULL; + from->lighter_uses = 0; +} + +bool Pockets_giveBack(Object *who) { + Object *owner = who->lighter_of; + if (!who->lighter || !owner || owner->lighter) return false; + Pockets_passLighter(who, owner); + return true; +} + +int Pockets_refill(Object *who) { + int got = POCKET_URINELLAS - who->urinellas; + if (got < 0) got = 0; + who->urinellas = POCKET_URINELLAS; + return got; +} diff --git a/entropie/src/people/pockets.h b/entropie/src/people/pockets.h new file mode 100644 index 0000000..adbb4b0 --- /dev/null +++ b/entropie/src/people/pockets.h @@ -0,0 +1,25 @@ +#ifndef PEOPLE_POCKETS_H +#define PEOPLE_POCKETS_H + +/* + * What people carry in their pockets: Urinellas (one-time use, up to + * POCKET_URINELLAS), a lighter (it runs empty; it can be lent, and not + * always given back). + */ + +#include <stdbool.h> + +#include "world/object.h" + +/* Uses who's lighter once: false if there's none, or it's empty. */ +bool Pockets_light(Object *who); +/* Somebody within range with a lighter that works (not who). */ +Object *Pockets_lighterNear(Object *who, Object *const *objects, int count, float range); +/* The lighter goes from one pocket to the other (whose it is stays known). */ +void Pockets_passLighter(Object *from, Object *to); +/* Someone else's lighter in who's pocket goes back to its owner (if they have none). */ +bool Pockets_giveBack(Object *who); +/* Takes Urinellas from a box: how many were taken. */ +int Pockets_refill(Object *who); + +#endif /* PEOPLE_POCKETS_H */ diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c index 1b4ec0a..0d64802 100644 --- a/entropie/src/people/talk.c +++ b/entropie/src/people/talk.c @@ -1,6 +1,7 @@ #include "people/talk.h" #include "world/camp.h" #include "world/tools.h" +#include "people/pockets.h" #include <stdio.h> #include <string.h> @@ -49,6 +50,8 @@ static const char *const CHOICES[NODE_COUNT][TALK_CHOICES] = { [NODE_TOOL] = { "Here you go", "No, I still need it" }, }; +static const Object *g_talk_player; /* (for the choices: what's between them) */ + static const Order TASKS[] = { ORDER_FIREWOOD, ORDER_TREES, ORDER_TRASH, ORDER_FIRE }; static float flatDistance(Vec3 a, Vec3 b) { @@ -205,6 +208,33 @@ static int talkTool(const Object *npc) { return held == TOOL_AXE || held == TOOL_CHAINSAW ? (int)held : (int)TOOL_CHAINSAW; } +/* The lighter between the two: what the player can say about it (instead of "Who are you?"). */ +enum { LIGHTER_NONE, LIGHTER_MINE_BACK, LIGHTER_RETURN, LIGHTER_BORROW }; + +static int lighterTopic(const Object *player, const Object *npc) { + if (npc->lighter && npc->lighter_of == player && !player->lighter) return LIGHTER_MINE_BACK; + if (player->lighter && player->lighter_of == npc && !npc->lighter) return LIGHTER_RETURN; + if ((!player->lighter || player->lighter_uses <= 0) && npc->lighter && npc->lighter_uses > 0) return LIGHTER_BORROW; + return LIGHTER_NONE; +} + +static void talkLighter(Talk *talk, Object *player, int topic) { + Object *npc = talk->with; + if (topic == LIGHTER_MINE_BACK) { + if (npc->drunk > 0.6f && npc->ai.character.willing < 0.5f) wrapUp(talk, "Lighter? What lighter?"); + else { Pockets_passLighter(npc, player); wrapUp(talk, shy(npc) ? "Oh! Sorry... here." : "Oh, right - here you go."); } + } else if (topic == LIGHTER_RETURN) { + Pockets_passLighter(player, npc); + wrapUp(talk, "Thanks!"); + } else if (npc->ai.annoyed > 0.6f) { + wrapUp(talk, "Get your own."); + } else { + if (player->lighter) { player->lighter = false; player->lighter_of = NULL; } /* (the empty one: away with it) */ + Pockets_passLighter(npc, player); + wrapUp(talk, npc->lighter_uses < 5 ? "Here - it's nearly empty though." : "Sure. I want it back though!"); + } +} + static void answer(Talk *talk, World *world, Object *player, int choice) { Object *npc = talk->with; switch (talk->node) { @@ -213,6 +243,8 @@ static void answer(Talk *talk, World *world, Object *player, int choice) { 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."); } + } else if (choice == 3 && lighterTopic(player, npc) != LIGHTER_NONE) { + talkLighter(talk, player, lighterTopic(player, npc)); } else if (choice == 3) { Ai_say(npc, npc->ai.character.about ? npc->ai.character.about : "Me? Nobody special."); } else if (choice == 4) { @@ -312,7 +344,7 @@ static int nodeFor(const Object *npc, Topic topic) { void Talk_begin(Talk *talk, World *world, Object *player, Object *with, Topic topic) { (void)world; - (void)player; + g_talk_player = player; if (talk->with) Ai_setTalking(talk->with, false); memset(talk, 0, sizeof(*talk)); talk->with = with; @@ -398,6 +430,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 && 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?" }; + int topic = lighterTopic(g_talk_player, talk->with); + if (topic != LIGHTER_NONE) out[2] = LIGHTER_LINE[topic]; + } if (talk->node == NODE_WORKER && woodJob(talk->with->ai.order) && n < TALK_CHOICES) { /* about the tool they use */ static char line[64]; const Object *npc = talk->with; diff --git a/entropie/src/render/figure.h b/entropie/src/render/figure.h index 062ef6e..e67578b 100644 --- a/entropie/src/render/figure.h +++ b/entropie/src/render/figure.h @@ -27,8 +27,11 @@ typedef struct { HairStyle hair_style; bool long_sleeves; int garment; /* GARMENT_*: what's on the legs */ + int top; /* TOP_*: the shirt on, or off (working in the sun) */ } FigureLook; +enum { TOP_SHIRT, TOP_BARE, TOP_CROP }; + /* Trousers (pants colour), shorts (knee-length), a skirt (pants colour) or a dress (shirt colour). */ enum { GARMENT_TROUSERS, GARMENT_SHORTS, GARMENT_SKIRT, GARMENT_DRESS }; @@ -66,7 +69,7 @@ typedef struct { Vec3 stream[4]; int stream_kind; /* STREAM_NONE / STREAM_PEE / STREAM_VOMIT */ float stream_width; /* meters */ - float undress; /* squatting to pee: trousers down / skirt up (0..1) */ + float undress; /* to pee: squatting trousers down / skirt up (0..1); standing fly open / skirt lifted in front (-1..0) */ } Skeleton; enum { STREAM_NONE, STREAM_PEE, STREAM_VOMIT, STREAM_WATER }; diff --git a/entropie/src/render/renderer.c b/entropie/src/render/renderer.c index 44b0557..65877d9 100644 --- a/entropie/src/render/renderer.c +++ b/entropie/src/render/renderer.c @@ -97,6 +97,7 @@ static bool loadShaders(Renderer *r) { f->stream_width = glGetUniformLocation(f->program, "streamWidth"); f->garment = glGetUniformLocation(f->program, "garment"); f->undress = glGetUniformLocation(f->program, "undress"); + f->top = glGetUniformLocation(f->program, "top"); f->view_pos = glGetUniformLocation(f->program, "viewPos"); f->light_dir = glGetUniformLocation(f->program, "lightDirection"); f->light_color = glGetUniformLocation(f->program, "lightColor"); @@ -504,6 +505,7 @@ void Renderer_drawFigure(Renderer *r, const FigurePose *pose) { glUniform1f(f->stream_width, sk->stream_width); glUniform1i(f->garment, pose->look.garment); glUniform1f(f->undress, sk->undress); + glUniform1i(f->top, pose->look.top); glBindVertexArray(r->fullscreen_vao); /* no vertex data: the card is built from gl_VertexID */ glDrawArrays(GL_TRIANGLES, 0, 6); diff --git a/entropie/src/render/renderer.h b/entropie/src/render/renderer.h index f3eb46d..7bca689 100644 --- a/entropie/src/render/renderer.h +++ b/entropie/src/render/renderer.h @@ -67,7 +67,7 @@ typedef struct { GLint skin, hair, shirt, pants, shoes, hair_style, long_sleeves; GLint head_forward, head_up, chest_forward, waist_bend, face, expression; GLint stream, stream_kind, stream_width; - GLint garment, undress; + GLint garment, undress, top; GLint view_pos, light_dir, light_color, shadow_color, sky_top, sky_horizon; GLint shadow_strength, shadow_map, fire_light; } FigureShader; diff --git a/entropie/src/world/ladder.c b/entropie/src/world/ladder.c index 507497f..2dc9bab 100644 --- a/entropie/src/world/ladder.c +++ b/entropie/src/world/ladder.c @@ -568,6 +568,7 @@ void Ladder_poseHands(Object *who, float dt) { if (who->ladder_off > 0.5f) return; /* (stepping off: let go) */ int k = rungAbove(l, who->ladder_at + HANDS_ABOVE * s); for (int h = 0; h < 2; h++) { + if (who->hands.state[h] == HAND_HOLDING) continue; /* (one-handed: the other holds something) */ /* hand over hand: one a rung above the other, swapping as you go */ int kh = k + ((k + h) % 2 == 0 ? 0 : -1); if (kh < 0) kh = 0; diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index baf0186..0e16c22 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -287,6 +287,10 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size addCollider(obj, vec3_zero(), vec3(0.1f, 0.05f, 0.1f), vec3_zero()); break; + case OBJ_URINELLA: /* (never made: they're only counted, in pockets) */ + Object_destroy(obj); + return NULL; + case OBJ_SHEARS: obj->mesh = Shapes_shears(size.y); obj->pivot = centered; @@ -1057,7 +1061,8 @@ void Object_describe(const Object *obj, char *out, size_t size) { case OBJ_SUPPLY: snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - take an apple" : obj->dispenses == OBJ_SAUSAGE ? "Cool box - take a sausage" - : obj->dispenses == OBJ_BAG ? "Garbage bags - take one" : "Water barrel - take a bottle"); + : obj->dispenses == OBJ_BAG ? "Garbage bags - take one" + : obj->dispenses == OBJ_URINELLA ? "Urinella box - fill your pocket (one-time use)" : "Water barrel - take a bottle"); break; case OBJ_BOX: case OBJ_CYLINDER: diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index 49ee372..52aa785 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -75,6 +75,7 @@ typedef enum { OBJ_STUMP, /* static: what's left of a felled tree, size = (diameter, height, -); work = how far it's been chopped out (see tools.h) */ OBJ_TAPE, /* a roll of duct tape: mends a melted hose (see hose.h) */ + OBJ_URINELLA, /* (only in pockets - a box of them is an OBJ_SUPPLY that dispenses these) */ } ObjectType; /* What a body needs over the day. All 0..1; 0 is fine (stamina: 1 is fine). */ @@ -97,7 +98,8 @@ typedef struct { #define OBJECT_MAX_COLLIDERS 3 -#define URINELLA_PACK 3 /* disposable Urinellas in a pack */ +#define POCKET_URINELLAS 4 /* disposable Urinellas a pocket holds (one-time use) */ +#define LIGHTER_USES 60 /* a full lighter lights this many times */ typedef struct Object { ObjectType type; @@ -165,7 +167,10 @@ typedef struct Object { float pour; /* characters: 0 = can hanging .. 1 = lifted and tipped, pouring */ bool female; /* characters: squats to pee, unless ... */ bool urinella; /* ... she has one of these (a pack of them) ... */ - int urinellas; /* ... fresh ones left (none left: she squats like the others) */ + int urinellas; /* ... fresh ones in the pocket (none: she squats like the others, and gets more) */ + bool lighter; /* characters: a lighter in the pocket ... */ + struct Object *lighter_of;/* ... someone else's (borrowed), or NULL: their own ... */ + int lighter_uses; /* ... lights left in it (0: empty) */ float dirt; /* characters: 0 = clean .. 1 = filthy (peeing outdoors, lying in the dirt) */ int scrapes; /* characters: shins and knees scraped on things (too fast, or clumsy) */ float scrape_cool; /* (seconds before the next one counts) */ @@ -213,12 +218,16 @@ typedef struct Object { int jobs_seen; /* characters: finished tool jobs already counted for practice */ float bump; /* characters: walked into something this fast (m/s) this step ... */ Vec3 bump_normal; /* ... facing away from it */ - float bump_top; /* ... its top this high above the feet */ + float bump_top; /* ... its top this high above the feet ... */ + bool bump_toe; /* ... a ladder lying there: it hardly moves, the toe hurts */ float bump_cool; /* seconds until a bump counts again */ Vec3 pee_hit; /* characters: where the stream comes down */ float extend, extend_goal; /* ladders: 0 = slid together .. 1 = all the way out */ float raised, raise_goal; /* ladders carried slid out: held near the foot, this steep up in front (radians, 0 = carried at the side) */ bool leaning; /* ladders: leaning against something (stands still) */ + float air_fall; /* characters: in the air, falling this fast at most (m/s; for the landing) */ + float ladder_skill; /* characters: practice climbing ladders one-handed (0..1) ... */ + float ladder_wobble; /* ... nearly slipped: the next slip is a fall (seconds this counts) */ Vec3 lean_normal; /* ladders: away from what it leans on */ float lean_top; /* ladders: where along it the edge is (0 = against a face, no getting off) */ float placing; /* ladders: being put up against something: 0 = not, else how far (0..1) ... */ diff --git a/entropie/src/world/physics.c b/entropie/src/world/physics.c index e33b404..9d4c26a 100644 --- a/entropie/src/world/physics.c +++ b/entropie/src/world/physics.c @@ -19,6 +19,8 @@ #define TOPPLE_SPEED 0.5f /* knocked into this fast, a bottle falls over */ #define KICK_SPEED 3.0f /* run into this fast, it's kicked away like a throw */ #define THROW_IGNORE_TIME 0.3f /* a thrown item doesn't hit its thrower right away */ +#define LADDER_GRIP 0.15f /* walked into, a ladder lying there moves this much of what a loose thing would ... */ +#define LADDER_SLIDE 0.25f /* ... and slides at most this fast (m/s) */ #define WALK_PUSH_WEIGHT 15.0f /* walking into it shoves it aside; heavier things stop you (F pushes them) */ #define STEP_OVER_HEIGHT 0.48f /* low things up to this (scaled, about the knee) are stepped over ... */ #define STEP_OVER_WIDTH 0.75f /* ... if they are no wider than a stride across */ @@ -85,12 +87,15 @@ static float halfAcross(const Collider *c) { return across; } -/* Kinds that are never stepped over (however low): people, buildings, trees, the fire. */ +/* Kinds that are never stepped over (however low): people, buildings, trees, + the fire; a ladder only lying on the ground. */ static bool neverOver(const Object *o) { switch (o->type) { case OBJ_HOUSE: case OBJ_BARN: case OBJ_TREE: case OBJ_BUSH: case OBJ_FIREPLACE: case OBJ_DOG: - case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DECAL: case OBJ_PUDDLE: case OBJ_STINK: case OBJ_LADDER: + case OBJ_HUMAN: case OBJ_PLAYER: case OBJ_DECAL: case OBJ_PUDDLE: case OBJ_STINK: return true; + case OBJ_LADDER: + return o->leaning; default: return false; } @@ -205,6 +210,8 @@ static float pushFactor(const Object *obj, float other_weight) { /* obj (moving) pushes a dynamic object sideways. normal points from other to obj. */ static void push(Object *obj, Object *other, Vec3 normal, float depth) { float share = obj->is_character && other->weight > WALK_PUSH_WEIGHT ? 0.0f : pushFactor(obj, other->weight); + bool grips = obj->is_character && other->type == OBJ_LADDER; /* (rungs and feet catch on the ground: it hardly slides) */ + if (grips) share *= LADDER_GRIP; float d = fminf(depth, MAX_PUSH_CORRECTION); if (obj->is_character && other->is_character) d = fminf(d, PERSON_EASE); /* (people standing close ease apart, no jolt) */ @@ -220,7 +227,7 @@ static void push(Object *obj, Object *other, Vec3 normal, float depth) { something just touching it (like a dropped bottle) doesn't shove it away. */ float pusher = vec3_dot(vec3_add(obj->movement, obj->velocity), away); float along = vec3_dot(other->velocity, away); - float target = fminf(pusher * share, PUSH_STRENGTH); + float target = fminf(pusher * share, grips ? LADDER_SLIDE : PUSH_STRENGTH); if (pusher > TOPPLE_SPEED) Object_topple(other, away); /* Kicked at a run: a bottle flies off and may break where it lands */ if (obj->is_character && Object_isBottle(other) && pusher > KICK_SPEED) { @@ -271,11 +278,11 @@ static bool touchPerson(Object *obj, Object *person, Vec3 normal, float depth) { return true; } -#define BUMP_WEIGHT 15.0f /* lighter things just get kicked aside */ +#define BUMP_WEIGHT 15.0f /* lighter things just get kicked aside (not a ladder) */ /* A walking person runs into something solid: how hard (see the world: drunk people fall). */ static void noteBump(Object *obj, const Object *other, Vec3 normal) { - if (!obj->is_character || (!other->is_static && !other->is_character && other->weight < BUMP_WEIGHT)) return; + if (!obj->is_character || (!other->is_static && !other->is_character && other->weight < BUMP_WEIGHT && other->type != OBJ_LADDER)) return; Vec3 n = vec3(normal.x, 0.0f, normal.z); if (vec3_length(n) < 1e-4f) return; n = vec3_normalize(n); @@ -287,6 +294,7 @@ static void noteBump(Object *obj, const Object *other, Vec3 normal) { for (int c = 0; c < other->collider_count; c++) top = fmaxf(top, Object_colliderCenter(other, c).y + projectRadius(&other->colliders[c], WORLD_UP)); obj->bump_top = top - obj->position.y; + obj->bump_toe = other->type == OBJ_LADDER && !other->leaning; } } diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c index 1664f23..2dc2e74 100644 --- a/entropie/src/world/scene.c +++ b/entropie/src/world/scene.c @@ -21,7 +21,7 @@ typedef struct { first entry. Order also defines physics order. */ static const Spawn SCENE[] = { /* Characters */ - { OBJ_PLAYER, PALETTE_MODERN, { 1, 4, 0 }, HUMAN_SIZE, 75, NO_ROTATION }, + { OBJ_PLAYER, PALETTE_MODERN, { 1, 1, 0 }, HUMAN_SIZE, 75, NO_ROTATION }, /* (dropped onto the ground: low, no hard landing) */ { OBJ_HUMAN, PALETTE_MODERN, { -1.5f, 1, -3 }, HUMAN_SIZE, 85, { 0, 180, 0 } }, /* facing the player */ { OBJ_HUMAN, PALETTE_MODERN, { 2.5f, 1, -4 }, HUMAN_SIZE, 55, { 0, 160, 0 } }, { OBJ_HUMAN, PALETTE_MODERN, { -5.0f, 1, 4 }, HUMAN_SIZE, 68, { 0, 120, 0 } }, @@ -84,6 +84,7 @@ static const int MAIN_HAND[] = { HAND_RIGHT, HAND_RIGHT, HAND_LEFT, HAND_RIGHT, /* Ida and Mila are women; Mila brought a Urinella (she pees standing). */ static const bool FEMALE[] = { false, false, true, true, false }; static const bool URINELLA[] = { false, false, false, true, false }; +static const bool LIGHTER[] = { true, true, false, false, true }; /* (a lighter in the pocket: You, Jonas, Ben) */ static const int GARMENT[] = { GARMENT_TROUSERS, GARMENT_SHORTS, GARMENT_SKIRT, GARMENT_DRESS, GARMENT_TROUSERS }; /* How sporty each is (0..1): not a matter of build (Ida is light but not sporty, Jonas is heavy and fit). */ static const float FITNESS[] = { 0.5f, 0.75f, 0.35f, 0.85f, 0.15f }; @@ -135,7 +136,9 @@ Object *Scene_load(World *world) { obj->ai.character = CHARACTERS[characters % TEMPERAMENT_COUNT]; obj->female = FEMALE[characters % TEMPERAMENT_COUNT]; obj->urinella = URINELLA[characters % TEMPERAMENT_COUNT]; - obj->urinellas = obj->urinella ? URINELLA_PACK : 0; + obj->urinellas = obj->urinella ? POCKET_URINELLAS - 1 : 0; + obj->lighter = LIGHTER[characters % TEMPERAMENT_COUNT]; + obj->lighter_uses = obj->lighter ? LIGHTER_USES * (1 + characters % 3) / 3 : 0; /* (some nearly empty) */ for (int k = 0; k < ORDER_COUNT; k++) obj->skill[k] = obj->ai.character.skill[k]; obj->tempo = obj->ai.character.tempo; characters++; diff --git a/entropie/src/world/site.c b/entropie/src/world/site.c index 9c3297e..14f31b0 100644 --- a/entropie/src/world/site.c +++ b/entropie/src/world/site.c @@ -904,6 +904,11 @@ bool Site_build(World *world) { Object *toilet = Object_createFurniture(FURN_TOILET, court(36.8f, -4.3f), vec3(1.1f, 2.3f, 1.1f), yawFor(mapDir(COURT_ANGLE + 90.0f))); if (!toilet || !World_add(world, toilet)) return false; + Object *urinellas = Object_create(OBJ_SUPPLY, PALETTE_GROUND, court(37.75f, -4.6f), vec3(0.3f, 0.18f, 0.2f), 100.0f); + if (!urinellas) return false; + urinellas->dispenses = OBJ_URINELLA; /* (a little carton beside the toilet) */ + Object_setRotation(urinellas, vec3(0.0f, yawFor(mapDir(COURT_ANGLE + 90.0f)), 0.0f)); + if (!World_add(world, urinellas)) return false; /* Alice, the dachshund */ Object *alice = Object_create(OBJ_DOG, PALETTE_WARM, court(YARD_A - 2.0f, YARD_B - 4.0f), vec3(0.2f, 0.35f, 0.7f), 9.0f); diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index ae15944..fa948ca 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -411,6 +411,12 @@ static void takeBump(World *world, Object *o, float dt) { o->bump_cool = fmaxf(0.0f, o->bump_cool - dt); float d = fminf(o->drunk, 1.0f); if (into < BUMP_MIN || o->bump_cool > 0.0f || Body_isBusy(&o->body) || o->climbing || o->on_ladder) return; + if (o->bump_toe && into < RUN_INTO && o->type != OBJ_DOG) { /* kicked a ladder: it hardly moves */ + o->bump_cool = 3.0f * BUMP_WAIT; + Ai_say(o, into > 1.5f ? "Ouch! My toe!" : "Ow..."); + if (d > 0.3f && dice(world) < d) Body_stagger(&o->body, vec3_scale(o->bump_normal, 0.4f)); + return; + } if (d < 0.1f) { /* sober: only running into it counts */ if (into < RUN_INTO) return; o->bump_cool = BUMP_WAIT; @@ -582,8 +588,32 @@ static void overFeet(Object *o) { #define FAR_DETAIL 30.0f /* m from the player: beyond, feet and hands don't look out for things */ +/* Down from high up: into the knees, deeper, a roll or a crash (see Body_land). */ +static void landFromHigh(World *world, Object *o) { + float speed = o->air_fall; + o->air_fall = 0.0f; + if (o->type == OBJ_DOG || speed < 5.0f) return; + float sure = clampf(o->fitness * 1.3f, 0.0f, 1.0f) * sqrtf(o->needs.stamina) * (1.0f - fminf(o->drunk, 1.0f)) + * (1.0f - 0.5f * Body_clumsiness(&o->body)); + Vec3 dir = vec3(o->velocity.x, 0.0f, o->velocity.z); + if (vec3_length(dir) < 0.3f) dir = Object_facing(o); + int how = Body_land(&o->body, vec3_normalize(dir), speed, sure); + float jar = smoothstepf(5.0f, 10.0f, speed); + o->needs.stamina = fmaxf(0.0f, o->needs.stamina - (how == LAND_CRASH ? 0.25f : 0.08f) * jar); + if (how == LAND_CRASH) { + o->dirt = fminf(1.0f, o->dirt + 0.1f); + Ai_say(o, speed > 9.5f ? "Aaargh... my ankle!" : "Ouch!"); + } else if (how == LAND_ROLL) { + o->dirt = fminf(1.0f, o->dirt + 0.05f); + } + (void)world; +} + /* Hands, feet and skeleton, then carried items follow the hands. */ static void animateCharacter(World *world, Object *o, float dt) { + if (o->climbing || o->on_ladder || o->is_held) o->air_fall = 0.0f; + else if (!o->is_grounded) o->air_fall = fmaxf(o->air_fall, -o->velocity.y); + else if (o->air_fall > 0.0f) landFromHigh(world, o); Hands_update(o, dt); Tools_update(o, dt); Ladder_poseHands(o, dt); /* on a ladder: hands on the rungs */