1 /**
2 * \file ui-birth.c
3 * \brief Text-based user interface for character creation
4 *
5 * Copyright (c) 1987 - 2015 Angband contributors
6 *
7 * This work is free software; you can redistribute it and/or modify it
8 * under the terms of either:
9 *
10 * a) the GNU General Public License as published by the Free Software
11 * Foundation, version 2, or
12 *
13 * b) the "Angband licence":
14 * This software may be copied and distributed for educational, research,
15 * and not for profit purposes provided that this copyright and statement
16 * are included in all such copies. Other copyrights may also apply.
17 */
18
19 #include "angband.h"
20 #include "cmds.h"
21 #include "cmd-core.h"
22 #include "game-event.h"
23 #include "game-input.h"
24 #include "obj-tval.h"
25 #include "player.h"
26 #include "player-spell.h"
27 #include "ui-birth.h"
28 #include "ui-display.h"
29 #include "ui-game.h"
30 #include "ui-help.h"
31 #include "ui-input.h"
32 #include "ui-menu.h"
33 #include "ui-options.h"
34 #include "ui-player.h"
35 #include "ui-prefs.h"
36 #include "ui-target.h"
37
38 /**
39 * Overview
40 * ========
41 * This file implements the user interface side of the birth process
42 * for the classic terminal-based UI of Angband.
43 *
44 * It models birth as a series of steps which must be carried out in
45 * a specified order, with the option of stepping backwards to revisit
46 * past choices.
47 *
48 * It starts when we receive the EVENT_ENTER_BIRTH event from the game,
49 * and ends when we receive the EVENT_LEAVE_BIRTH event. In between,
50 * we will repeatedly be asked to supply a game command, which change
51 * the state of the character being rolled. Once the player is happy
52 * with their character, we send the CMD_ACCEPT_CHARACTER command.
53 */
54
55 /**
56 * A local-to-this-file global to hold the most important bit of state
57 * between calls to the game proper. Probably not strictly necessary,
58 * but reduces complexity a bit. */
59 enum birth_stage
60 {
61 BIRTH_BACK = -1,
62 BIRTH_RESET = 0,
63 BIRTH_QUICKSTART,
64 BIRTH_RACE_CHOICE,
65 BIRTH_CLASS_CHOICE,
66 BIRTH_ROLLER_CHOICE,
67 BIRTH_POINTBASED,
68 BIRTH_ROLLER,
69 BIRTH_NAME_CHOICE,
70 BIRTH_HISTORY_CHOICE,
71 BIRTH_FINAL_CONFIRM,
72 BIRTH_COMPLETE
73 };
74
75
76 enum birth_questions
77 {
78 BQ_METHOD = 0,
79 BQ_RACE,
80 BQ_CLASS,
81 BQ_ROLLER,
82 MAX_BIRTH_QUESTIONS
83 };
84
85 enum birth_rollers
86 {
87 BR_POINTBASED = 0,
88 BR_NORMAL,
89 MAX_BIRTH_ROLLERS
90 };
91
92
93 static void point_based_start(void);
94 static bool quickstart_allowed = false;
95 bool arg_force_name;
96
97 /**
98 * ------------------------------------------------------------------------
99 * Quickstart? screen.
100 * ------------------------------------------------------------------------ */
textui_birth_quickstart(void)101 static enum birth_stage textui_birth_quickstart(void)
102 //phantom name change changes
103 {
104 const char *prompt = "['Y': use as is; 'N': redo; 'C': change name/history; '=': set birth options]";
105
106 enum birth_stage next = BIRTH_QUICKSTART;
107
108 /* Prompt for it */
109 prt("New character based on previous one:", 0, 0);
110 prt(prompt, Term->hgt - 1, Term->wid / 2 - strlen(prompt) / 2);
111
112 do {
113 /* Get a key */
114 struct keypress ke = inkey();
115
116 if (ke.code == 'N' || ke.code == 'n') {
117 cmdq_push(CMD_BIRTH_RESET);
118 next = BIRTH_RACE_CHOICE;
119 } else if (ke.code == KTRL('X')) {
120 quit(NULL);
121 } else if ( !arg_force_name && (ke.code == 'C' || ke.code == 'c')) {
122 next = BIRTH_NAME_CHOICE;
123 } else if (ke.code == '=') {
124 do_cmd_options_birth();
125 } else if (ke.code == 'Y' || ke.code == 'y') {
126 cmdq_push(CMD_ACCEPT_CHARACTER);
127 next = BIRTH_COMPLETE;
128 }
129 } while (next == BIRTH_QUICKSTART);
130
131 /* Clear prompt */
132 clear_from(23);
133
134 return next;
135 }
136
137 /**
138 * ------------------------------------------------------------------------
139 * The various "menu" bits of the birth process - namely choice of race,
140 * class, and roller type.
141 * ------------------------------------------------------------------------ */
142
143 /**
144 * The various menus
145 */
146 static struct menu race_menu, class_menu, roller_menu;
147
148 /**
149 * Locations of the menus, etc. on the screen
150 */
151 #define HEADER_ROW 1
152 #define QUESTION_ROW 7
153 #define TABLE_ROW 9
154
155 #define QUESTION_COL 2
156 #define RACE_COL 2
157 #define RACE_AUX_COL 19
158 #define CLASS_COL 19
159 #define CLASS_AUX_COL 36
160 #define ROLLER_COL 36
161 #define HIST_INSTRUCT_ROW 18
162
163 #define MENU_ROWS TABLE_ROW + 14
164
165 /**
166 * upper left column and row, width, and lower column
167 */
168 static region race_region = {RACE_COL, TABLE_ROW, 17, MENU_ROWS};
169 static region class_region = {CLASS_COL, TABLE_ROW, 17, MENU_ROWS};
170 static region roller_region = {ROLLER_COL, TABLE_ROW, 34, MENU_ROWS};
171
172 /**
173 * We use different menu "browse functions" to display the help text
174 * sometimes supplied with the menu items - currently just the list
175 * of bonuses, etc, corresponding to each race and class.
176 */
177 typedef void (*browse_f) (int oid, void *db, const region *l);
178
179 /**
180 * We have one of these structures for each menu we display - it holds
181 * the useful information for the menu - text of the menu items, "help"
182 * text, current (or default) selection, and whether random selection
183 * is allowed.
184 */
185 struct birthmenu_data
186 {
187 const char **items;
188 const char *hint;
189 bool allow_random;
190 };
191
192 /**
193 * A custom "display" function for our menus that simply displays the
194 * text from our stored data in a different colour if it's currently
195 * selected.
196 */
birthmenu_display(struct menu * menu,int oid,bool cursor,int row,int col,int width)197 static void birthmenu_display(struct menu *menu, int oid, bool cursor,
198 int row, int col, int width)
199 {
200 struct birthmenu_data *data = menu->menu_data;
201
202 byte attr = curs_attrs[CURS_KNOWN][0 != cursor];
203 c_put_str(attr, data->items[oid], row, col);
204 }
205
206 /**
207 * Our custom menu iterator, only really needed to allow us to override
208 * the default handling of "commands" in the standard iterators (hence
209 * only defining the display and handler parts).
210 */
211 static const menu_iter birth_iter = { NULL, NULL, birthmenu_display, NULL, NULL };
212
skill_help(const int r_skills[],const int c_skills[],int mhp,int exp,int infra)213 static void skill_help(const int r_skills[], const int c_skills[], int mhp, int exp, int infra)
214 {
215 s16b skills[SKILL_MAX];
216 unsigned i;
217
218 for (i = 0; i < SKILL_MAX ; ++i)
219 skills[i] = (r_skills ? r_skills[i] : 0 ) + (c_skills ? c_skills[i] : 0);
220
221 text_out_e("Hit/Shoot/Throw: %+d/%+d/%+d\n", skills[SKILL_TO_HIT_MELEE],
222 skills[SKILL_TO_HIT_BOW], skills[SKILL_TO_HIT_THROW]);
223 text_out_e("Hit die: %2d XP mod: %d%%\n", mhp, exp);
224 text_out_e("Disarm: %+3d/%+3d Devices: %+3d\n", skills[SKILL_DISARM_PHYS],
225 skills[SKILL_DISARM_MAGIC], skills[SKILL_DEVICE]);
226 text_out_e("Save: %+3d Stealth: %+3d\n", skills[SKILL_SAVE],
227 skills[SKILL_STEALTH]);
228 if (infra >= 0)
229 text_out_e("Infravision: %d ft\n", infra * 10);
230 text_out_e("Digging: %+d\n", skills[SKILL_DIGGING]);
231 text_out_e("Search: %+d", skills[SKILL_SEARCH]);
232 if (infra < 0)
233 text_out_e("\n");
234 }
235
race_help(int i,void * db,const region * l)236 static void race_help(int i, void *db, const region *l)
237 {
238 int j;
239 struct player_race *r = player_id2race(i);
240 int len = (STAT_MAX + 1) / 2;
241
242 struct player_ability *ability;
243 int n_flags = 0;
244 int flag_space = 3;
245
246 if (!r) return;
247
248 /* Output to the screen */
249 text_out_hook = text_out_to_screen;
250
251 /* Indent output */
252 text_out_indent = RACE_AUX_COL;
253 Term_gotoxy(RACE_AUX_COL, TABLE_ROW);
254
255 for (j = 0; j < len; j++) {
256 const char *name = stat_names_reduced[j];
257 int adj = r->r_adj[j];
258
259 text_out_e("%s%+3d", name, adj);
260
261 if (j * 2 + 1 < STAT_MAX) {
262 name = stat_names_reduced[j + len];
263 adj = r->r_adj[j + len];
264 text_out_e(" %s%+3d", name, adj);
265 }
266
267 text_out("\n");
268 }
269
270 text_out_e("\n");
271 skill_help(r->r_skills, NULL, r->r_mhp, r->r_exp, r->infra);
272 text_out_e("\n");
273
274 for (ability = player_abilities; ability; ability = ability->next) {
275 if (n_flags >= flag_space) break;
276 if (streq(ability->type, "object") &&
277 !of_has(r->flags, ability->index)) {
278 continue;
279 } else if (streq(ability->type, "player") &&
280 !pf_has(r->pflags, ability->index)) {
281 continue;
282 } else if (streq(ability->type, "element") &&
283 (r->el_info[ability->index].res_level != ability->value)) {
284 continue;
285 }
286 text_out_e("\n%s", ability->name);
287 n_flags++;
288 }
289
290 while (n_flags < flag_space) {
291 text_out_e("\n");
292 n_flags++;
293 }
294
295 /* Reset text_out() indentation */
296 text_out_indent = 0;
297 }
298
class_help(int i,void * db,const region * l)299 static void class_help(int i, void *db, const region *l)
300 {
301 int j;
302 struct player_class *c = player_id2class(i);
303 const struct player_race *r = player->race;
304 int len = (STAT_MAX + 1) / 2;
305
306 struct player_ability *ability;
307 int n_flags = 0;
308 int flag_space = 5;
309
310 if (!c) return;
311
312 /* Output to the screen */
313 text_out_hook = text_out_to_screen;
314
315 /* Indent output */
316 text_out_indent = CLASS_AUX_COL;
317 Term_gotoxy(CLASS_AUX_COL, TABLE_ROW);
318
319 for (j = 0; j < len; j++) {
320 const char *name = stat_names_reduced[j];
321 int adj = c->c_adj[j] + r->r_adj[j];
322
323 text_out_e("%s%+3d", name, adj);
324
325 if (j*2 + 1 < STAT_MAX) {
326 name = stat_names_reduced[j + len];
327 adj = c->c_adj[j + len] + r->r_adj[j + len];
328 text_out_e(" %s%+3d", name, adj);
329 }
330
331 text_out("\n");
332 }
333
334 text_out_e("\n");
335
336 skill_help(r->r_skills, c->c_skills, r->r_mhp + c->c_mhp,
337 r->r_exp + c->c_exp, -1);
338
339 if (c->magic.total_spells) {
340 int count;
341 struct magic_realm *realm = class_magic_realms(c, &count), *realm_next;
342 char buf[120];
343
344 my_strcpy(buf, realm->name, sizeof(buf));
345 realm_next = realm->next;
346 mem_free(realm);
347 realm = realm_next;
348 if (count > 1) {
349 while (realm) {
350 count--;
351 if (count) {
352 my_strcat(buf, ", ", sizeof(buf));
353 } else {
354 my_strcat(buf, " and ", sizeof(buf));
355 }
356 my_strcat(buf, realm->name, sizeof(buf));
357 realm_next = realm->next;
358 mem_free(realm);
359 realm = realm_next;
360 }
361 }
362 text_out_e("\nLearns %s magic", buf);
363 }
364
365 for (ability = player_abilities; ability; ability = ability->next) {
366 if (n_flags >= flag_space) break;
367 if (streq(ability->type, "object") &&
368 !of_has(c->flags, ability->index)) {
369 continue;
370 } else if (streq(ability->type, "player") &&
371 !pf_has(c->pflags, ability->index)) {
372 continue;
373 } else if (streq(ability->type, "element")) {
374 continue;
375 }
376
377 text_out_e("\n%s", ability->name);
378 n_flags++;
379 }
380
381 while (n_flags < flag_space) {
382 text_out_e("\n");
383 n_flags++;
384 }
385
386 /* Reset text_out() indentation */
387 text_out_indent = 0;
388 }
389
390 /**
391 * Set up one of our menus ready to display choices for a birth question.
392 * This is slightly involved.
393 */
init_birth_menu(struct menu * menu,int n_choices,int initial_choice,const region * reg,bool allow_random,browse_f aux)394 static void init_birth_menu(struct menu *menu, int n_choices,
395 int initial_choice, const region *reg,
396 bool allow_random, browse_f aux)
397 {
398 struct birthmenu_data *menu_data;
399
400 /* Initialise a basic menu */
401 menu_init(menu, MN_SKIN_SCROLL, &birth_iter);
402
403 /* A couple of behavioural flags - we want selections letters in
404 lower case and a double tap to act as a selection. */
405 menu->selections = lower_case;
406 menu->flags = MN_DBL_TAP;
407
408 /* Copy across the game's suggested initial selection, etc. */
409 menu->cursor = initial_choice;
410
411 /* Allocate sufficient space for our own bits of menu information. */
412 menu_data = mem_alloc(sizeof *menu_data);
413
414 /* Allocate space for an array of menu item texts and help texts
415 (where applicable) */
416 menu_data->items = mem_alloc(n_choices * sizeof *menu_data->items);
417 menu_data->allow_random = allow_random;
418
419 /* Set private data */
420 menu_setpriv(menu, n_choices, menu_data);
421
422 /* Set up the "browse" hook to display help text (where applicable). */
423 menu->browse_hook = aux;
424
425 /* Lay out the menu appropriately */
426 menu_layout(menu, reg);
427 }
428
429
430
setup_menus(void)431 static void setup_menus(void)
432 {
433 int i, n;
434 struct player_class *c;
435 struct player_race *r;
436
437 const char *roller_choices[MAX_BIRTH_ROLLERS] = {
438 "Point-based",
439 "Standard roller"
440 };
441
442 struct birthmenu_data *mdata;
443
444 /* Count the races */
445 n = 0;
446 for (r = races; r; r = r->next) n++;
447
448 /* Race menu. */
449 init_birth_menu(&race_menu, n, player->race ? player->race->ridx : 0,
450 &race_region, true, race_help);
451 mdata = race_menu.menu_data;
452
453 for (i = 0, r = races; r; r = r->next, i++)
454 mdata->items[r->ridx] = r->name;
455 mdata->hint = "Race affects stats and skills, and may confer resistances and abilities.";
456
457 /* Count the classes */
458 n = 0;
459 for (c = classes; c; c = c->next) n++;
460
461 /* Class menu similar to race. */
462 init_birth_menu(&class_menu, n, player->class ? player->class->cidx : 0,
463 &class_region, true, class_help);
464 mdata = class_menu.menu_data;
465
466 for (i = 0, c = classes; c; c = c->next, i++)
467 mdata->items[c->cidx] = c->name;
468 mdata->hint = "Class affects stats, skills, and other character traits.";
469
470 /* Roller menu straightforward */
471 init_birth_menu(&roller_menu, MAX_BIRTH_ROLLERS, 0, &roller_region, false,
472 NULL);
473 mdata = roller_menu.menu_data;
474 for (i = 0; i < MAX_BIRTH_ROLLERS; i++)
475 mdata->items[i] = roller_choices[i];
476 mdata->hint = "Choose how to generate your intrinsic stats. Point-based is recommended.";
477 }
478
479 /**
480 * Cleans up our stored menu info when we've finished with it.
481 */
free_birth_menu(struct menu * menu)482 static void free_birth_menu(struct menu *menu)
483 {
484 struct birthmenu_data *data = menu->menu_data;
485
486 if (data) {
487 mem_free(data->items);
488 mem_free(data);
489 }
490 }
491
free_birth_menus(void)492 static void free_birth_menus(void)
493 {
494 /* We don't need these any more. */
495 free_birth_menu(&race_menu);
496 free_birth_menu(&class_menu);
497 free_birth_menu(&roller_menu);
498 }
499
500 /**
501 * Clear the previous question
502 */
clear_question(void)503 static void clear_question(void)
504 {
505 int i;
506
507 for (i = QUESTION_ROW; i < TABLE_ROW; i++)
508 /* Clear line, position cursor */
509 Term_erase(0, i, 255);
510 }
511
512
513 #define BIRTH_MENU_HELPTEXT \
514 "{light blue}Please select your character traits from the menus below:{/}\n\n" \
515 "Use the {light green}movement keys{/} to scroll the menu, " \
516 "{light green}Enter{/} to select the current menu item, '{light green}*{/}' " \
517 "for a random menu item, '{light green}ESC{/}' to step back through the " \
518 "birth process, '{light green}={/}' for the birth options, '{light green}?{/}' " \
519 "for help, or '{light green}Ctrl-X{/}' to quit."
520
521 /**
522 * Show the birth instructions on an otherwise blank screen
523 */
print_menu_instructions(void)524 static void print_menu_instructions(void)
525 {
526 /* Clear screen */
527 Term_clear();
528
529 /* Output to the screen */
530 text_out_hook = text_out_to_screen;
531
532 /* Indent output */
533 text_out_indent = QUESTION_COL;
534 Term_gotoxy(QUESTION_COL, HEADER_ROW);
535
536 /* Display some helpful information */
537 text_out_e(BIRTH_MENU_HELPTEXT);
538
539 /* Reset text_out() indentation */
540 text_out_indent = 0;
541 }
542
543 /**
544 * Allow the user to select from the current menu, and return the
545 * corresponding command to the game. Some actions are handled entirely
546 * by the UI (displaying help text, for instance).
547 */
menu_question(enum birth_stage current,struct menu * current_menu,cmd_code choice_command)548 static enum birth_stage menu_question(enum birth_stage current,
549 struct menu *current_menu,
550 cmd_code choice_command)
551 {
552 struct birthmenu_data *menu_data = menu_priv(current_menu);
553 ui_event cx;
554
555 enum birth_stage next = BIRTH_RESET;
556
557 /* Print the question currently being asked. */
558 clear_question();
559 Term_putstr(QUESTION_COL, QUESTION_ROW, -1, COLOUR_YELLOW, menu_data->hint);
560
561 current_menu->cmd_keys = "?=*\x18"; /* ?, =, *, <ctl-X> */
562
563 while (next == BIRTH_RESET) {
564 /* Display the menu, wait for a selection of some sort to be made. */
565 cx = menu_select(current_menu, EVT_KBRD, false);
566
567 /* As all the menus are displayed in "hierarchical" style, we allow
568 use of "back" (left arrow key or equivalent) to step back in
569 the proces as well as "escape". */
570 if (cx.type == EVT_ESCAPE) {
571 next = BIRTH_BACK;
572 } else if (cx.type == EVT_SELECT) {
573 if (current == BIRTH_ROLLER_CHOICE) {
574 if (current_menu->cursor) {
575 /* Do a first roll of the stats */
576 cmdq_push(CMD_ROLL_STATS);
577 next = current + 2;
578 } else {
579 /*
580 * Make sure we've got a point-based char to play with.
581 * We call point_based_start here to make sure we get
582 * an update on the points totals before trying to
583 * display the screen. The call to CMD_RESET_STATS
584 * forces a rebuying of the stats to give us up-to-date
585 * totals. This is, it should go without saying, a hack.
586 */
587 point_based_start();
588 cmdq_push(CMD_RESET_STATS);
589 cmd_set_arg_choice(cmdq_peek(), "choice", true);
590 next = current + 1;
591 }
592 } else {
593 cmdq_push(choice_command);
594 cmd_set_arg_choice(cmdq_peek(), "choice", current_menu->cursor);
595 next = current + 1;
596 }
597 } else if (cx.type == EVT_KBRD) {
598 /* '*' chooses an option at random from those the game's provided */
599 if (cx.key.code == '*' && menu_data->allow_random) {
600 current_menu->cursor = randint0(current_menu->count);
601 cmdq_push(choice_command);
602 cmd_set_arg_choice(cmdq_peek(), "choice", current_menu->cursor);
603
604 menu_refresh(current_menu, false);
605 next = current + 1;
606 } else if (cx.key.code == '=') {
607 do_cmd_options_birth();
608 next = current;
609 } else if (cx.key.code == KTRL('X')) {
610 quit(NULL);
611 } else if (cx.key.code == '?') {
612 do_cmd_help();
613 }
614 }
615 }
616
617 return next;
618 }
619
620 /**
621 * ------------------------------------------------------------------------
622 * The rolling bit of the roller.
623 * ------------------------------------------------------------------------ */
roller_command(bool first_call)624 static enum birth_stage roller_command(bool first_call)
625 {
626 char prompt[80] = "";
627 size_t promptlen = 0;
628
629 struct keypress ch;
630
631 enum birth_stage next = BIRTH_ROLLER;
632
633 /* Used to keep track of whether we've rolled a character before or not. */
634 static bool prev_roll = false;
635
636 /* Display the player - a bit cheaty, but never mind. */
637 display_player(0);
638
639 if (first_call)
640 prev_roll = false;
641
642 /* Prepare a prompt (must squeeze everything in) */
643 strnfcat(prompt, sizeof (prompt), &promptlen, "['r' to reroll");
644 if (prev_roll)
645 strnfcat(prompt, sizeof(prompt), &promptlen, ", 'p' for previous roll");
646 strnfcat(prompt, sizeof (prompt), &promptlen, " or 'Enter' to accept]");
647
648 /* Prompt for it */
649 prt(prompt, Term->hgt - 1, Term->wid / 2 - promptlen / 2);
650
651 /* Prompt and get a command */
652 ch = inkey();
653
654 /* Analyse the command */
655 if (ch.code == ESCAPE) {
656 /* Back out */
657 next = BIRTH_BACK;
658 } else if (ch.code == KC_ENTER) {
659 /* 'Enter' accepts the roll */
660 next = BIRTH_NAME_CHOICE;
661 } else if ((ch.code == ' ') || (ch.code == 'r')) {
662 /* Reroll this character */
663 cmdq_push(CMD_ROLL_STATS);
664 prev_roll = true;
665 } else if (prev_roll && (ch.code == 'p')) {
666 /* Previous character */
667 cmdq_push(CMD_PREV_STATS);
668 } else if (ch.code == KTRL('X')) {
669 /* Quit */
670 quit(NULL);
671 } else if (ch.code == '?') {
672 /* Help XXX */
673 do_cmd_help();
674 } else {
675 /* Nothing handled directly here */
676 bell("Illegal roller command!");
677 }
678
679 return next;
680 }
681
682 /**
683 * ------------------------------------------------------------------------
684 * Point-based stat allocation.
685 * ------------------------------------------------------------------------ */
686
687 /* The locations of the "costs" area on the birth screen. */
688 #define COSTS_ROW 2
689 #define COSTS_COL (42 + 32)
690 #define TOTAL_COL (42 + 19)
691
692 /**
693 * This is called whenever a stat changes. We take the easy road, and just
694 * redisplay them all using the standard function.
695 */
point_based_stats(game_event_type type,game_event_data * data,void * user)696 static void point_based_stats(game_event_type type, game_event_data *data,
697 void *user)
698 {
699 display_player_stat_info();
700 }
701
702 /**
703 * This is called whenever any of the other miscellaneous stat-dependent things
704 * changed. We are hooked into changes in the amount of gold in this case,
705 * but redisplay everything because it's easier.
706 */
point_based_misc(game_event_type type,game_event_data * data,void * user)707 static void point_based_misc(game_event_type type, game_event_data *data,
708 void *user)
709 {
710 display_player_xtra_info();
711 }
712
713
714 /**
715 * This is called whenever the points totals are changed (in birth.c), so
716 * that we can update our display of how many points have been spent and
717 * are available.
718 */
point_based_points(game_event_type type,game_event_data * data,void * user)719 static void point_based_points(game_event_type type, game_event_data *data,
720 void *user)
721 {
722 int i;
723 int sum = 0;
724 int *stats = data->birthstats.stats;
725
726 /* Display the costs header */
727 put_str("Cost", COSTS_ROW - 1, COSTS_COL);
728
729 /* Display the costs */
730 for (i = 0; i < STAT_MAX; i++) {
731 /* Display cost */
732 put_str(format("%4d", stats[i]), COSTS_ROW + i, COSTS_COL);
733 sum += stats[i];
734 }
735
736 put_str(format("Total Cost: %2d/%2d", sum,
737 data->birthstats.remaining + sum), COSTS_ROW + STAT_MAX,
738 TOTAL_COL);
739 }
740
point_based_start(void)741 static void point_based_start(void)
742 {
743 const char *prompt = "[up/down to move, left/right to modify, 'r' to reset, 'Enter' to accept]";
744
745 /* Clear */
746 Term_clear();
747
748 /* Display the player */
749 display_player_xtra_info();
750 display_player_stat_info();
751
752 prt(prompt, Term->hgt - 1, Term->wid / 2 - strlen(prompt) / 2);
753
754 /* Register handlers for various events - cheat a bit because we redraw
755 the lot at once rather than each bit at a time. */
756 event_add_handler(EVENT_BIRTHPOINTS, point_based_points, NULL);
757 event_add_handler(EVENT_STATS, point_based_stats, NULL);
758 event_add_handler(EVENT_GOLD, point_based_misc, NULL);
759 }
760
point_based_stop(void)761 static void point_based_stop(void)
762 {
763 event_remove_handler(EVENT_BIRTHPOINTS, point_based_points, NULL);
764 event_remove_handler(EVENT_STATS, point_based_stats, NULL);
765 event_remove_handler(EVENT_GOLD, point_based_misc, NULL);
766 }
767
point_based_command(void)768 static enum birth_stage point_based_command(void)
769 {
770 static int stat = 0;
771 struct keypress ch;
772 enum birth_stage next = BIRTH_POINTBASED;
773
774 /* Place cursor just after cost of current stat */
775 Term_gotoxy(COSTS_COL + 4, COSTS_ROW + stat);
776
777 /* Get key */
778 ch = inkey();
779
780 if (ch.code == KTRL('X')) {
781 quit(NULL);
782 } else if (ch.code == ESCAPE) {
783 /* Go back a step, or back to the start of this step */
784 next = BIRTH_BACK;
785 } else if (ch.code == 'r' || ch.code == 'R') {
786 cmdq_push(CMD_RESET_STATS);
787 cmd_set_arg_choice(cmdq_peek(), "choice", false);
788 } else if (ch.code == KC_ENTER) {
789 /* Done */
790 next = BIRTH_NAME_CHOICE;
791 } else {
792 int dir = target_dir(ch);
793
794 /* Prev stat, looping round to the bottom when going off the top */
795 if (dir == 8)
796 stat = (stat + STAT_MAX - 1) % STAT_MAX;
797
798 /* Next stat, looping round to the top when going off the bottom */
799 if (dir == 2)
800 stat = (stat + 1) % STAT_MAX;
801
802 /* Decrease stat (if possible) */
803 if (dir == 4) {
804 cmdq_push(CMD_SELL_STAT);
805 cmd_set_arg_choice(cmdq_peek(), "choice", stat);
806 }
807
808 /* Increase stat (if possible) */
809 if (dir == 6) {
810 cmdq_push(CMD_BUY_STAT);
811 cmd_set_arg_choice(cmdq_peek(), "choice", stat);
812 }
813 }
814
815 return next;
816 }
817
818 /**
819 * ------------------------------------------------------------------------
820 * Asking for the player's chosen name.
821 * ------------------------------------------------------------------------ */
822 //phantom changes for server
get_name_command(void)823 static enum birth_stage get_name_command(void)
824 {
825 enum birth_stage next;
826 char name[PLAYER_NAME_LEN];
827
828 /* Use frontend-provided savefile name if requested */
829 if (arg_name[0]) {
830 my_strcpy(player->full_name, arg_name, sizeof(player->full_name));
831 }
832
833 if (arg_force_name) {
834 next = BIRTH_HISTORY_CHOICE;
835 } else if (get_character_name(name, sizeof(name))) {
836 cmdq_push(CMD_NAME_CHOICE);
837 cmd_set_arg_string(cmdq_peek(), "name", name);
838 next = BIRTH_HISTORY_CHOICE;
839 } else {
840 next = BIRTH_BACK;
841 }
842
843
844 return next;
845 }
846
get_screen_loc(size_t cursor,int * x,int * y,size_t n_lines,size_t * line_starts,size_t * line_lengths)847 void get_screen_loc(size_t cursor, int *x, int *y, size_t n_lines, size_t *line_starts, size_t *line_lengths)
848 {
849 size_t lengths_so_far = 0;
850 size_t i;
851
852 if (!line_starts || !line_lengths) return;
853
854 for (i = 0; i < n_lines; i++) {
855 if (cursor >= line_starts[i]) {
856 if (cursor <= (line_starts[i] + line_lengths[i])) {
857 *y = i;
858 *x = cursor - lengths_so_far;
859 break;
860 }
861 }
862 /* +1 for the space */
863 lengths_so_far += line_lengths[i] + 1;
864 }
865 }
866
edit_text(char * buffer,int buflen)867 int edit_text(char *buffer, int buflen) {
868 int len = strlen(buffer);
869 bool done = false;
870 int cursor = 0;
871
872 while (!done) {
873 int x = 0, y = 0;
874 struct keypress ke;
875
876 region area = { 1, HIST_INSTRUCT_ROW + 1, 71, 5 };
877 textblock *tb = textblock_new();
878
879 size_t *line_starts = NULL, *line_lengths = NULL;
880 size_t n_lines;
881 /*
882 * This is the total number of UTF-8 characters; can be less
883 * less than len, the number of 8-bit units in the buffer,
884 * if a single character is encoded with more than one 8-bit
885 * unit.
886 */
887 int ulen;
888
889 /* Display on screen */
890 clear_from(HIST_INSTRUCT_ROW);
891 textblock_append(tb, "%s", buffer);
892 textui_textblock_place(tb, area, NULL);
893
894 n_lines = textblock_calculate_lines(tb,
895 &line_starts, &line_lengths, area.width);
896 ulen = (n_lines > 0) ? line_starts[n_lines - 1] +
897 line_lengths[n_lines - 1]: 0;
898
899 /* Set cursor to current editing position */
900 get_screen_loc(cursor, &x, &y, n_lines, line_starts, line_lengths);
901 Term_gotoxy(1 + x, 19 + y);
902
903 ke = inkey();
904 switch (ke.code) {
905 case ESCAPE:
906 return -1;
907
908 case KC_ENTER:
909 done = true;
910 break;
911
912 case ARROW_LEFT:
913 if (cursor > 0) cursor--;
914 break;
915
916 case ARROW_RIGHT:
917 if (cursor < ulen) cursor++;
918 break;
919
920 case ARROW_DOWN: {
921 int add = line_lengths[y] + 1;
922 if (cursor + add < ulen) cursor += add;
923 break;
924 }
925
926 case ARROW_UP:
927 if (y > 0) {
928 int up = line_lengths[y - 1] + 1;
929 if (cursor - up >= 0) cursor -= up;
930 }
931 break;
932
933 case KC_END:
934 cursor = MAX(0, ulen);
935 break;
936
937 case KC_HOME:
938 cursor = 0;
939 break;
940
941 case KC_BACKSPACE:
942 case KC_DELETE: {
943 char *ocurs, *oshift;
944
945 /* Refuse to backspace into oblivion */
946 if ((ke.code == KC_BACKSPACE && cursor == 0) ||
947 (ke.code == KC_DELETE && cursor >= ulen))
948 break;
949
950 /*
951 * Move the string from k to nul along to the
952 * left by 1. First, have to get offset
953 * corresponding to the cursor position.
954 */
955 ocurs = utf8_fskip(buffer, cursor, NULL);
956 assert(ocurs);
957 if (ke.code == KC_BACKSPACE) {
958 /* Get offset of the previous character. */
959 oshift = utf8_rskip(ocurs, 1, buffer);
960 assert(oshift);
961 memmove(oshift, ocurs,
962 len - (ocurs - buffer));
963 /* Decrement */
964 --cursor;
965 len -= ocurs - oshift;
966 } else {
967 /* Get offset of the next character. */
968 oshift = utf8_fskip(ocurs, 1, NULL);
969 assert(oshift);
970 memmove(ocurs, oshift,
971 len - (oshift - buffer));
972 /* Decrement. */
973 len -= oshift - ocurs;
974 }
975
976 /* Terminate */
977 buffer[len] = '\0';
978
979 break;
980 }
981
982 default: {
983 bool atnull = (cursor == ulen);
984 char encoded[5];
985 int n_enc;
986 char *ocurs;
987
988 if (!keycode_isprint(ke.code))
989 break;
990
991 n_enc = utf32_to_utf8(encoded,
992 N_ELEMENTS(encoded), &ke.code, 1, NULL);
993
994 /*
995 * Make sure we have something to add and have
996 * enough space.
997 */
998 if (n_enc == 0 || n_enc + len >= buflen) {
999 break;
1000 }
1001
1002 /* Insert the encoded character. */
1003 if (atnull) {
1004 ocurs = buffer + len;
1005 } else {
1006 ocurs = utf8_fskip(buffer, cursor, NULL);
1007 assert(ocurs);
1008 /*
1009 * Move the rest of the buffer along
1010 * to make room.
1011 */
1012 memmove(ocurs + n_enc, ocurs,
1013 len - (ocurs - buffer));
1014 }
1015 memcpy(ocurs, encoded, n_enc);
1016
1017 /* Update cursor position and length. */
1018 ++cursor;
1019 len += n_enc;
1020
1021 /* Terminate */
1022 buffer[len] = '\0';
1023
1024 break;
1025 }
1026 }
1027
1028 mem_free(line_starts);
1029 mem_free(line_lengths);
1030 textblock_free(tb);
1031 }
1032
1033 return 0;
1034 }
1035
1036 /**
1037 * ------------------------------------------------------------------------
1038 * Allowing the player to choose their history.
1039 * ------------------------------------------------------------------------ */
get_history_command(void)1040 static enum birth_stage get_history_command(void)
1041 {
1042 enum birth_stage next = 0;
1043 struct keypress ke;
1044 char old_history[240];
1045
1046 /* Save the original history */
1047 my_strcpy(old_history, player->history, sizeof(old_history));
1048
1049 /* Ask for some history */
1050 prt("Accept character history? [y/n]", 0, 0);
1051 ke = inkey();
1052
1053 /* Quit, go back, change history, or accept */
1054 if (ke.code == KTRL('X')) {
1055 quit(NULL);
1056 } else if (ke.code == ESCAPE) {
1057 next = BIRTH_BACK;
1058 } else if (ke.code == 'N' || ke.code == 'n') {
1059 char history[240];
1060 my_strcpy(history, player->history, sizeof(history));
1061
1062 switch (edit_text(history, sizeof(history))) {
1063 case -1:
1064 next = BIRTH_BACK;
1065 break;
1066 case 0:
1067 cmdq_push(CMD_HISTORY_CHOICE);
1068 cmd_set_arg_string(cmdq_peek(), "history", history);
1069 next = BIRTH_HISTORY_CHOICE;
1070 }
1071 } else {
1072 next = BIRTH_FINAL_CONFIRM;
1073 }
1074
1075 return next;
1076 }
1077
1078 /**
1079 * ------------------------------------------------------------------------
1080 * Final confirmation of character.
1081 * ------------------------------------------------------------------------ */
get_confirm_command(void)1082 static enum birth_stage get_confirm_command(void)
1083 {
1084 const char *prompt = "['ESC' to step back, 'S' to start over, or any other key to continue]";
1085 struct keypress ke;
1086
1087 enum birth_stage next = BIRTH_RESET;
1088
1089 /* Prompt for it */
1090 prt(prompt, Term->hgt - 1, Term->wid / 2 - strlen(prompt) / 2);
1091
1092 /* Get a key */
1093 ke = inkey();
1094
1095 /* Start over */
1096 if (ke.code == 'S' || ke.code == 's') {
1097 next = BIRTH_RESET;
1098 } else if (ke.code == KTRL('X')) {
1099 quit(NULL);
1100 } else if (ke.code == ESCAPE) {
1101 next = BIRTH_BACK;
1102 } else {
1103 cmdq_push(CMD_ACCEPT_CHARACTER);
1104 next = BIRTH_COMPLETE;
1105 }
1106
1107 /* Clear prompt */
1108 clear_from(23);
1109
1110 return next;
1111 }
1112
1113
1114
1115 /**
1116 * ------------------------------------------------------------------------
1117 * Things that relate to the world outside this file: receiving game events
1118 * and being asked for game commands.
1119 * ------------------------------------------------------------------------ */
1120
1121 /**
1122 * This is called when we receive a request for a command in the birth
1123 * process.
1124
1125 * The birth process continues until we send a final character confirmation
1126 * command (or quit), so this is effectively called in a loop by the main
1127 * game.
1128 *
1129 * We're imposing a step-based system onto the main game here, so we need
1130 * to keep track of where we're up to, where each step moves on to, etc.
1131 */
textui_do_birth(void)1132 int textui_do_birth(void)
1133 {
1134 enum birth_stage current_stage = BIRTH_RESET;
1135 enum birth_stage prev = BIRTH_BACK;
1136 enum birth_stage roller = BIRTH_RESET;
1137 enum birth_stage next = current_stage;
1138
1139 bool done = false;
1140
1141 cmdq_push(CMD_BIRTH_INIT);
1142 cmdq_execute(CTX_BIRTH);
1143
1144 while (!done) {
1145
1146 switch (current_stage)
1147 {
1148 case BIRTH_RESET:
1149 {
1150 cmdq_push(CMD_BIRTH_RESET);
1151
1152 roller = BIRTH_RESET;
1153
1154 if (quickstart_allowed)
1155 next = BIRTH_QUICKSTART;
1156 else
1157 next = BIRTH_RACE_CHOICE;
1158
1159 break;
1160 }
1161
1162 case BIRTH_QUICKSTART:
1163 {
1164 display_player(0);
1165 next = textui_birth_quickstart();
1166 if (next == BIRTH_COMPLETE)
1167 done = true;
1168 break;
1169 }
1170
1171 case BIRTH_CLASS_CHOICE:
1172 case BIRTH_RACE_CHOICE:
1173 case BIRTH_ROLLER_CHOICE:
1174 {
1175 struct menu *menu = &race_menu;
1176 cmd_code command = CMD_CHOOSE_RACE;
1177
1178 Term_clear();
1179 print_menu_instructions();
1180
1181 if (current_stage > BIRTH_RACE_CHOICE) {
1182 menu_refresh(&race_menu, false);
1183 menu = &class_menu;
1184 command = CMD_CHOOSE_CLASS;
1185 }
1186
1187 if (current_stage > BIRTH_CLASS_CHOICE) {
1188 menu_refresh(&class_menu, false);
1189 menu = &roller_menu;
1190 }
1191
1192 next = menu_question(current_stage, menu, command);
1193
1194 if (next == BIRTH_BACK)
1195 next = current_stage - 1;
1196
1197 /* Make sure the character gets reset before quickstarting */
1198 if (next == BIRTH_QUICKSTART)
1199 next = BIRTH_RESET;
1200
1201 break;
1202 }
1203
1204 case BIRTH_POINTBASED:
1205 {
1206 roller = BIRTH_POINTBASED;
1207
1208 if (prev > BIRTH_POINTBASED)
1209 point_based_start();
1210
1211 next = point_based_command();
1212
1213 if (next == BIRTH_BACK)
1214 next = BIRTH_ROLLER_CHOICE;
1215
1216 if (next != BIRTH_POINTBASED)
1217 point_based_stop();
1218
1219 break;
1220 }
1221
1222 case BIRTH_ROLLER:
1223 {
1224 roller = BIRTH_ROLLER;
1225 next = roller_command(prev < BIRTH_ROLLER);
1226 if (next == BIRTH_BACK)
1227 next = BIRTH_ROLLER_CHOICE;
1228
1229 break;
1230 }
1231
1232 case BIRTH_NAME_CHOICE:
1233 {
1234 if (prev < BIRTH_NAME_CHOICE)
1235 display_player(0);
1236
1237 next = get_name_command();
1238 if (next == BIRTH_BACK)
1239 next = roller;
1240
1241 break;
1242 }
1243
1244 case BIRTH_HISTORY_CHOICE:
1245 {
1246 if (prev < BIRTH_HISTORY_CHOICE)
1247 display_player(0);
1248
1249 next = get_history_command();
1250 if (next == BIRTH_BACK)
1251 next = BIRTH_NAME_CHOICE;
1252
1253 break;
1254 }
1255
1256 case BIRTH_FINAL_CONFIRM:
1257 {
1258 if (prev < BIRTH_FINAL_CONFIRM)
1259 display_player(0);
1260
1261 next = get_confirm_command();
1262 if (next == BIRTH_BACK)
1263 next = BIRTH_HISTORY_CHOICE;
1264
1265 if (next == BIRTH_COMPLETE)
1266 done = true;
1267
1268 break;
1269 }
1270
1271 default:
1272 {
1273 /* Remove dodgy compiler warning, */
1274 }
1275 }
1276
1277 prev = current_stage;
1278 current_stage = next;
1279
1280 /* Execute whatever commands have been sent */
1281 cmdq_execute(CTX_BIRTH);
1282 }
1283
1284 return 0;
1285 }
1286
1287 /**
1288 * Called when we enter the birth mode - so we set up handlers, command hooks,
1289 * etc, here.
1290 */
ui_enter_birthscreen(game_event_type type,game_event_data * data,void * user)1291 static void ui_enter_birthscreen(game_event_type type, game_event_data *data,
1292 void *user)
1293 {
1294 /* Set the ugly static global that tells us if quickstart's available. */
1295 quickstart_allowed = data->flag;
1296
1297 setup_menus();
1298 }
1299
ui_leave_birthscreen(game_event_type type,game_event_data * data,void * user)1300 static void ui_leave_birthscreen(game_event_type type, game_event_data *data,
1301 void *user)
1302 {
1303 /* Set the savefile name if it's not already set */
1304 if (!savefile[0])
1305 savefile_set_name(player->full_name, true, true);
1306
1307 free_birth_menus();
1308 }
1309
1310
ui_init_birthstate_handlers(void)1311 void ui_init_birthstate_handlers(void)
1312 {
1313 event_add_handler(EVENT_ENTER_BIRTH, ui_enter_birthscreen, NULL);
1314 event_add_handler(EVENT_LEAVE_BIRTH, ui_leave_birthscreen, NULL);
1315 }
1316
1317