Nimbin[12]?Games / Entropie / entropie/src/game/game_dev.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_dev.c 13 KB · 256 lines raw
/*
 * Dev menu (F1): pick anybody (you too) and change what they are, how they
 * feel and who they are. Up / Down: the line (the list scrolls), Left / Right:
 * change it.
 */
#include <math.h>
#include <stdio.h>
#include <string.h>

#include "game/game_internal.h"
#include "world/injury.h"
#include "world/weather.h"

typedef enum {
    DEV_WHO, DEV_TELEPORT, DEV_TIME, DEV_SPEED, DEV_WEATHER, DEV_WIND, DEV_START, DEV_FESTIVAL, DEV_BIGTREE, DEV_FEMALE, DEV_URINELLA, DEV_SPORTY, DEV_HURT, DEV_DRUNK, DEV_ENERGY,
    DEV_BLADDER, DEV_HUNGER, DEV_THIRST, DEV_SLEEPY, DEV_STRENGTH, DEV_ANNOYED,
    DEV_PRIDE, DEV_LIVELY, DEV_NERVES, DEV_CLUMSY, DEV_WITS, DEV_HEIGHTS, DEV_WILLING, DEV_COMMIT, DEV_LIKE, DEV_LOVE,
    DEV_LIKES,                       /* likes for each job ... */
    DEV_SKILLS = DEV_LIKES + 4,      /* ... skill at each job */
    DEV_ROWS = DEV_SKILLS + 4
} DevRow;

static const char *const JOB[4] = { "firewood", "trees", "trash", "fire" };
#define CREW_PAGES 3

/* The people (you first), and the one picked. */
static int people(const Game *game, Object **out, int max) {
    int n = 0;
    if (game->player && n < max) out[n++] = game->player;
    for (int i = 0; i < game->world.count && n < max; i++) {
        Object *o = game->world.objects[i];
        if (o->is_character && o->type == OBJ_HUMAN) out[n++] = o;
    }
    return n;
}

static Object *picked(const Game *game) {
    Object *list[32];
    int n = people(game, list, 32);
    if (n == 0) return NULL;
    int k = ((game->dev_who % n) + n) % n;
    return list[k];
}

static float step01(float v, int dir, float step) {
    return fminf(1.0f, fmaxf(0.0f, v + (float)dir * step));
}

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);
    float jwas = 1.0f + 0.15f * (o->fitness - 0.5f), jnow = 1.0f + 0.15f * (fit - 0.5f);
    o->top_speed *= now / was;
    o->jump_speed *= jnow / jwas;
    o->fitness = fit;
}

