Nimbin[12]?Games / Entropie / entropie/src/game/game_talk.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

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.gz
entropie/src/game/game_talk.c 9.6 KB · 220 lines raw
/*
 * Talking and calling out: X talks to whoever is right in front, or opens the
 * shout menu (everybody, someone by name, the dog). A shout carries so far,
 * less over a running chainsaw; whoever hears it may come (see Ai_call).
 * G / H: helpers, R: a quick call to the one you look at.
 */

#include "game/game_internal.h"

#define HELP_CALL_RANGE 15.0f
#define TALK_REACH      2.2f   /* someone this close in front: X talks to them */
#define SHOUT_RANGE     35.0f  /* a shout carries this far ... */
#define NOISE_RANGE     6.0f   /* ... not over a chainsaw this close to the listener */
#define SHOUT_NAMES     3      /* people the menu offers to call by name */

/* G: the nearest person who isn't helping yet comes to help (press again for more). */
void game_callHelp(Game *game) {
    Object *nearest = NULL;
    float best = HELP_CALL_RANGE;
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (o->type != OBJ_HUMAN || Ai_isHelping(o) || Body_isDown(&o->body) || o->lives_here) continue; /* (the owners don't help: your festival) */
        float d = vec3_length(vec3_sub(o->position, game->player->position));
        if (d < best) {
            best = d;
            nearest = o;
        }
    }
    if (nearest) Ai_help(nearest, game->player);
}

/* Someone right in front of the player, close enough to talk to. */
Object *game_personInFront(const Game *game) {
    const Object *p = game->player;
    Vec3 facing = Object_facing(p);
    Object *best = NULL;
    float best_dist = TALK_REACH;
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (o == p || o->type != OBJ_HUMAN || Body_isDown(&o->body)) continue;
        Vec3 to = vec3_sub(o->position, p->position);
        to.y = 0.0f;
        float d = vec3_length(to);
        if (d < best_dist && d > 1e-3f && vec3_dot(vec3_scale(to, 1.0f / d), facing) > 0.75f) {
            best_dist = d;
            best = o;
        }
    }
    return best;
}

/* Does a shout from the player reach them? Not asleep, not too far; a chainsaw
   running near them drowns it out; drink makes them hard of hearing. */
static bool hears(const Game *game, const Object *o) {
    if (o->body.asleep || o->toilet) return false;
    float range = SHOUT_RANGE * (1.0f - 0.3f * fminf(o->drunk, 1.0f));
    for (int i = 0; i < game->world.count; i++) {
        const Object *w = game->world.objects[i];
        if (w->is_character && Tools_isWorking(w) && w->work_state.tool == TOOL_CHAINSAW &&
            vec3_length(vec3_sub(w->position, o->position)) < NOISE_RANGE) range *= 0.3f;
    }
    return vec3_length(vec3_sub(o->position, game->player->position)) < range;
}

/* Calls one of them over: true if they heard it (they come, or say why not). */
static bool callOne(Game *game, Object *o) {
    if (o->indoors > 0.0f) { /* in the house: out they come */
        o->indoors = fminf(o->indoors, 3.0f);
        Ai_say(o, "Coming!");
    }
    if (!hears(game, o)) return false;
    if (o->type == OBJ_DOG) Dog_call(o, game->player);
    else if (!Ai_call(o, game->player)) Ai_say(o, "Not now!");
    return true;
}

/* R: calls the one you look at (or the nearest) over, if they can hear it. */
void game_callOver(Game *game) {
    Object *p = game->player;
    Vec3 look = vec3_normalize(vec3(game->camera.front.x, 0.0f, game->camera.front.z));
    Object *best = NULL;
    float best_score = -1e9f;
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (o == p || (o->type != OBJ_HUMAN && o->type != OBJ_DOG)) continue;
        Vec3 to = vec3_sub(o->position, p->position);
        to.y = 0.0f;
        float d = vec3_length(to);
        if (d > SHOUT_RANGE || d < 1e-3f) continue;
        float score = vec3_dot(vec3_scale(to, 1.0f / d), look) * 2.0f - d / SHOUT_RANGE;
        if (score > best_score) {
            best_score = score;
            best = o;
        }
    }
    static char line[64];
    if (!best) { say(game, "Hello? ...nobody hears you"); return; }
    snprintf(line, sizeof(line), "Hey, %s!", best->name);
    say(game, line);
    if (!callOne(game, best)) tellMore(game, "no answer");
}

/* ---------- The shout menu ---------- */

/* Fills the menu: everybody, the nearest few by name, the dog. */
static void openShout(Game *game) {
    const Object *p = game->player;
    game->shout_count = 0;
    game->shout_dog = NULL;
    for (int i = 0; i < game->world.count; i++) {
        Object *o = game->world.objects[i];
        if (o->type == OBJ_DOG && !game->shout_dog) game->shout_dog = o;
        if (o->type != OBJ_HUMAN) continue;
        /* keep the nearest SHOUT_NAMES, nearest first */
        float d = vec3_length(vec3_sub(o->position, p->position));
        int at = game->shout_count;
        while (at > 0 && vec3_length(vec3_sub(game->shout_to[at - 1]->position, p->position)) > d) at--;
        if (at >= SHOUT_NAMES) continue;
        int last = game->shout_count < SHOUT_NAMES ? game->shout_count++ : SHOUT_NAMES - 1;
        for (int k = last; k > at; k--) game->shout_to[k] = game->shout_to[k - 1];
        game->shout_to[at] = o;
    }
    game->shouting = true;
}

