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.76.0: smoking, smooth shadow edges
- Smoking: packs and tobacco in the pockets (used up); smokers get a craving and light up in a quiet moment - rolling one first, the lighter to it, drags (the tip glows), smoke breathed out, the butt flicked away - Shared: "Got a light?", bumming a cigarette (or a no); talk to ask for one or offer one; K lights yours / puts it out; Tab shows your pockets - Shadows: soft, smooth edges instead of stair steps Includes 1.75.0 (if not committed separately): - 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 | 17 ++++++- entropie/STATE.md | 13 +++++- entropie/assets/shaders/common.glsl | 25 +++++++--- entropie/assets/shaders/figure.frag | 17 ++++++- entropie/commit_message.txt | 10 +++- entropie/src/core/gl.h | 3 +- entropie/src/core/input.c | 1 + entropie/src/core/input.h | 1 + entropie/src/game/game.c | 7 ++- entropie/src/game/game_actions.c | 27 +++++++++++ entropie/src/people/ai.c | 59 +++++++++++++++++++++++ entropie/src/people/body.c | 66 ++++++++++++++++++++++++++ entropie/src/people/body.h | 21 +++++++++ entropie/src/people/body_internal.h | 5 ++ entropie/src/people/body_pose.c | 93 ++++++++++++++++++++++++++++++++++++- entropie/src/people/pockets.c | 36 ++++++++++++++ entropie/src/people/pockets.h | 11 ++++- entropie/src/people/talk.c | 20 +++++++- entropie/src/render/figure.h | 2 + entropie/src/render/renderer.c | 15 ++++++ entropie/src/render/renderer.h | 3 ++ entropie/src/render/shapes.h | 2 + entropie/src/render/shapes_props.c | 8 ++++ entropie/src/world/chips.c | 33 +++++++++++-- entropie/src/world/chips.h | 6 ++- entropie/src/world/object.c | 4 +- entropie/src/world/object.h | 9 +++- entropie/src/world/scene.c | 8 ++++ entropie/src/world/world.c | 36 ++++++++++++++ 30 files changed, 532 insertions(+), 28 deletions(-) diff --git a/entropie/Makefile b/entropie/Makefile index ec6f7aa..c2cd9f4 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 1.75.0 +VERSION := 1.76.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index f499ce0..4c80286 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -13,7 +13,7 @@ What the game can do so far, who is in it and what is still open: Painted in the rotoscope style of *Waking Life* (2001): - Wobbly geometry and boiling ink outlines, animated at 12 fps -- Flat paint tones with hand-drawn shadow shapes +- Flat paint tones with hand-drawn shadow shapes (soft, smooth edges) - Color moods that change through the day, under a calm painted sky - People are painted figures with faces, hair and real arm and leg movement - Feet are planted on the ground: real steps, side steps, slopes, roofs @@ -418,6 +418,20 @@ say no more often and don't come to you for a bit. 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 +## Smoking + +- Ben and Jonas roll their own (tobacco), you and Ben have a pack, Mila + smokes now and then and bums them, Ida doesn't smoke +- Smokers get a craving over time. In a quiet moment (idle, a break, by + the fire) they light up: rolling first if it's tobacco (both hands), + then the lighter to it - theirs, or "Got a light?" from somebody near +- A drag now and then (the tip glows), smoke breathed out, the hand up in + front between drags. Hands needed: it goes into the mouth +- Done: flicked away or dropped as a butt (into the fire when close) +- K: light one (yours, or somebody next to you gives you a light), K again + puts it out. Talk: ask somebody for a cigarette, or offer one +- Packs and tobacco run out; Tab (skills page) shows your pockets + ## Day and needs - Hunger, thirst, a full bladder and sleepiness build up over the day @@ -482,6 +496,7 @@ say no more often and don't come to you for a bit. | E / Q or right click at its foot | Take the ladder down | | Left click on the fire, hand free | Take food off the grill | | P | Pee (next to the toilet: use it); Shift: further, mouse: aim | +| K | Light a cigarette / put it out | | Z | Sleep / get up | | C | Sit down / get up; on fire: drop and roll; at a fire with a drink: pour it on | | Space (asleep, hold) | Sleep faster | diff --git a/entropie/STATE.md b/entropie/STATE.md index 43b82b9..97d70ff 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -19,6 +19,7 @@ Updated with every version (see the version line at the end). | Main hand | right | right | left | right | right | | Pees | standing | standing | squatting | standing (Urinella, 3 in the pocket) | standing | | 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 | Alice the dog: follows, can be petted and called. @@ -59,6 +60,12 @@ Tools they shouldn't use (`ai_work.c`, Ai_shouldntUse): the chainsaw with tree s 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"). +Smoking: `craving` grows smoker / 300 s (`world.c` smoke), falls 1/90 s while smoking. NPCs (`ai.c` smokeBreak, quiet moment = idle, wander, sitting, a work break, fire duty; 0.98: anywhere): craving 0.75+, something in the pocket (tobacco first when both hands are free, else a ready one), a light (own lighter, or somebody within 4 m: "Got a light?") -> Body_smoke. None: bum one off somebody within 6 m (refused under 3 left, annoyed, or not willing: "Buy your own"); the player is asked. Cooldowns 20-80 s. Body (`body.c` updateSmoking): roll 7 s (both hands at the lap, licked at the end; waits while the hands are needed), light 1.8 s (in the lips, lighter hand and the cupping hand at the face), burns 200 s (x4 during a drag), a drag every 8-20 s (nervous: sooner) of 1.4 s, then 1.4 s breathing out (puffs every 0.14 s; a thread off the tip every 0.35 s). Hand needed: into the mouth; back to a free hand after a drag. Standing still the hand is held up in front (`cig_hold`). Drawn in the figure shader (`cigA`, `cigTip`, `cigGlow`: filter, paper, ash, ember). Butt (`OBJ_TRASH` made with size.x < 0, "Cigarette butt"): flicked or dropped; within 3 m of a burning fire it goes in. Down (a fall): dropped. Player K (`game_actions.c` smokeOrStop); talk choice 3: "Can I have a cigarette?" / "Want a cigarette?" (lighter topics first). + +Smoke puffs (`chips.c` Chips_puff): small blocks drifting up, growing, drawn without the ink outline (`Renderer_drawSmoke`: no normal/depth write). + +Shadows: 3x3 PCF with each tap blended bilinearly between its 4 texels (`common.glsl` shadowVisibility, 16 lookups): no stair steps at the edges. + 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. @@ -162,7 +169,9 @@ Most important first. - More gestures and feelings to pick personalities from: the lists in Body / Fidgets and Gestures can grow **People** -- 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 +- The work place: a cramped or badly fitting spot - take the work (the log, the tools) somewhere with room, call somebody over to help move heavy things; or (tidy people) clear the place first +- Ladders: an opened ladder carried nearly upright (else too hard to carry); put it down anywhere, physics decides if it stands; an unsteady one may fall when climbed; somebody holds it (asked, or helpful people on their own when they see you on it), or a foot on the bottom rung +- Smoking, still to do: buying more (a kiosk), the mood (nerves) from a craving, an ashtray - 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) @@ -175,4 +184,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.75.0 (build 2ada6fc)` +State of `entropie 1.76.0 (build 7028602)` diff --git a/entropie/assets/shaders/common.glsl b/entropie/assets/shaders/common.glsl index 5efaa0d..3cd0271 100644 --- a/entropie/assets/shaders/common.glsl +++ b/entropie/assets/shaders/common.glsl @@ -42,19 +42,30 @@ vec3 wobble(vec3 p, float t) { // ---------- Painterly lighting (shared by scene and figure shaders) ---------- -// 1 = lit, 0 = shadowed (3x3 PCF). lightSpacePos is already projected. +// 1 = lit, 0 = shadowed. lightSpacePos is already projected. +// 3x3 PCF, each tap blended between its four texels (bilinear): the edge runs +// smoothly between texels instead of in stair steps. float shadowVisibility(sampler2D map, vec4 lightSpacePos, vec3 n, vec3 l) { vec3 p = lightSpacePos.xyz / lightSpacePos.w * 0.5 + 0.5; if (p.x <= 0.0 || p.x >= 1.0 || p.y <= 0.0 || p.y >= 1.0 || p.z >= 1.0) return 1.0; float bias = 0.0003 + 0.0010 * (1.0 - clamp(dot(n, l), 0.0, 1.0)); - vec2 texel = 1.0 / vec2(textureSize(map, 0)); - float lit = 0.0; - for (int x = -1; x <= 1; ++x) - for (int y = -1; y <= 1; ++y) - lit += (p.z - bias > texture(map, p.xy + vec2(x, y) * texel).r) ? 0.0 : 1.0; - return lit / 9.0; + vec2 size = vec2(textureSize(map, 0)); + vec2 uv = p.xy * size - 0.5; + vec2 cell = floor(uv), f = uv - cell; + float lit[16]; // compare results on the 4x4 texels round the point + for (int y = 0; y < 4; ++y) + for (int x = 0; x < 4; ++x) + lit[y * 4 + x] = (p.z - bias > texture(map, (cell + vec2(x - 1, y - 1) + 0.5) / size).r) ? 0.0 : 1.0; + float sum = 0.0; + for (int y = 0; y < 3; ++y) + for (int x = 0; x < 3; ++x) { + float a = mix(lit[y * 4 + x], lit[y * 4 + x + 1], f.x); + float b = mix(lit[(y + 1) * 4 + x], lit[(y + 1) * 4 + x + 1], f.x); + sum += mix(a, b, f.y); + } + return sum / 9.0; } // Blotchy paint patches and brush streaks. Returns the patch value in .w. diff --git a/entropie/assets/shaders/figure.frag b/entropie/assets/shaders/figure.frag index 7e16967..3e46783 100644 --- a/entropie/assets/shaders/figure.frag +++ b/entropie/assets/shaders/figure.frag @@ -39,6 +39,8 @@ 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; // to pee: squatting trousers down / skirt up (0..1); standing fly open / skirt lifted in front (-1..0) +uniform vec3 cigA, cigTip; // a cigarette (figure space): the end at the hand / lips, the burning end +uniform float cigGlow; // -1 none, 0 not lit .. 1 a drag uniform int top; // 0 a shirt, 1 bare-chested, 2 just a top (shirt off in the sun) uniform mat4 viewProj; @@ -54,14 +56,14 @@ uniform float shadowStrength; uniform float boilTime; uniform sampler2D shadowMap; -const int M_SKIN = 0, M_HAIR = 1, M_SHIRT = 2, M_PANTS = 3, M_SHOES = 4, M_PEE = 5, M_SICK = 6, M_WATER = 7; +const int M_SKIN = 0, M_HAIR = 1, M_SHIRT = 2, M_PANTS = 3, M_SHOES = 4, M_PEE = 5, M_SICK = 6, M_WATER = 7, M_CIG = 8; const float FIGURE_LIGHT_WRAP = 0.6; // soft, flat faces instead of a hard light split const float SELF_SHADOW_SKIP = 0.10; // meters // ---------- Body parts ---------- -const int MAX_PARTS = 28; +const int MAX_PARTS = 30; vec3 partA[MAX_PARTS], partB[MAX_PARTS]; float partR1[MAX_PARTS], partR2[MAX_PARTS]; float partBlend[MAX_PARTS]; // smooth-union radius with the rest of the body @@ -147,6 +149,9 @@ void buildBody() { addPart(onHead(vec3(-0.090, -0.005, -0.005)), onHead(vec3(-0.090, -0.020, -0.005)), 0.020, 0.018, 0.01, 1.0, M_SKIN); addPart(onHead(vec3( 0.090, -0.005, -0.005)), onHead(vec3( 0.090, -0.020, -0.005)), 0.020, 0.018, 0.01, 1.0, M_SKIN); + // A cigarette + if (cigGlow >= 0.0) addPart(cigA, cigTip, 0.0045, 0.0042, 0.001, 1.0, M_CIG); + // A stream down to the ground, thinning out (pee) or in clumps (sick) if (streamKind > 0) { int m = streamKind == 1 ? M_PEE : streamKind == 3 ? M_WATER : M_SICK; @@ -303,6 +308,7 @@ vec3 materialColor(int mat) { if (mat == M_PEE) return vec3(0.96, 0.86, 0.38); if (mat == M_SICK) return vec3(0.70, 0.66, 0.30); if (mat == M_WATER) return vec3(0.78, 0.90, 0.96); + if (mat == M_CIG) return vec3(0.96, 0.95, 0.92); return skinColor; } @@ -379,6 +385,12 @@ void main() { if (mat == M_SKIN) base = faceDetail(base, p); else if (mat == M_HAIR) base = hairDetail(base, p); else if (mat < M_PEE) base = clothDetail(mat, base, p); + float ember = 0.0; + if (mat == M_CIG) { // filter at the hand end, ash and ember at the other + float along = dot(p - cigA, normalize(cigTip - cigA)), left = length(cigTip - cigA) - along; + if (along < 0.02) base = vec3(0.85, 0.58, 0.32); + if (left < 0.006) { base = mix(vec3(0.55, 0.53, 0.5), vec3(1.0, 0.35, 0.08), step(0.05, cigGlow)); ember = cigGlow; } + } vec3 l = normalize(lightDirection); // Step towards the light first, so tiny features (nose, chin) don't shadow the face @@ -388,6 +400,7 @@ void main() { vec3 color = paintLight(mix(base, paint.rgb, 0.25), n, l, visible, world, boilTime, lightColor, shadowColor, skyTop, FIGURE_LIGHT_WRAP); color += fireGlow(base, n, world); + color += vec3(1.0, 0.4, 0.1) * ember * 1.5; // (a drag: it glows) color = applyFog(color, length(viewPos - world), skyHorizon); OutColor = vec4(color, 1.0); diff --git a/entropie/commit_message.txt b/entropie/commit_message.txt index e0084eb..662ff21 100644 --- a/entropie/commit_message.txt +++ b/entropie/commit_message.txt @@ -1,5 +1,13 @@ -entropie 1.75.0: pockets (Urinellas, lighter), hands free to pee, landings, ladders +entropie 1.76.0: smoking, smooth shadow edges +- Smoking: packs and tobacco in the pockets (used up); smokers get a + craving and light up in a quiet moment - rolling one first, the lighter + to it, drags (the tip glows), smoke breathed out, the butt flicked away +- Shared: "Got a light?", bumming a cigarette (or a no); talk to ask for + one or offer one; K lights yours / puts it out; Tab shows your pockets +- Shadows: soft, smooth edges instead of stair steps + +Includes 1.75.0 (if not committed separately): - 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 diff --git a/entropie/src/core/gl.h b/entropie/src/core/gl.h index 6c8ae17..a407b91 100644 --- a/entropie/src/core/gl.h +++ b/entropie/src/core/gl.h @@ -50,7 +50,8 @@ X(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D) \ X(PFNGLCHECKFRAMEBUFFERSTATUSPROC, glCheckFramebufferStatus) \ X(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers) \ - X(PFNGLDRAWBUFFERSPROC, glDrawBuffers) + X(PFNGLDRAWBUFFERSPROC, glDrawBuffers) \ + X(PFNGLCOLORMASKIPROC, glColorMaski) #define GL_DECLARE_FN(type, name) extern type name; GL_FUNCTION_LIST(GL_DECLARE_FN) diff --git a/entropie/src/core/input.c b/entropie/src/core/input.c index da29c54..487cc08 100644 --- a/entropie/src/core/input.c +++ b/entropie/src/core/input.c @@ -30,6 +30,7 @@ void Input_poll(Input *input) { case SDL_SCANCODE_I: input->show_keys = true; break; case SDL_SCANCODE_TAB: input->show_stats = true; break; case SDL_SCANCODE_P: input->pee = true; break; + case SDL_SCANCODE_K: input->smoke = true; break; case SDL_SCANCODE_Z: input->sleep = true; break; case SDL_SCANCODE_LSHIFT: input->sprint_tap = true; break; case SDL_SCANCODE_1: input->choice = 1; break; diff --git a/entropie/src/core/input.h b/entropie/src/core/input.h index d3bc14e..d7aa749 100644 --- a/entropie/src/core/input.h +++ b/entropie/src/core/input.h @@ -26,6 +26,7 @@ typedef struct { bool click_held; /* left mouse held down: keep doing it */ bool right_click; /* right mouse pressed: put down / let go */ bool pee; /* P pressed */ + bool smoke; /* K pressed */ bool sleep; /* Z pressed: lie down to sleep / get up */ bool toggle_view; /* V pressed: first / third person */ bool call_help; /* G pressed: one more person comes to help */ diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c index 0300a78..5fcf13a 100644 --- a/entropie/src/game/game.c +++ b/entropie/src/game/game.c @@ -104,6 +104,7 @@ void Game_needsText(const Game *game, char *out, size_t size) { needWord(n->thirst, NULL, "parched"), needWord(n->sleepiness, NULL, "exhausted"), needWord(n->bladder, NULL, "bursting"), + game->player->smoker > 0.0f && game->player->craving > 0.85f && !Body_isSmoking(&game->player->body) ? "dying for a smoke" : NULL, }; int k = 0; if (size > 0) out[0] = '\0'; @@ -129,6 +130,7 @@ int Game_stats(const Game *game, StatBar *bars, int max, const char **title, cha { "Felling", p->skill[ORDER_TREES], false }, { "Tidying", p->skill[ORDER_TRASH], false }, { "Fire", p->skill[ORDER_FIRE], false }, + { "Ladder (one hand)", p->ladder_skill, false }, }; const bool skill_page = game->stats_page == 1; const StatBar *all = skill_page ? skills : body; @@ -137,7 +139,10 @@ int Game_stats(const Game *game, StatBar *bars, int max, const char **title, cha for (int i = 0; i < total && count < max; i++) bars[count++] = all[i]; *title = skill_page ? "Skills" : "Body"; if (skill_page) { - snprintf(about, size, "Practice makes you quicker at a job"); + char lighter[24] = "no lighter"; + if (p->lighter) snprintf(lighter, sizeof lighter, p->lighter_uses > 0 ? "lighter (%d)" : "empty lighter", p->lighter_uses); + snprintf(about, size, "Pockets: %d cigarettes, tobacco for %d, %s, %d Urinellas", + p->cigarettes, p->tobacco, lighter, p->urinellas); } else { float jump = Object_jumpSpeed(p); snprintf(about, size, "Sprint %.1f m/s Jump %d cm %d kg %s-handed", diff --git a/entropie/src/game/game_actions.c b/entropie/src/game/game_actions.c index cb92e14..539e29a 100644 --- a/entropie/src/game/game_actions.c +++ b/entropie/src/game/game_actions.c @@ -340,6 +340,32 @@ 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. */ +/* K: a cigarette (rolled if all you have is tobacco: both hands free), lit with + your lighter or somebody's next to you; K again puts it out. */ +static void smokeOrStop(Game *game) { + Object *p = game->player; + if (Body_isSmoking(&p->body)) { + Body_stopSmoking(&p->body); + say(game, "You put the cigarette out"); + return; + } + if (!Pockets_hasSmoke(p)) { say(game, "Nothing to smoke - ask somebody for one (talk to them)"); return; } + Object *light = p->lighter && p->lighter_uses > 0 ? p + : Pockets_lighterNear(p, game->world.objects, game->world.count, 2.5f); + if (!light) { say(game, p->lighter ? "Your lighter is empty - stand next to somebody with one" : "No lighter - stand next to somebody with one"); return; } + bool both = p->hands.state[HAND_LEFT] == HAND_FREE && p->hands.state[HAND_RIGHT] == HAND_FREE; + bool rolled = false; + if (!Pockets_takeSmoke(p, both, &rolled)) { say(game, "Both hands free to roll one"); return; } + int hand = p->hands.state[p->main_hand] == HAND_FREE ? p->main_hand : Hands_freeHand(p); + if (!Body_smoke(&p->body, rolled, hand >= 0 ? hand : 2)) { + if (rolled) p->tobacco++; else p->cigarettes++; + return; + } + Pockets_light(light); + if (light != p && light->type == OBJ_HUMAN) Ai_say(light, "Here, a light."); + say(game, rolled ? "You roll a cigarette" : light != p ? "Somebody gives you a light" : "You light a cigarette"); +} + /* 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. */ @@ -1017,6 +1043,7 @@ void game_handleActions(Game *game, const Input *input) { /* T: tap to toss it, hold to wind up (overhand, harder), let go to throw */ if (input->throw_held && (input->throw_item || Hands_isWindingUp(p))) Hands_windUp(p); else if (Hands_isWindingUp(p) || input->throw_item) Hands_throw(p, game->camera.front); + if (input->smoke) smokeOrStop(game); if (input->pee) { Object *toilet = furnitureNear(game, FURN_TOILET, 1.3f); if (p->needs.bladder <= 0.15f) say(game, "No need right now"); diff --git a/entropie/src/people/ai.c b/entropie/src/people/ai.c index 57ae80f..d359ac0 100644 --- a/entropie/src/people/ai.c +++ b/entropie/src/people/ai.c @@ -1184,6 +1184,64 @@ static void lighterManners(Object *self, Object *const *objects, int count, floa } } +/* Smokers: the craving grows; when it's strong and there's a quiet moment, a + cigarette (rolled if there's tobacco and both hands are free), lit with + their lighter or somebody's near ("Got a light?"). None left: they bum one + off somebody near (the player is asked). */ +static bool quietMoment(const Object *self) { + const Ai *ai = &self->ai; + if (ai->talking || self->climbing || self->on_ladder || Body_isBusy(&self->body)) return false; + if (ai->mode == AI_IDLE || ai->mode == AI_WANDER || self->sitting) return true; + return ai->mode == AI_WORK && (ai->resting || ai->order == ORDER_FIRE); +} + +static void smokeBreak(Object *self, Object *const *objects, int count, float dt) { + Ai *ai = &self->ai; + if (self->smoker <= 0.0f) return; + self->smoke_ask = fmaxf(0.0f, self->smoke_ask - dt); + if (Body_isSmoking(&self->body)) return; /* (the craving: see the world, smoke) */ + if (self->craving < 0.75f || self->smoke_ask > 0.0f || ai->speech_time > 0.0f) return; + if (!quietMoment(self) && self->craving < 0.98f) return; /* (dying for one: whatever they're doing) */ + int hand = Hands_freeHand(self); + bool both = self->hands.state[HAND_LEFT] == HAND_FREE && self->hands.state[HAND_RIGHT] == HAND_FREE; + if (!Pockets_hasSmoke(self)) { + Object *giver = Pockets_smokesNear(self, objects, count, 6.0f); + self->smoke_ask = 40.0f + 40.0f * random01(ai); + if (!giver) { + if (self->craving > 0.95f) Ai_say(self, "I could really use a smoke..."); + return; + } + if (giver->type == OBJ_PLAYER) { Ai_say(self, "Hey - got a cigarette for me?"); return; } + Ai_say(self, withName("Can I bum a cigarette, %s?", giver)); + int has = giver->cigarettes + giver->tobacco; + if (giver->type == OBJ_HUMAN && (has < 3 || giver->ai.annoyed > 0.5f || random01(ai) > 0.4f + 0.6f * giver->ai.character.willing)) + Ai_say(giver, has < 3 ? "Sorry, those are my last ones." : "Buy your own for once."); + else if (Pockets_giveSmoke(giver, self)) { + if (giver->type == OBJ_HUMAN) Ai_say(giver, giver->tobacco > 0 && giver->cigarettes == 0 ? "Here, I rolled you one." : "Sure, here."); + self->smoke_ask = 2.0f; /* (and lights it in a moment) */ + } + return; + } + Object *light = self->lighter && self->lighter_uses > 0 ? self : Pockets_lighterNear(self, objects, count, 4.0f); + if (!light) { + Ai_say(self, "Anyone got a light?"); + self->smoke_ask = 45.0f; + return; + } + bool rolled = false; + self->smoke_ask = 20.0f; /* (whatever happens: not again right away) */ + if (!Pockets_takeSmoke(self, both, &rolled)) return; /* (only tobacco and the hands are busy: later) */ + if (!Body_smoke(&self->body, rolled, hand >= 0 ? (self->hands.state[self->main_hand] == HAND_FREE ? self->main_hand : hand) : 2)) { + if (rolled) self->tobacco++; else self->cigarettes++; /* (back in the pocket) */ + return; + } + Pockets_light(light); + if (light != self) { + Ai_say(self, light->type == OBJ_PLAYER ? "Thanks for the light!" : withName("Got a light, %s?", light)); + if (light->type == OBJ_HUMAN && light->ai.speech_time <= 0.0f) Ai_say(light, "Here."); + } +} + void Ai_update(Object *self, Object *const *objects, int count, float dt) { ai_ensureReady(self); Ai *ai = &self->ai; @@ -1217,6 +1275,7 @@ void Ai_update(Object *self, Object *const *objects, int count, float dt) { ai_sunShirt(self, dt); if (takeBack(self)) return; lighterManners(self, objects, count, dt); + smokeBreak(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/body.c b/entropie/src/people/body.c index bcb85b8..6fe3847 100644 --- a/entropie/src/people/body.c +++ b/entropie/src/people/body.c @@ -304,6 +304,71 @@ void Body_trip(Body *b, int foot, Vec3 facing, float severity) { Body_fall(b, vec3_add(facing, vec3_scale(right, side * 0.7f))); } +/* ---------- Smoking ---------- */ + +#define CIG_TIME 200.0f /* seconds it burns (a bit quicker with each drag) */ +#define EXHALE_TIME 1.4f + +bool Body_smoke(Body *b, bool rolled, int hand) { + if (b->smoke_phase != SMOKE_NONE || b->pose != POSE_UP) return false; + b->smoke_phase = rolled ? SMOKE_ROLL : SMOKE_LIGHT; + b->smoke_t = 0.0f; + b->cig_left = 1.0f; + b->cig_hand = hand; + b->drag = b->exhale = 0.0f; + b->drag_timer = 4.0f; + return true; +} + +bool Body_isSmoking(const Body *b) { + return b->smoke_phase != SMOKE_NONE; +} + +void Body_stopSmoking(Body *b) { + if (b->smoke_phase == SMOKE_NONE) return; + b->butt = b->smoke_phase == SMOKE_ON; /* (half rolled: just put away) */ + b->smoke_phase = SMOKE_NONE; + b->drag = b->exhale = 0.0f; +} + +bool Body_takeButt(Body *b) { + bool butt = b->butt; + b->butt = false; + return butt; +} + +static void updateSmoking(Body *b, float speed, float dt) { + if (b->smoke_phase == SMOKE_NONE) return; + if (b->pose != POSE_UP) { Body_stopSmoking(b); return; } /* (down: out of the hand) */ + b->smoke_t += dt; + b->exhale = fmaxf(0.0f, b->exhale - dt); + if (b->smoke_phase == SMOKE_ROLL) { + if (b->hand_weight[0] > 0.5f || b->hand_weight[1] > 0.5f || speed > 1.0f) { b->smoke_t = fminf(b->smoke_t, 0.5f); return; } /* (hands needed: rolling waits) */ + if (b->smoke_t >= ROLL_CIG_TIME) { b->smoke_phase = SMOKE_LIGHT; b->smoke_t = 0.0f; } + return; + } + if (b->smoke_phase == SMOKE_LIGHT) { + if (b->smoke_t >= LIGHT_TIME) { b->smoke_phase = SMOKE_ON; b->smoke_t = 0.0f; b->exhale = EXHALE_TIME; } + return; + } + float hold = speed < 0.5f && b->cig_hand < 2 ? 1.0f : 0.0f; /* (standing about: up in front; walking: it swings along) */ + b->cig_hold += clampf(hold - b->cig_hold, -2.5f * dt, 2.5f * dt); + /* In a hand that's needed for something: into the mouth; back to a free hand after a drag */ + if (b->cig_hand < 2 && b->hand_weight[b->cig_hand] > 0.3f) b->cig_hand = 2; + b->cig_left -= dt / CIG_TIME * (b->drag > 0.0f ? 4.0f : 1.0f); + if (b->drag > 0.0f && (b->drag -= dt) <= 0.0f) { + b->drag = 0.0f; + b->exhale = EXHALE_TIME; + if (b->cig_hand == 2) + for (int h = 1; h >= 0; h--) if (b->hand_weight[h] < 0.05f) { b->cig_hand = h; break; } + } + if (b->drag <= 0.0f && (b->drag_timer -= dt) <= 0.0f) { + b->drag = DRAG_TIME; + b->drag_timer = 8.0f + 12.0f * random01(b) - 4.0f * b->shown.nerves; /* (nervous: more often) */ + } + if (b->cig_left <= 0.0f) { b->butt = true; b->smoke_phase = SMOKE_NONE; b->drag = 0.0f; } +} + bool Body_takeLanding(Body *b, int *foot, Vec3 *at) { if (!b->landed) return false; *foot = b->landed - 1; @@ -879,6 +944,7 @@ void Body_update(Body *b, const BodyFrame *f) { } float speed = vec3_length(b->velocity); updateExpression(b, f, speed); + updateSmoking(b, speed, dt); if (updateDown(b, f, s)) return; /* Side steps and backward steps are shorter than steps forward; shy people take small steps */ diff --git a/entropie/src/people/body.h b/entropie/src/people/body.h index f207e71..45cb148 100644 --- a/entropie/src/people/body.h +++ b/entropie/src/people/body.h @@ -236,8 +236,21 @@ typedef struct { /* Peeing */ Vec3 pee_aim; /* world direction to aim at (zero: just ahead and down) */ float pee_power; /* 0 = normal .. 1 = pushing hard: further, done sooner */ + + /* Smoking (see Body_smoke) */ + int smoke_phase; /* SMOKE_NONE / _ROLL (rolling one) / _LIGHT / _ON */ + float smoke_t; /* seconds in this phase */ + float cig_left; /* 1 = fresh .. 0 = down to the filter */ + int cig_hand; /* HAND_LEFT / HAND_RIGHT, or 2: in the mouth */ + float drag; /* > 0 while taking a drag (seconds left) */ + float drag_timer; /* until the next one */ + float exhale; /* > 0: smoke coming out (seconds left) */ + float cig_hold; /* 0..1 eased: the cigarette hand up in front (standing about) */ + bool butt; /* finished or dropped: the world drops a butt (taken) */ } Body; +enum { SMOKE_NONE, SMOKE_ROLL, SMOKE_LIGHT, SMOKE_ON }; + typedef struct { Vec3 position; /* character base point */ Vec3 facing; @@ -266,6 +279,14 @@ void Body_fall(Body *body, Vec3 dir); 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); +/* Lights a cigarette (rolled: rolls it first, both hands) into hand (or 2: the mouth). + It burns down over a few minutes, a drag now and then, smoke breathed out. */ +bool Body_smoke(Body *body, bool rolled, int hand); +bool Body_isSmoking(const Body *body); +/* Done with it early: dropped (a butt for the world). */ +void Body_stopSmoking(Body *body); +/* A cigarette just finished or dropped (the world drops the butt). */ +bool Body_takeButt(Body *body); /* 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_internal.h b/entropie/src/people/body_internal.h index 1045831..9a10701 100644 --- a/entropie/src/people/body_internal.h +++ b/entropie/src/people/body_internal.h @@ -208,6 +208,11 @@ static inline float runAmount(float speed) { return clampf((speed - RUN_SPEED) / 2.5f, 0.0f, 1.0f); } +/* Smoking (body.c) */ +#define ROLL_CIG_TIME 7.0f /* rolling one */ +#define LIGHT_TIME 1.8f /* the flame to it, the first drag */ +#define DRAG_TIME 1.4f + /* Peeing: 0..1, hands at the belt, head down a little. */ static inline float peeing(const Body *b) { if (b->pee <= 0.0f) return 0.0f; diff --git a/entropie/src/people/body_pose.c b/entropie/src/people/body_pose.c index a327817..2c0e43c 100644 --- a/entropie/src/people/body_pose.c +++ b/entropie/src/people/body_pose.c @@ -317,6 +317,44 @@ static Vec3 turnTowards(Vec3 a, Vec3 b, float t, Vec3 via) { clear of the shoulder (out in front of it), never through it; the elbow turns with it, down and back on the way. Smooth in the weight and in both goals, whatever they are. */ +/* Smoking: where this hand goes (rolling one, the lighter to it, a drag, holding it + up in front) and how much (0: none). Unscaled figure space, like the other goals. */ +static float smokeArm(const Body *b, const Vec3 *j, const Skeleton *sk, int side, Vec3 *palm, Vec3 *pole) { + if (b->smoke_phase == SMOKE_NONE || b->hand_weight[side] > 0.95f) return 0.0f; + const float sign = side == 0 ? -1.0f : 1.0f; + Vec3 head = j[JOINT_HEAD], fwd = sk->head_forward, up = sk->head_up; + Vec3 hs = vec3_normalize(vec3_cross(up, fwd)); + if (hs.x < 0.0f) hs = vec3_scale(hs, -1.0f); + Vec3 mouth = vec3_add(head, vec3_add(vec3_scale(fwd, 0.13f), vec3_scale(up, -0.075f))); + *pole = vec3(sign * 0.4f, -1.0f, 0.2f); + float t = b->smoke_t, w = 0.0f; + if (b->smoke_phase == SMOKE_ROLL) { /* both hands in front, the paper between them; licked at the end */ + w = smoothstepf(0.0f, 0.5f, t); + float lick = smoothstepf(ROLL_CIG_TIME - 1.1f, ROLL_CIG_TIME - 0.7f, t); + float roll = 0.008f * sinf(t * 7.0f + (side ? PI_F : 0.0f)) * (1.0f - lick); + Vec3 lap = vec3_add(j[JOINT_PELVIS], vec3(sign * 0.035f, 0.2f + roll, 0.27f)); + *palm = vec3_lerp(lap, vec3_add(mouth, vec3_scale(hs, sign * 0.035f)), lick); + if (lick <= 0.0f) *pole = vec3(sign * 0.8f, -0.5f, -0.3f); + return w; + } + if (b->smoke_phase == SMOKE_LIGHT) { /* in the mouth; the lighter to it, the other hand round the flame */ + w = smoothstepf(0.0f, 0.4f, t) * (1.0f - smoothstepf(LIGHT_TIME - 0.3f, LIGHT_TIME, t)); + bool lighter = side == 1; + *palm = vec3_add(mouth, vec3_add(vec3_scale(fwd, lighter ? 0.07f : 0.06f), + vec3_add(vec3_scale(up, lighter ? -0.05f : -0.02f), vec3_scale(hs, sign * 0.035f)))); + return w; + } + if (b->cig_hand != side) return 0.0f; + float d = DRAG_TIME - b->drag; /* (into the drag) */ + float drag = b->drag > 0.0f ? smoothstepf(0.0f, 0.45f, d) * (1.0f - smoothstepf(DRAG_TIME - 0.4f, DRAG_TIME, d)) : 0.0f; + Vec3 held = vec3_add(j[side == 0 ? JOINT_SHOULDER_L : JOINT_SHOULDER_R], vec3(sign * 0.09f, -0.24f, 0.26f)); /* forearm up, hand in front of the chest */ + Vec3 lips = vec3_add(mouth, vec3_scale(hs, sign * 0.02f)); + w = fmaxf(drag, 0.85f * b->cig_hold); + *palm = vec3_lerp(held, lips, clampf(drag / fmaxf(w, 1e-3f), 0.0f, 1.0f)); /* (a drag from wherever it was) */ + if (drag < 0.5f) *pole = vec3(sign * 0.6f, -1.0f, -0.3f); + return w; +} + static void blendArm(ArmGoal *arm, Vec3 palm, Vec3 pole, float w) { w = clampf(w, 0.0f, 1.0f); if (w <= 0.0f) return; @@ -699,6 +737,13 @@ static void solveStanding(Body *b, Vec3 position, Vec3 facing, float s, float dt if (akimbo > 0.001f) blendArm(&arm, vec3_add(j[JOINT_PELVIS], vec3(sign * 0.19f, 0.03f, -0.01f)), vec3(sign, 0.0f, -0.6f), akimbo); + /* Smoking: rolling, lighting, a drag, holding it */ + { + Vec3 palm, pole; + float smoke = smokeArm(b, j, out, side, &palm, &pole) * (1.0f - b->hand_weight[side]); + if (smoke > 0.001f) blendArm(&arm, palm, pole, smoke); + } + /* Actions: reaching, carrying, pushing */ float w = b->hand_weight[side]; Vec3 target = vec3_scale(Figure_toLocal(position, facing, b->hand_target[side]), 1.0f / s); @@ -928,6 +973,43 @@ static Vec3 arcAt(const StreamArc *a, float t) { return vec3_add(vec3_add(a->start, vec3_scale(a->dir, a->speed * t)), vec3(0.0f, -a->down * t - 0.5f * GRAVITY * t * t, 0.0f)); } +/* The cigarette, from the final pose: between the fingers, in the lips, or the + paper between the hands being rolled. */ +static void placeCigarette(const Body *b, float s, Skeleton *out) { + out->cig_glow = -1.0f; + if (b->smoke_phase == SMOKE_NONE || b->pose != POSE_UP) return; + const Vec3 *j = out->joints; + float len = 0.078f * s * (0.3f + 0.7f * clampf(b->cig_left, 0.0f, 1.0f)); + if (b->smoke_phase == SMOKE_ROLL) { + Vec3 l = j[JOINT_HAND_L], r = j[JOINT_HAND_R], mid = vec3_scale(vec3_add(l, r), 0.5f); + Vec3 across = vec3_sub(r, l); + across = vec3_length(across) > 1e-3f ? vec3_normalize(across) : vec3(1.0f, 0.0f, 0.0f); + out->cig_a = vec3_sub(mid, vec3_scale(across, 0.035f * s)); + out->cig_tip = vec3_add(mid, vec3_scale(across, 0.035f * s)); + out->cig_glow = 0.0f; + return; + } + Vec3 fwd = out->head_forward, up = out->head_up; + if (b->smoke_phase == SMOKE_LIGHT || b->cig_hand == 2) { /* in the lips, pointing down a little */ + out->cig_a = vec3_add(j[JOINT_HEAD], vec3_scale(vec3_add(vec3_scale(fwd, 0.095f), vec3_scale(up, -0.062f)), s)); + out->cig_tip = vec3_add(out->cig_a, vec3_scale(vec3_normalize(vec3_add(vec3_scale(fwd, 0.9f), vec3_scale(up, -0.35f))), len)); + } else { + int side = b->cig_hand; + float sign = side == 0 ? -1.0f : 1.0f; + Vec3 hand = j[side == 0 ? JOINT_HAND_L : JOINT_HAND_R], wrist = j[side == 0 ? JOINT_WRIST_L : JOINT_WRIST_R]; + Vec3 dir = vec3_sub(hand, wrist); + dir = vec3_length(dir) > 1e-4f ? vec3_normalize(dir) : vec3(0.0f, -1.0f, 0.0f); + Vec3 o = vec3(sign * 0.5f, 0.15f, 0.85f); /* (out of the fingers: forward and away from the body) */ + o = vec3_sub(o, vec3_scale(dir, vec3_dot(o, dir))); + o = vec3_length(o) > 1e-3f ? vec3_normalize(o) : vec3(0.0f, 0.0f, 1.0f); + out->cig_a = vec3_add(hand, vec3_add(vec3_scale(dir, 0.02f * s), vec3_scale(o, -0.012f * s))); + out->cig_tip = vec3_add(out->cig_a, vec3_scale(o, len)); + } + float drag = b->drag > 0.0f ? smoothstepf(0.0f, 0.5f, DRAG_TIME - b->drag) * smoothstepf(0.0f, 0.3f, b->drag) : 0.0f; + out->cig_glow = b->smoke_phase == SMOKE_LIGHT ? smoothstepf(LIGHT_TIME * 0.55f, LIGHT_TIME * 0.8f, b->smoke_t) + : 0.25f + 0.75f * drag; +} + static void solveStream(const Body *b, Vec3 facing, Skeleton *out, StreamArc *arc) { const float GRAVITY = 9.81f; out->stream_kind = STREAM_NONE; @@ -1452,10 +1534,16 @@ void Body_solve(Body *b, Vec3 position, Vec3 facing, float height, float dt, Ske if (b->pose == POSE_UP && b->sit > 0.01f) { Skeleton sitting = *out; solveSitting(&sitting, b->sit_style, b->seat_height / s, sinf(b->clock * 1.6f)); - for (int side = 0; side < 2; side++) { /* busy hands still do their thing sitting down */ + for (int side = 0; side < 2; side++) { /* busy hands still do their thing sitting down (smoking too) */ float w = clampf(b->hand_weight[side], 0.0f, 1.0f); - if (w < 0.001f || !b->hand_ready[side]) continue; int sh = side == 0 ? JOINT_SHOULDER_L : JOINT_SHOULDER_R; + Vec3 smoke_palm, smoke_pole; + float smoke = smokeArm(b, sitting.joints, &sitting, side, &smoke_palm, &smoke_pole) * (1.0f - w); + if (smoke > 0.001f) { + Vec3 at = vec3_lerp(sitting.joints[sh + 3], smoke_palm, clampf(smoke, 0.0f, 1.0f)); + reachArm(sitting.joints, sh, at, smoke_pole); + } + if (w < 0.001f || !b->hand_ready[side]) continue; Vec3 palm = vec3_lerp(sitting.joints[sh + 3], b->hand_local[side], w); Vec3 rel = vec3_sub(palm, sitting.joints[sh]); if (vec3_length(rel) < 0.2f) palm = vec3_add(sitting.joints[sh], vec3_scale(vec3_normalize(vec3_add(rel, vec3(0.0f, -0.05f, 0.1f))), 0.2f)); @@ -1479,6 +1567,7 @@ void Body_solve(Body *b, Vec3 position, Vec3 facing, float height, float dt, Ske limitSpeeds(b, s, dt, out); keepBodyOut(b, position, facing, s, dt, true, out); /* (contact wins over the speed limit) */ keepArmsOut(b, position, facing, s, dt, out); + placeCigarette(b, s, out); b->shown_valid = b->pose == POSE_UP && b->sit < 0.02f && !b->airborne; if (b->shown_valid) { b->ankle_shown[0] = Figure_toWorld(position, facing, out->joints[JOINT_ANKLE_L]); diff --git a/entropie/src/people/pockets.c b/entropie/src/people/pockets.c index d4033b1..1217d45 100644 --- a/entropie/src/people/pockets.c +++ b/entropie/src/people/pockets.c @@ -42,3 +42,39 @@ int Pockets_refill(Object *who) { who->urinellas = POCKET_URINELLAS; return got; } + +bool Pockets_hasSmoke(const Object *who) { + return who->cigarettes > 0 || who->tobacco > 0; +} + +bool Pockets_takeSmoke(Object *who, bool can_roll, bool *rolled) { + if (who->tobacco > 0 && (can_roll || who->cigarettes <= 0)) { + if (!can_roll) return false; /* (only tobacco, and no hands for rolling) */ + who->tobacco--; + *rolled = true; + return true; + } + if (who->cigarettes <= 0) return false; + who->cigarettes--; + *rolled = false; + return true; +} + +Object *Pockets_smokesNear(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 || !Pockets_hasSmoke(o)) 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; +} + +bool Pockets_giveSmoke(Object *from, Object *to) { + if (from->cigarettes > 0) from->cigarettes--; + else if (from->tobacco > 0) from->tobacco--; + else return false; + to->cigarettes++; + return true; +} diff --git a/entropie/src/people/pockets.h b/entropie/src/people/pockets.h index adbb4b0..e63c839 100644 --- a/entropie/src/people/pockets.h +++ b/entropie/src/people/pockets.h @@ -4,7 +4,7 @@ /* * 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). + * always given back), cigarettes or tobacco to roll them (shared, used up). */ #include <stdbool.h> @@ -19,6 +19,15 @@ Object *Pockets_lighterNear(Object *who, Object *const *objects, int count, floa 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); +/* Something to smoke in the pocket (a cigarette, or tobacco to roll one). */ +bool Pockets_hasSmoke(const Object *who); +/* Takes one out: tobacco first if they may roll (can_roll: both hands free), else a ready one. + *rolled: it has to be rolled first. False: nothing (that they can use now). */ +bool Pockets_takeSmoke(Object *who, bool can_roll, bool *rolled); +/* Somebody within range with something to smoke (not who). */ +Object *Pockets_smokesNear(Object *who, Object *const *objects, int count, float range); +/* One cigarette from one pocket to the other (a roll-up for them if it's tobacco). */ +bool Pockets_giveSmoke(Object *from, Object *to); /* Takes Urinellas from a box: how many were taken. */ int Pockets_refill(Object *who); diff --git a/entropie/src/people/talk.c b/entropie/src/people/talk.c index 0d64802..9bb8c82 100644 --- a/entropie/src/people/talk.c +++ b/entropie/src/people/talk.c @@ -209,11 +209,14 @@ static int talkTool(const Object *npc) { } /* 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 }; +/* ... or a cigarette: asking them for one, offering one. */ +enum { LIGHTER_NONE, LIGHTER_MINE_BACK, LIGHTER_RETURN, LIGHTER_BORROW, CIG_ASK, CIG_OFFER }; 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 (npc->smoker > 0.0f && Pockets_hasSmoke(player) && (!Pockets_hasSmoke(npc) || npc->craving > 0.6f)) return CIG_OFFER; + if (!Pockets_hasSmoke(player) && Pockets_hasSmoke(npc)) return CIG_ASK; if ((!player->lighter || player->lighter_uses <= 0) && npc->lighter && npc->lighter_uses > 0) return LIGHTER_BORROW; return LIGHTER_NONE; } @@ -223,6 +226,18 @@ static void talkLighter(Talk *talk, Object *player, int topic) { 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 == CIG_OFFER) { + Pockets_giveSmoke(player, npc); + npc->craving = fmaxf(npc->craving, 0.8f); /* (lit soon) */ + npc->ai.annoyed = fmaxf(0.0f, npc->ai.annoyed - 0.3f); + wrapUp(talk, shy(npc) ? "Oh... thank you!" : "You're a star. Thanks!"); + } else if (topic == CIG_ASK) { + int has = npc->cigarettes + npc->tobacco; + if (has < 3 || npc->ai.annoyed > 0.5f) wrapUp(talk, has < 3 ? "Sorry, those are my last ones." : "Buy your own."); + else { + Pockets_giveSmoke(npc, player); + wrapUp(talk, npc->cigarettes == 0 ? "Here, I rolled you one." : "Sure, have one."); + } } else if (topic == LIGHTER_RETURN) { Pockets_passLighter(player, npc); wrapUp(talk, "Thanks!"); @@ -431,7 +446,8 @@ int Talk_choices(const Talk *talk, const char *out[TALK_CHOICES]) { 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?" }; + 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?" }; int topic = lighterTopic(g_talk_player, talk->with); if (topic != LIGHTER_NONE) out[2] = LIGHTER_LINE[topic]; } diff --git a/entropie/src/render/figure.h b/entropie/src/render/figure.h index e67578b..2454205 100644 --- a/entropie/src/render/figure.h +++ b/entropie/src/render/figure.h @@ -69,6 +69,8 @@ typedef struct { Vec3 stream[4]; int stream_kind; /* STREAM_NONE / STREAM_PEE / STREAM_VOMIT */ float stream_width; /* meters */ + Vec3 cig_a, cig_tip; /* a cigarette (figure space): the end at the hand / lips, the burning end ... */ + float cig_glow; /* ... how it glows (-1: no cigarette, 0: not lit .. 1: a drag) */ float undress; /* to pee: squatting trousers down / skirt up (0..1); standing fly open / skirt lifted in front (-1..0) */ } Skeleton; diff --git a/entropie/src/render/renderer.c b/entropie/src/render/renderer.c index 65877d9..171fc6f 100644 --- a/entropie/src/render/renderer.c +++ b/entropie/src/render/renderer.c @@ -98,6 +98,9 @@ static bool loadShaders(Renderer *r) { f->garment = glGetUniformLocation(f->program, "garment"); f->undress = glGetUniformLocation(f->program, "undress"); f->top = glGetUniformLocation(f->program, "top"); + f->cig_a = glGetUniformLocation(f->program, "cigA"); + f->cig_tip = glGetUniformLocation(f->program, "cigTip"); + f->cig_glow = glGetUniformLocation(f->program, "cigGlow"); f->view_pos = glGetUniformLocation(f->program, "viewPos"); f->light_dir = glGetUniformLocation(f->program, "lightDirection"); f->light_color = glGetUniformLocation(f->program, "lightColor"); @@ -374,6 +377,15 @@ void Renderer_drawMeshTinted(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb glUniform4f(r->scene.tint, 1.0f, 1.0f, 1.0f, 0.0f); } +void Renderer_drawSmoke(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb) { + if (!meshInView(r, mesh, model)) return; + glColorMaski(1, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* (no normal / depth for the ink: no outline) */ + glDepthMask(GL_FALSE); + Renderer_drawMeshTinted(r, mesh, model, rgb, 0.0f); + glDepthMask(GL_TRUE); + glColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); +} + void Renderer_drawGrass(Renderer *r, const Mesh *mesh, float terrain_size, float grass_depth) { if (!meshInView(r, mesh, mat4_identity())) return; glUniform1f(r->scene.grass, terrain_size); @@ -506,6 +518,9 @@ void Renderer_drawFigure(Renderer *r, const FigurePose *pose) { glUniform1i(f->garment, pose->look.garment); glUniform1f(f->undress, sk->undress); glUniform1i(f->top, pose->look.top); + setVec3(f->cig_a, sk->cig_a); + setVec3(f->cig_tip, sk->cig_tip); + glUniform1f(f->cig_glow, sk->cig_glow); 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 7bca689..8c58413 100644 --- a/entropie/src/render/renderer.h +++ b/entropie/src/render/renderer.h @@ -68,6 +68,7 @@ typedef struct { GLint head_forward, head_up, chest_forward, waist_bend, face, expression; GLint stream, stream_kind, stream_width; GLint garment, undress, top; + GLint cig_a, cig_tip, cig_glow; GLint view_pos, light_dir, light_color, shadow_color, sky_top, sky_horizon; GLint shadow_strength, shadow_map, fire_light; } FigureShader; @@ -129,6 +130,8 @@ void Renderer_drawFigure(Renderer *r, const FigurePose *pose); void Renderer_beginScenePass(Renderer *r, const Camera *camera, const Lighting *light); /* Like Renderer_drawMesh, darkened by rgb and glowing like embers by glow (0..1). */ void Renderer_drawMeshTinted(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb, float glow); +/* Like Renderer_drawMeshTinted, but no ink outline round it and nothing hides behind it (smoke). */ +void Renderer_drawSmoke(Renderer *r, const Mesh *mesh, Mat4 model, Vec3 rgb); /* Grass blades (see world/meadow.h): trampled with the terrain, swaying. */ void Renderer_drawGrass(Renderer *r, const Mesh *mesh, float terrain_size, float grass_depth); void Renderer_drawMesh(Renderer *r, const Mesh *mesh, Mat4 model); diff --git a/entropie/src/render/shapes.h b/entropie/src/render/shapes.h index 5e4c89b..6333729 100644 --- a/entropie/src/render/shapes.h +++ b/entropie/src/render/shapes.h @@ -88,6 +88,8 @@ Mesh Shapes_flames(void); /* Litter, centered on the origin; kind as in OBJ_TRASH (a palette). */ Mesh Shapes_trash(Palette kind, unsigned seed); +/* A cigarette butt: the filter and a burnt end. */ +Mesh Shapes_butt(void); /* Size of a piece of litter. */ Vec3 Shapes_trashSize(Palette kind); /* A black garbage bag, centered; fullness 0 (empty) .. 1 (full). */ diff --git a/entropie/src/render/shapes_props.c b/entropie/src/render/shapes_props.c index 715080a..697f8a5 100644 --- a/entropie/src/render/shapes_props.c +++ b/entropie/src/render/shapes_props.c @@ -502,6 +502,14 @@ Vec3 Shapes_trashSize(Palette kind) { } } +Mesh Shapes_butt(void) { + MeshBuilder b; + MeshBuilder_init(&b); + addBlock(&b, vec3(-0.006f, 0.0f, 0.0f), vec3(0.018f, 0.011f, 0.011f), vec3(0.86f, 0.6f, 0.34f)); /* filter */ + addBlock(&b, vec3(0.009f, 0.0f, 0.0f), vec3(0.012f, 0.01f, 0.01f), vec3(0.55f, 0.53f, 0.5f)); /* burnt end */ + return Mesh_fromBuilder(&b); +} + Mesh Shapes_trash(Palette kind, unsigned seed) { const Vec3 size = Shapes_trashSize(kind); MeshBuilder b; diff --git a/entropie/src/world/chips.c b/entropie/src/world/chips.c index e4808b2..cb806e6 100644 --- a/entropie/src/world/chips.c +++ b/entropie/src/world/chips.c @@ -58,11 +58,37 @@ void Chips_spray(Chips *c, Vec3 at, Vec3 dir, int n, bool dust) { } } +#define SMOKE_LIFE 1.8f + +void Chips_puff(Chips *c, Vec3 at, Vec3 dir, bool small) { + Chip *ch = &c->chips[c->next]; + c->next = (c->next + 1) % CHIP_MAX; + Vec3 jitter = vec3(rnd(c) - 0.5f, rnd(c) - 0.5f, rnd(c) - 0.5f); + *ch = (Chip){ + .p = vec3_add(at, vec3_scale(jitter, 0.01f)), + .v = vec3_add(vec3_scale(dir, small ? 0.05f : 0.6f + 0.3f * rnd(c)), vec3_add(vec3_scale(jitter, 0.1f), vec3(0.0f, 0.1f, 0.0f))), + .life = (small ? 0.6f : 1.0f) * SMOKE_LIFE * (0.7f + 0.5f * rnd(c)), + .size = small ? 0.006f : 0.012f, + .angle = 6.28f * rnd(c), + .spin = (rnd(c) - 0.5f) * 2.0f, + .color = vec3_scale(vec3(0.86f, 0.87f, 0.9f), 0.9f + 0.1f * rnd(c)), + .smoke = true, + }; +} + void Chips_update(Chips *c, const Terrain *terrain, float dt) { for (int i = 0; i < CHIP_MAX; i++) { Chip *ch = &c->chips[i]; if (ch->life <= 0.0f) continue; ch->life -= dt; + if (ch->smoke) { /* slows down, rises, spreads */ + ch->v = vec3_scale(ch->v, expf(-2.5f * dt)); + ch->v.y += 0.35f * dt; + ch->p = vec3_add(ch->p, vec3_scale(ch->v, dt)); + ch->size += 0.03f * dt; + ch->angle += ch->spin * dt; + continue; + } if (ch->down) continue; float drag = ch->dust ? 3.5f : 0.6f; ch->v = vec3_scale(ch->v, expf(-drag * dt)); @@ -83,10 +109,11 @@ void Chips_draw(const Chips *c, Renderer *renderer) { for (int i = 0; i < CHIP_MAX; i++) { const Chip *ch = &c->chips[i]; if (ch->life <= 0.0f) continue; - float shrink = fminf(1.0f, ch->life / (ch->dust ? 0.3f : 1.5f)); /* (fading away at the end) */ + float shrink = fminf(1.0f, ch->life / (ch->smoke ? 0.5f : ch->dust ? 0.3f : 1.5f)); /* (fading away at the end) */ float s = ch->size * shrink; Mat4 m = mat4_mul(mat4_translate(ch->p), mat4_mul(mat4_rotate_yxz(vec3(ch->down ? 0.0f : ch->angle, ch->angle * 0.7f, 0.0f)), - mat4_scale(vec3(s, s * (ch->dust ? 1.0f : 0.35f), s * (ch->dust ? 1.0f : 0.7f))))); - Renderer_drawMeshTinted(renderer, &c->mesh, m, ch->color, 0.0f); + mat4_scale(vec3(s, s * (ch->dust || ch->smoke ? 1.0f : 0.35f), s * (ch->dust || ch->smoke ? 1.0f : 0.7f))))); + if (ch->smoke) Renderer_drawSmoke(renderer, &c->mesh, m, ch->color); + else Renderer_drawMeshTinted(renderer, &c->mesh, m, ch->color, 0.0f); } } diff --git a/entropie/src/world/chips.h b/entropie/src/world/chips.h index 7d83cc3..333e546 100644 --- a/entropie/src/world/chips.h +++ b/entropie/src/world/chips.h @@ -3,7 +3,8 @@ /* * Bits that fly off work: wood chips off the axe (they land and lie a while) - * and sawdust out of the chainsaw (it drifts down and is gone). + * and sawdust out of the chainsaw (it drifts down and is gone); puffs of + * cigarette smoke. */ #include <stdbool.h> @@ -22,6 +23,7 @@ typedef struct { float angle, spin; Vec3 color; bool dust; + bool smoke; /* a puff of smoke: drifts up, grows, gone */ bool down; /* lying on the ground */ } Chip; @@ -36,6 +38,8 @@ void Chips_init(Chips *c); void Chips_destroy(Chips *c); /* n bits from at, flying out along dir (roughly); dust: sawdust, else wood chips. */ void Chips_spray(Chips *c, Vec3 at, Vec3 dir, int n, bool dust); +/* A little puff of smoke at at (breathed out along dir, or off a cigarette's tip: small). */ +void Chips_puff(Chips *c, Vec3 at, Vec3 dir, bool small); void Chips_update(Chips *c, const Terrain *terrain, float dt); void Chips_draw(const Chips *c, Renderer *renderer); diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index 0e16c22..f5c5109 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -214,8 +214,8 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size case OBJ_TRASH: obj->variant = (int)palette; - obj->size = Shapes_trashSize(palette); - obj->mesh = Shapes_trash(palette, (unsigned)(position.x * 131.0f + position.z * 71.0f)); + obj->size = size.x < 0.0f ? vec3(0.03f, 0.012f, 0.012f) : Shapes_trashSize(palette); /* (a cigarette butt) */ + obj->mesh = size.x < 0.0f ? Shapes_butt() : Shapes_trash(palette, (unsigned)(position.x * 131.0f + position.z * 71.0f)); obj->pivot = vec3(0.0f, obj->size.y * 0.5f, 0.0f); addCollider(obj, vec3_zero(), obj->size, vec3_zero()); break; diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index 52aa785..be758df 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -62,7 +62,8 @@ typedef enum { fuel = kg of wood in it, burning = lit (see camp.h) */ OBJ_SAUSAGE, /* size.y = length; food, best grilled */ OBJ_TRASH, /* litter, palette: PALETTE_WARM paper, PALETTE_MODERN a can, - PALETTE_STANDARD plastic, PALETTE_GROUND a broken bit of junk */ + PALETTE_STANDARD plastic, PALETTE_GROUND a broken bit of junk; + size.x < 0: a cigarette butt */ OBJ_BAG, /* garbage bag; stored = pieces of trash in it */ OBJ_BIN, /* static: a big waste container, size = box; trash and bags go in */ OBJ_BUSH, /* static: a round bush, size = (diameter, height, -) */ @@ -171,6 +172,12 @@ typedef struct Object { 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) */ + int cigarettes; /* characters: ready ones in the pocket (a pack, or roll-ups given) ... */ + int tobacco; /* ... tobacco and papers for this many roll-ups */ + float smoker; /* 0 = doesn't smoke .. 1 = chain smoker: how fast the craving grows */ + float craving; /* 0..1: wants a smoke */ + float puff_timer; /* smoke breathed out: until the next puff */ + float smoke_ask; /* until they ask anybody for a cigarette or a light again */ 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) */ diff --git a/entropie/src/world/scene.c b/entropie/src/world/scene.c index 2dc2e74..edf9752 100644 --- a/entropie/src/world/scene.c +++ b/entropie/src/world/scene.c @@ -85,6 +85,10 @@ static const int MAIN_HAND[] = { HAND_RIGHT, HAND_RIGHT, HAND_LEFT, HAND_RIGHT, 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) */ +/* 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 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 }; /* 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 }; @@ -139,6 +143,10 @@ Object *Scene_load(World *world) { 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) */ + obj->smoker = SMOKER[characters % TEMPERAMENT_COUNT]; + obj->cigarettes = CIGARETTES[characters % TEMPERAMENT_COUNT]; + obj->tobacco = TOBACCO[characters % TEMPERAMENT_COUNT]; + obj->craving = 0.3f + 0.15f * (float)(characters % 3); 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/world.c b/entropie/src/world/world.c index fa948ca..a95e152 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -588,6 +588,41 @@ static void overFeet(Object *o) { #define FAR_DETAIL 30.0f /* m from the player: beyond, feet and hands don't look out for things */ +/* Smoking: puffs breathed out and off the tip, the butt when it's done (into a + burning fire close by, else on the ground). */ +#define CRAVE_TIME 300.0f /* s from nothing to a strong craving, for a chain smoker */ + +static void smoke(World *world, Object *o, Vec3 facing, float dt) { + const Skeleton *sk = &o->skeleton; + if (Body_isSmoking(&o->body)) o->craving = fmaxf(0.0f, o->craving - dt / 90.0f); + else o->craving = fminf(1.0f, o->craving + dt * o->smoker / CRAVE_TIME); + if (Body_takeButt(&o->body)) { + Vec3 at = Figure_toWorld(o->position, facing, sk->cig_glow >= 0.0f ? sk->cig_tip : sk->joints[JOINT_HAND_R]); + Object *fire = Camp_fireNear(world->objects, world->count, o->position, 3.0f, true); + if (!fire) { + Object *butt = Object_create(OBJ_TRASH, PALETTE_WARM, at, vec3(-1.0f, 0.0f, 0.0f), 0.005f); + if (butt) { + snprintf(butt->name, sizeof(butt->name), "Cigarette butt"); + Vec3 flick = vec3_add(vec3_scale(facing, 1.2f + 1.5f * worldDice(world)), vec3(0.0f, 1.0f, 0.0f)); + butt->velocity = worldDice(world) < 0.5f ? flick : vec3_zero(); /* (flicked away, or dropped and stepped on) */ + if (!World_add(world, butt)) Object_destroy(butt); + } + } + } + if (sk->cig_glow < 0.0f || o->far_off) return; + if ((o->puff_timer -= dt) > 0.0f) return; + if (o->body.exhale > 0.0f) { /* out of the mouth (and the nose) */ + Vec3 mouth = vec3_add(sk->joints[JOINT_HEAD], vec3_add(vec3_scale(sk->head_forward, 0.11f * o->look.height / 1.75f), + vec3_scale(sk->head_up, -0.06f * o->look.height / 1.75f))); + Vec3 out = Figure_toWorld(vec3_zero(), facing, vec3_add(sk->head_forward, vec3(0.0f, -0.3f, 0.0f))); + Chips_puff(&world->chips, Figure_toWorld(o->position, facing, mouth), out, false); + o->puff_timer = 0.14f; + } else if (sk->cig_glow > 0.0f) { /* a thin thread off the tip */ + Chips_puff(&world->chips, Figure_toWorld(o->position, facing, sk->cig_tip), vec3(0.0f, 1.0f, 0.0f), true); + o->puff_timer = 0.35f; + } +} + /* 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; @@ -673,6 +708,7 @@ static void animateCharacter(World *world, Object *o, float dt) { } if (o->spraying) water(world, o, dt); if (Body_isDown(&o->body)) o->dirt = fminf(1.0f, o->dirt + 0.003f * dt); /* lying in the dirt */ + smoke(world, o, frame.facing, dt); } /* ---------- What people carry bumps into things ---------- */