void game_dev(Game *game, const Input *input) {
    if (input->crew_menu) { game->show_crew = !game->show_crew; if (game->show_crew) game->show_dev = false; }
    if (game->show_crew && input->page) { game->crew_page = (game->crew_page + input->page + CREW_PAGES) % CREW_PAGES; return; }
    if (input->dev_menu) { game->show_dev = !game->show_dev; if (game->show_dev) game->show_crew = false; }
    if (!game->show_dev) return;
    if (input->menu_move) game->dev_row = (game->dev_row + input->menu_move + DEV_ROWS) % DEV_ROWS;
    int dir = input->page;
    if (!dir) return;
    if (game->dev_row == DEV_WHO) {
        game->dev_who += dir;
        return;
    }
    if (game->dev_row == DEV_TELEPORT) {
        teleport(game, dir);
        return;
    }
    if (game->dev_row == DEV_TIME) { /* an hour on or back */
        float day = 24.0f * 3600.0f;
        game->daycycle.seconds = fmodf(game->daycycle.seconds + (float)dir * 3600.0f + day, day);
        return;
    }
    if (game->dev_row == DEV_SPEED) {
        static const float SPEEDS[] = { 1.0f, 3.0f, 10.0f, 30.0f, 60.0f, 120.0f };
        int k = 0;
        for (int i = 0; i < 6; i++) if (fabsf(game->daycycle.time_scale - SPEEDS[i]) < fabsf(game->daycycle.time_scale - SPEEDS[k])) k = i;
        k = k + dir < 0 ? 0 : k + dir > 5 ? 5 : k + dir;
        game->daycycle.time_scale = SPEEDS[k];
        return;
    }
    if (game->dev_row == DEV_WEATHER) {
        game->world.weather = (game->world.weather + dir + WEATHER_KINDS) % WEATHER_KINDS;
        if (game->world.weather == WEATHER_AUTO) game->world.rain_left = 0.0f; /* (on its own again: from now) */
        return;
    }
    if (game->dev_row == DEV_WIND) {
        game->world.wind_kind = (game->world.wind_kind + dir + WIND_KINDS) % WIND_KINDS;
        if (game->world.wind_kind == WIND_AUTO) game->world.wind_left = 0.0f; /* (on its own again: from now) */
        return;
    }
    if (game->dev_row == DEV_START) {
        if (dir > 0) game_skipArrival(game);
        return;
    }
    if (game->dev_row == DEV_BIGTREE) {
        if (dir > 0) game_bigTreeJump(game);
        return;
    }
    if (game->dev_row == DEV_FESTIVAL) {
        if (dir > 0) game_festivalJump(game);
        return;
    }
    Object *o = picked(game);
    if (!o) return;
    Needs *n = &o->needs;
    Character *c = &o->ai.character;
    int row = game->dev_row;
    if (row >= DEV_LIKES && row < DEV_SKILLS) {
        float *like = &c->likes[ORDER_FIREWOOD + row - DEV_LIKES];
        *like = stepSigned(*like, dir, 0.1f);
        return;
    }
    if (row >= DEV_SKILLS && row < DEV_ROWS) {
        float *skill = &o->skill[ORDER_FIREWOOD + row - DEV_SKILLS];
        *skill = step01(*skill, dir, 0.1f);
        return;
    }
    switch ((DevRow)row) {
    case DEV_FEMALE:   o->female = !o->female; 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_HURT:     game_injuryHeal(game, o); 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;
    case DEV_BLADDER:  n->bladder = step01(n->bladder, dir, 0.1f); break;
    case DEV_HUNGER:   n->hunger = step01(n->hunger, dir, 0.1f); break;
    case DEV_THIRST:   n->thirst = step01(n->thirst, dir, 0.1f); break;
    case DEV_SLEEPY:   n->sleepiness = step01(n->sleepiness, dir, 0.1f); break;
    case DEV_STRENGTH: o->strength = fminf(200.0f, fmaxf(30.0f, o->strength + (float)dir * 10.0f)); break;
    case DEV_ANNOYED:  o->ai.annoyed = step01(o->ai.annoyed, dir, 0.1f); break;
    case DEV_PRIDE:    o->temperament.pride = stepSigned(o->temperament.pride, dir, 0.1f); break;
    case DEV_LIVELY:   o->temperament.energy = stepSigned(o->temperament.energy, dir, 0.1f); break;
    case DEV_NERVES:   o->temperament.nerves = step01(o->temperament.nerves, dir, 0.1f); break;
    case DEV_CLUMSY:   o->body.quirk[3] = stepSigned(o->body.quirk[3], dir, 0.2f); break; /* (clumsiness = 0.5 + 0.5 x this) */
    case DEV_WITS:     o->wits = step01(o->wits, dir, 0.05f); break;
    case DEV_HEIGHTS:  o->height_fear = step01(o->height_fear, dir, 0.05f); break;
    case DEV_WILLING:  c->willing = step01(c->willing, dir, 0.1f); break;
    case DEV_COMMIT:   o->commitment = step01(o->commitment, dir, 0.1f); break;
    case DEV_LIKE:     o->like = step01(o->like, dir, 0.1f); break;
    case DEV_LOVE:     o->love = step01(o->love, dir, 0.1f); break;
    default: break;
    }
}