static void shout(Game *game, int choice) {
    static char line[64];
    game->shouting = false;
    int heard = 0;
    bool storm = game->world.fest.stage == FEST_WARNED || game->world.fest.stage == FEST_STORM;
    if (storm && choice == 1) { /* (the storm coming: that first) */
        heard = game_shoutStorm(game);
        if (!heard) tellMore(game, "nobody near who doesn't know already");
        return;
    }
    if (storm) choice--;
    int shown = game->shout_count < TALK_CHOICES - 1 - (storm ? 2 : 1) ? game->shout_count : TALK_CHOICES - 1 - (storm ? 2 : 1); /* (names on the menu) */
    if (choice == 1) {
        say(game, "Hey, everybody! Over here!");
        for (int i = 0; i < game->world.count; i++) { /* (your lot: not Kurti and his sons - call them by name) */
            Object *o = game->world.objects[i];
            if (o->type == OBJ_HUMAN && !o->lives_here && !o->guest && callOne(game, o)) heard++; /* (the crew: not the guests) */
        }
    } else if (choice - 2 < shown) {
        Object *o = game->shout_to[choice - 2];
        snprintf(line, sizeof(line), "%s! Come here!", o->name);
        say(game, line);
        heard = callOne(game, o);
    } else if (game->shout_dog) {
        snprintf(line, sizeof(line), "%s! Here, girl!", game->shout_dog->name);
        say(game, line);
        heard = callOne(game, game->shout_dog);
    }
    if (!heard) tellMore(game, "...nobody seems to hear you");
}

int Game_choices(const Game *game, const char *out[TALK_CHOICES]) {
    int meeting = Game_meetingChoices(game, out); /* (a question at a meeting: that first) */
    if (meeting) return meeting;
    if (!game->shouting && !Talk_active(&game->talk)) { int touch = game_touchChoices(game, out); if (touch) return touch; } /* (holding somebody) */
    if (!game->shouting) return Talk_choices(&game->talk, out);
    Game *g = (Game *)game; /* (the lines live in the game) */
    int n = 0;
    if (game->world.fest.stage == FEST_WARNED || game->world.fest.stage == FEST_STORM) out[n++] = "Shout: \"Storm! Everybody inside!\"";
    out[n++] = "Shout: \"Hey, everybody! Over here!\"";
    for (int k = 0; k < game->shout_count && n < TALK_CHOICES - 1; k++) {
        snprintf(g->shout_lines[k], sizeof(g->shout_lines[k]), "Call %s (%.0f m)", game->shout_to[k]->name,
                 vec3_length(vec3_sub(game->shout_to[k]->position, game->player->position)));
        out[n++] = g->shout_lines[k];
    }
    if (game->shout_dog) {
        snprintf(g->shout_lines[SHOUT_NAMES], sizeof(g->shout_lines[SHOUT_NAMES]), "Call %s", game->shout_dog->name);
        out[n++] = g->shout_lines[SHOUT_NAMES];
    }
    return n;
}

/* Conversations: X talks to whoever is in front (or opens the shout menu);
   answers with 1-5, right click, X or walking off ends them. People who
   walked up to say something get to say it. */
void game_talk(Game *game, const Input *input, float dt) {
    Talk *t = &game->talk;
    if (game->meeting.kind && game->meeting.asking) return; /* (the keys answer the meeting) */
    bool leave = input->right_click;
    if (input->talk) {
        if (game->shouting) game->shouting = false;
        else if (Talk_active(t)) leave = true;
        else if (!Body_isBusy(&game->player->body)) {
            Object *person = game_personInFront(game);
            if (person) Talk_begin(t, &game->world, game->player, person, TOPIC_NONE);
            else openShout(game);
        }
    }
    if (game->shouting) {
        if (input->right_click || Talk_active(t)) game->shouting = false;
        else if (input->choice >= 1) {
            const char *lines[TALK_CHOICES];
            if (input->choice <= Game_choices(game, lines)) shout(game, input->choice);
        }
        return;
    }
    if (!Talk_active(t) && input->choice >= 1) { /* holding somebody: what you do */
        const char *lines[TALK_CHOICES];
        if (game_touchChoices(game, lines) >= input->choice) { game_touchAnswer(game, input->choice); return; }
    }
    if (!Talk_active(t)) {
        for (int i = 0; i < game->world.count; i++) {
            Object *o = game->world.objects[i];
            if (o->type == OBJ_HUMAN && o->ai.mode == AI_APPROACH && o->ai.ready_to_talk && !Body_isBusy(&game->player->body)) {
                Talk_begin(t, &game->world, game->player, o, o->ai.topic);
                break;
            }
        }
    }
    Talk_update(t, &game->world, game->player, input->choice, leave, dt);
    if (t->errand) { /* a shopping run agreed: off they go */
        game_shopStart(game, t->errand, t->shop);
        t->errand = NULL;
    }
}