/* * 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 #include #include #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; }