/* One line of the menu. */
static void devLine(const Game *game, const Object *o, int row, char *out) {
    const Needs *n = &o->needs;
    const Character *c = &o->ai.character;
    if (row >= DEV_LIKES && row < DEV_SKILLS) {
        snprintf(out, 96, "Likes %s:  %+.1f", JOB[row - DEV_LIKES], c->likes[ORDER_FIREWOOD + row - DEV_LIKES]);
        return;
    }
    if (row >= DEV_SKILLS && row < DEV_ROWS) {
        snprintf(out, 96, "Skill %s:  %.0f%%", JOB[row - DEV_SKILLS], o->skill[ORDER_FIREWOOD + row - DEV_SKILLS] * 100.0f);
        return;
    }
    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_TIME: {
        int m = (int)(game->daycycle.seconds / 60.0f);
        snprintf(out, 96, "Time:  %02d:%02d  (an hour back / on)", (m / 60) % 24, m % 60);
        break;
    }
    case DEV_SPEED:    snprintf(out, 96, "Time runs:  %.0fx", game->daycycle.time_scale); break;
    case DEV_WEATHER:  snprintf(out, 96, "Weather:  %s  (raining %.0f%%, wet %.0f%%)", Weather_name((WeatherKind)game->world.weather),
                                game->world.rain * 100.0f, game->world.wet * 100.0f); break;
    case DEV_WIND:     snprintf(out, 96, "Wind:  %s  (%.0f m/s)", Weather_windName((WindKind)game->world.wind_kind), Weather_windSpeed(&game->world)); break;
    case DEV_BIGTREE:  snprintf(out, 96, "The big tree:  %s  (on ->)", game_bigTreeStatus(game)); break;
    case DEV_FESTIVAL: snprintf(out, 96, "The festival:  %s", game_festivalJumpText(game)); break;
    case DEV_START:    snprintf(out, 96, "The start:  %s", game->arrival.task == TASK_NONE && game->arrival.stage != ARRIVAL_DRIVING ? "done" : "skip it ->"); 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;
    case DEV_HURT: {
        char what[56];
        if (o->injured) snprintf(what, sizeof what, "a broken %s (lying there)", o->injured == INJ_ARM ? "arm" : "leg");
        else if (o->hospital_until > 0.0f) snprintf(what, sizeof what, "in the hospital");
        else if (o->look.cast) snprintf(what, sizeof what, "%s in a cast", Injury_castKind(o) == INJ_ARM ? "arm" : "leg");
        else snprintf(what, sizeof what, "%s", o->sprain > 0.0f || o->scrapes ? "" : "nothing");
        size_t len = strlen(what);
        if (o->sprain > 0.0f) snprintf(what + len, sizeof what - len, "%sa twisted ankle (%.0f h)", len ? ", " : "", (double)(o->sprain / 3600.0f));
        len = strlen(what);
        if (o->scrapes) snprintf(what + len, sizeof what - len, "%s%d scrapes", len ? ", " : "", o->scrapes);
        snprintf(out, 96, "Hurt:  %s%s", what, strcmp(what, "nothing") ? "  (<- / ->: heal)" : "");
        break;
    }
    case DEV_DRUNK:    snprintf(out, 96, "Drunk:  %.1f", o->drunk); break;
    case DEV_ENERGY:   snprintf(out, 96, "Energy:  %.0f%%", n->stamina * 100.0f); break;
    case DEV_BLADDER:  snprintf(out, 96, "Bladder:  %.0f%%", n->bladder * 100.0f); break;
    case DEV_HUNGER:   snprintf(out, 96, "Hunger:  %.0f%%", n->hunger * 100.0f); break;
    case DEV_THIRST:   snprintf(out, 96, "Thirst:  %.0f%%", n->thirst * 100.0f); break;
    case DEV_SLEEPY:   snprintf(out, 96, "Sleepy:  %.0f%%", n->sleepiness * 100.0f); break;
    case DEV_STRENGTH: snprintf(out, 96, "Strength:  %.0f", o->strength); break;
    case DEV_ANNOYED:  snprintf(out, 96, "Annoyed:  %.0f%%", o->ai.annoyed * 100.0f); break;
    case DEV_PRIDE:    snprintf(out, 96, "Pride (shy .. proud):  %+.1f", o->temperament.pride); break;
    case DEV_LIVELY:   snprintf(out, 96, "Lively (tired .. lively):  %+.1f", o->temperament.energy); break;
    case DEV_NERVES:   snprintf(out, 96, "Nerves (calm .. restless):  %.1f", o->temperament.nerves); break;
    case DEV_CLUMSY:   snprintf(out, 96, "Clumsy:  %.0f%%", (0.5f + 0.5f * o->body.quirk[3]) * 100.0f); break;
    case DEV_WITS:     snprintf(out, 96, "Wits:  %.2f", o->wits); break;
    case DEV_HEIGHTS:  snprintf(out, 96, "Afraid of heights:  %.2f", o->height_fear); break;
    case DEV_WILLING:  snprintf(out, 96, "Willing:  %.0f%%", c->willing * 100.0f); break;
    case DEV_COMMIT:   snprintf(out, 96, "Into the festival (commitment):  %.0f%%", (double)(o->commitment * 100.0f)); break;
    case DEV_LIKE:     snprintf(out, 96, "Likes you:  %.0f%%   (the place's awareness %.0f%%)", (double)(o->like * 100.0f), (double)(game->world.aware * 100.0f)); break;
    case DEV_LOVE:     snprintf(out, 96, "Drawn to you (love):  %.0f%%", (double)(o->love * 100.0f)); break;
    default:           out[0] = '\0'; break;
    }
}

int Game_devLines(const Game *game, char lines[][96], int max, int *chosen) {
    Object *o = picked(game);
    if (!o || max < 2) return 0;
    int shown = max - 1; /* (a window onto the list, around the line picked) */
    int first = game->dev_row - shown / 2;
    if (first > DEV_ROWS - shown) first = DEV_ROWS - shown;
    if (first < 0) first = 0;
    snprintf(lines[0], 96, "Dev  (F1 closes;  Up / Down: line %d/%d,  Left / Right: change)", game->dev_row + 1, DEV_ROWS);
    int n = 1;
    for (int row = first; row < DEV_ROWS && n < max; row++) {
        devLine(game, o, row, lines[n]);
        if (row == game->dev_row) *chosen = n;
        n++;
    }
    return n;
}