1 /*
2 * Copyright (C) 2001-2013 The Exult Team
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 */
18
19 #ifdef HAVE_CONFIG_H
20 # include <config.h>
21 #endif
22
23 #include "actors.h"
24 #include "conversation.h"
25 #include "exult.h"
26 #include "game.h"
27 #include "gamewin.h"
28 #include "mouse.h"
29 #include "useval.h"
30 #include "miscinf.h"
31 #include "data/exult_bg_flx.h"
32 #include "array_size.h"
33 #include "touchui.h"
34
35 using std::size_t;
36 using std::strcpy;
37 using std::string;
38
39 //TODO: show_face & show_avatar_choices seem to share code?
40 //TODO: show_avatar_choices shouldn't first convert to char**, probably
41
42 /*
43 * Store information about an NPC's face and text on the screen during
44 * a conversation:
45 */
46 class Npc_face_info {
47 public:
48 ShapeID shape;
49 int face_num; // NPC's face shape #.
50 //int frame;
51 bool text_pending; // Text has been written, but user
52 // has not yet been prompted.
53 TileRect face_rect; // Rectangle where face is shown.
54 TileRect text_rect; // Rectangle NPC statement is shown in.
55 bool large_face; // Guardian, snake.
56 int last_text_height; // Height of last text painted.
57 string cur_text; // Current text being shown.
Npc_face_info(ShapeID & sid,int num)58 Npc_face_info(ShapeID &sid, int num)
59 : shape(sid), face_num(num),
60 text_pending(false), large_face(false)
61 { }
62 };
63
~Conversation()64 Conversation::~Conversation() {
65 delete [] conv_choices;
66 }
67
68
clear_answers()69 void Conversation::clear_answers() {
70 answers.clear();
71 }
72
add_answer(const char * str)73 void Conversation::add_answer(const char *str) {
74 remove_answer(str);
75 string s(str);
76 answers.push_back(s);
77 }
78
79 /*
80 * Add an answer to the list.
81 */
82
add_answer(Usecode_value & val)83 void Conversation::add_answer(Usecode_value &val) {
84 const char *str;
85 int size = val.get_array_size();
86 if (size) { // An array?
87 for (int i = 0; i < size; i++)
88 add_answer(val.get_elem(i));
89 } else if ((str = val.get_str_value()) != nullptr)
90 add_answer(str);
91 }
92
remove_answer(const char * str)93 void Conversation::remove_answer(const char *str) {
94 auto it = std::find(answers.cbegin(), answers.cend(), str);
95
96 if (it != answers.cend())
97 answers.erase(it);
98 }
99
100 /*
101 * Remove an answer from the list.
102 */
103
remove_answer(Usecode_value & val)104 void Conversation::remove_answer(Usecode_value &val) {
105 const char *str;
106 if (val.is_array()) {
107 int size = val.get_array_size();
108 for (int i = 0; i < size; i++) {
109 str = val.get_elem(i).get_str_value();
110 if (str) remove_answer(str);
111 }
112 } else {
113 str = val.get_str_value();
114 remove_answer(str);
115 }
116 }
117
118 /*
119 * Initialize face list.
120 */
121
init_faces()122 void Conversation::init_faces() {
123 const int max_faces = array_size(face_info);
124 for (int i = 0; i < max_faces; i++) {
125 if (face_info[i])
126 delete face_info[i];
127 face_info[i] = nullptr;
128 if (touchui != nullptr) {
129 touchui->showGameControls();
130 }
131 }
132 num_faces = 0;
133 last_face_shown = -1;
134 }
135
set_face_rect(Npc_face_info * info,Npc_face_info * prev,int screenw,int screenh)136 void Conversation::set_face_rect(
137 Npc_face_info *info,
138 Npc_face_info *prev,
139 int screenw,
140 int screenh
141 ) {
142 int text_height = sman->get_text_height(0);
143 // Figure starting y-coord.
144 // Get character's portrait.
145 Shape_frame *face = info->shape.get_shapenum() >= 0 ? info->shape.get_shape() : nullptr;
146 int face_w = 32;
147 int face_h = 32;
148 if (face) {
149 face_w = face->get_width();
150 face_h = face->get_height();
151 }
152 int startx;
153 int extraw;
154 if (face_w >= 119) {
155 startx = (screenw - face_w) / 2;
156 extraw = 0;
157 info->large_face = true;
158 } else {
159 startx = 8;
160 extraw = 4;
161 }
162 int starty;
163 int extrah;
164 if (face_h >= 142) {
165 starty = (screenh - face_h) / 2;
166 extrah = 0;
167 } else if (prev) {
168 starty = prev->text_rect.y + prev->last_text_height;
169 if (starty < prev->face_rect.y + prev->face_rect.h)
170 starty = prev->face_rect.y + prev->face_rect.h;
171 starty += 2 * text_height;
172 if (starty + face_h > screenh - 1)
173 starty = screenh - face_h - 1;
174 extrah = 4;
175 } else {
176 starty = 1;
177 extrah = 4;
178 }
179 info->face_rect = gwin->clip_to_win(TileRect(startx, starty,
180 face_w + extraw, face_h + extrah));
181 TileRect &fbox = info->face_rect;
182 // This is where NPC text will go.
183 info->text_rect = gwin->clip_to_win(TileRect(
184 fbox.x + fbox.w + 3, fbox.y + 3,
185 screenw - fbox.x - fbox.w - 6, 4 * text_height));
186 // No room? (Serpent?)
187 if (info->large_face) {
188 // Show in lower center.
189 int x = screenw / 5;
190 int y = 3 * (screenh / 4);
191 info->text_rect = TileRect(x, y,
192 screenw - (2 * x), screenh - y - 4);
193 }
194 info->last_text_height = info->text_rect.h;
195 }
196
197 /*
198 * Show a "face" on the screen. Npc_text_rect is also set.
199 * If shape < 0, an empty space is shown.
200 */
201
show_face(int shape,int frame,int slot)202 void Conversation::show_face(int shape, int frame, int slot) {
203 ShapeID face_sid(shape, frame, SF_FACES_VGA);
204
205 const int max_faces = array_size(face_info);
206
207 // Make sure mode is set right.
208 Palette *pal = gwin->get_pal(); // Watch for weirdness (lightning).
209 if (pal->get_brightness() >= 300)
210 pal->set(-1, 100);
211
212 // Get screen dims.
213 int screenw = gwin->get_width();
214 int screenh = gwin->get_height();
215 Npc_face_info *info = nullptr;
216 // See if already on screen.
217 for (int i = 0; i < max_faces; i++)
218 if (face_info[i] && face_info[i]->face_num == shape) {
219 info = face_info[i];
220 last_face_shown = i;
221 break;
222 }
223 if (!info) { // New one?
224 if (num_faces == max_faces)
225 // None free? Steal last one.
226 remove_slot_face(max_faces - 1);
227 info = new Npc_face_info(face_sid, shape);
228 if (slot == -1) // Want next one?
229 slot = num_faces;
230 // Get last one shown.
231 Npc_face_info *prev = slot ? face_info[slot - 1] : nullptr;
232 last_face_shown = slot;
233 if (!face_info[slot])
234 num_faces++; // We're adding one (not replacing).
235 else
236 delete face_info[slot];
237 face_info[slot] = info;
238 set_face_rect(info, prev, screenw, screenh);
239 }
240 gwin->get_win()->set_clip(0, 0, screenw, screenh);
241 paint_faces(); // Paint all faces.
242 if (touchui != nullptr) {
243 touchui->hideGameControls();
244 }
245 gwin->get_win()->clear_clip();
246 }
247
248 /*
249 * Change the frame of the face on given slot.
250 */
251
change_face_frame(int frame,int slot)252 void Conversation::change_face_frame(int frame, int slot) {
253 const int max_faces = array_size(face_info);
254 // Make sure mode is set right.
255 Palette *pal = gwin->get_pal(); // Watch for weirdness (lightning).
256 if (pal->get_brightness() >= 300)
257 pal->set(-1, 100);
258
259 if (slot >= max_faces || !face_info[slot])
260 return; // Invalid slot.
261
262 last_face_shown = slot;
263 Npc_face_info *info = face_info[slot];
264 // These are needed in case conversation is done.
265 if (info->shape.get_shapenum() < 0 ||
266 frame > info->shape.get_num_frames())
267 return; // Invalid frame.
268
269 if (frame == info->shape.get_framenum())
270 return; // We are done here.
271
272 info->shape.set_frame(frame);
273 // Get screen dims.
274 int screenw = gwin->get_width();
275 int screenh = gwin->get_height();
276 Npc_face_info *prev = slot ? face_info[slot - 1] : nullptr;
277 set_face_rect(info, prev, screenw, screenh);
278
279 gwin->get_win()->set_clip(0, 0, screenw, screenh);
280 paint_faces(); // Paint all faces.
281 gwin->get_win()->clear_clip();
282 }
283
284 /*
285 * Remove face from screen.
286 */
287
remove_face(int shape)288 void Conversation::remove_face(int shape) {
289 const int max_faces = array_size(face_info);
290 int i; // See if already on screen.
291 for (i = 0; i < max_faces; i++)
292 if (face_info[i] && face_info[i]->face_num == shape)
293 break;
294 if (i == max_faces)
295 return; // Not found.
296 remove_slot_face(i);
297 }
298
299 /*
300 * Remove face from indicated slot (SI).
301 */
302
remove_slot_face(int slot)303 void Conversation::remove_slot_face(
304 int slot
305 ) {
306 const int max_faces = array_size(face_info);
307 if (slot >= max_faces || !face_info[slot])
308 return; // Invalid.
309 Npc_face_info *info = face_info[slot];
310 // These are needed in case conversa-
311 // tion is done.
312 if (info->large_face)
313 gwin->set_all_dirty();
314 else {
315 gwin->add_dirty(info->face_rect);
316 gwin->add_dirty(info->text_rect);
317 }
318 delete face_info[slot];
319 face_info[slot] = nullptr;
320 num_faces--;
321 if (last_face_shown == slot) { // Just in case.
322 int j;
323 for (j = max_faces - 1; j >= 0; j--)
324 if (face_info[j])
325 break;
326 last_face_shown = j;
327 if (touchui != nullptr && num_faces == 0) {
328 touchui->showGameControls();
329 }
330 }
331 }
332
333
334 /*
335 * Show what the NPC had to say.
336 */
337
show_npc_message(const char * msg)338 void Conversation::show_npc_message(const char *msg) {
339 if (last_face_shown == -1)
340 return;
341 Npc_face_info *info = face_info[last_face_shown];
342 int font = info->large_face ? 7 : 0; // Use red for Guardian, snake.
343 info->cur_text = "";
344 TileRect &box = info->text_rect;
345 // gwin->paint(box); // Clear what was there before.
346 // paint_faces();
347 gwin->paint();
348 int height; // Break at punctuation.
349 /* NOTE: The original centers text for Guardian, snake. */
350 while ((height = sman->paint_text_box(font, msg, box.x, box.y,
351 box.w, box.h, -1, true, info->large_face,
352 gwin->get_text_bg())) < 0) {
353 // More to do?
354 info->cur_text = string(msg, -height);
355 int x;
356 int y;
357 char c;
358 gwin->paint(); // Paint scenery beneath
359 Get_click(x, y, Mouse::hand, &c, false, this, true);
360 gwin->paint();
361 msg += -height;
362 }
363 // All fit? Store height painted.
364 info->last_text_height = height;
365 info->cur_text = msg;
366 info->text_pending = true;
367 gwin->set_painted();
368 // gwin->show();
369 }
370
371
372 /*
373 * Is there NPC text that the user hasn't had a chance to read?
374 */
375
is_npc_text_pending()376 bool Conversation::is_npc_text_pending() {
377 const int max_faces = array_size(face_info);
378 for (int i = 0; i < max_faces; i++)
379 if (face_info[i] && face_info[i]->text_pending)
380 return true;
381 return false;
382 }
383
384 /*
385 * Clear text-pending flags.
386 */
387
clear_text_pending()388 void Conversation::clear_text_pending() {
389 const int max_faces = array_size(face_info);
390 for (int i = 0; i < max_faces; i++) // Clear 'pending' flags.
391 if (face_info[i])
392 face_info[i]->text_pending = false;
393 }
394
395 /*
396 * Show the Avatar's conversation choices (and face).
397 */
398
show_avatar_choices(int num_choices,char ** choices)399 void Conversation::show_avatar_choices(int num_choices, char **choices) {
400 bool SI = Game::get_game_type() == SERPENT_ISLE;
401 Main_actor *main_actor = gwin->get_main_actor();
402 const int max_faces = array_size(face_info);
403 // Get screen rectangle.
404 TileRect sbox = gwin->get_game_rect();
405 int x = 0;
406 int y = 0; // Keep track of coords. in box.
407 int height = sman->get_text_height(0);
408 int space_width = sman->get_text_width(0, " ");
409
410 // Get main actor's portrait, checking for Petra flag.
411 int shape = Shapeinfo_lookup::GetFaceReplacement(0);
412 int frame = 0;
413
414 if (shape == 0) {
415 Skin_data *skin = Shapeinfo_lookup::GetSkinInfoSafe(main_actor);
416 if (main_actor->get_flag(Obj_flags::tattooed)) {
417 shape = skin->alter_face_shape;
418 frame = skin->alter_face_frame;
419 } else {
420 shape = skin->face_shape;
421 frame = skin->face_frame;
422 }
423 }
424
425 ShapeID face_sid(shape, frame, SF_FACES_VGA);
426 Shape_frame *face = face_sid.get_shape();
427 int empty; // Find face prev. to 1st empty slot.
428 for (empty = 0; empty < max_faces; empty++)
429 if (!face_info[empty])
430 break;
431 // Get last one shown.
432 Npc_face_info *prev = empty ? face_info[empty - 1] : nullptr;
433 int fx = prev ? prev->face_rect.x + prev->face_rect.w + 4 : 16;
434 int fy;
435 if (SI) {
436 if (num_faces == max_faces)
437 // Remove face #1 if still there.
438 remove_slot_face(max_faces - 1);
439 fy = sbox.h - 2 - face->get_height();
440 fx = 8;
441 } else if (!prev)
442 fy = sbox.h - face->get_height() - 3 * height;
443 else {
444 fy = prev->text_rect.y + prev->last_text_height;
445 if (fy < prev->face_rect.y + prev->face_rect.h)
446 fy = prev->face_rect.y + prev->face_rect.h;
447 fy += height;
448 }
449 TileRect mbox(fx, fy, face->get_width(), face->get_height());
450 mbox = mbox.intersect(sbox);
451 avatar_face = mbox; // Repaint entire width.
452 // Set to where to draw sentences.
453 TileRect tbox(mbox.x + mbox.w + 8, mbox.y + 4,
454 sbox.w - mbox.x - mbox.w - 16,
455 5 * height); // Try 5 lines.
456 tbox = tbox.intersect(sbox);
457 // Draw portrait.
458 sman->paint_shape(mbox.x + face->get_xleft(),
459 mbox.y + face->get_yabove(), face);
460 delete [] conv_choices; // Set up new list of choices.
461 conv_choices = new TileRect[num_choices + 1];
462 for (int i = 0; i < num_choices; i++) {
463 char text[256];
464 text[0] = 127; // A circle.
465 strcpy(&text[1], choices[i]);
466 int width = sman->get_text_width(0, text);
467 if (x > 0 && x + width >= tbox.w) {
468 // Start a new line.
469 x = 0;
470 y += height - 1;
471 }
472 // Store info.
473 conv_choices[i] = TileRect(tbox.x + x, tbox.y + y,
474 width, height);
475 conv_choices[i] = conv_choices[i].intersect(sbox);
476 avatar_face = avatar_face.add(conv_choices[i]);
477 sman->paint_text_box(0, text, tbox.x + x, tbox.y + y,
478 width + space_width, height, 0, false, false,
479 gwin->get_text_bg());
480 x += width + space_width;
481 }
482 avatar_face.enlarge((3 * c_tilesize) / 4); // Encloses entire area.
483 avatar_face = avatar_face.intersect(sbox);
484 // Terminate the list.
485 conv_choices[num_choices] = TileRect(0, 0, 0, 0);
486 clear_text_pending();
487 gwin->set_painted();
488 }
489
show_avatar_choices()490 void Conversation::show_avatar_choices() {
491 char **result;
492 size_t i; // Blame MSVC
493
494 result = new char *[answers.size()];
495 for (i = 0; i < answers.size(); i++) {
496 result[i] = new char[answers[i].size() + 1];
497 strcpy(result[i], answers[i].c_str());
498 }
499 show_avatar_choices(answers.size(), result);
500 for (i = 0; i < answers.size(); i++) {
501 delete [] result[i];
502 }
503 delete [] result;
504 }
505
clear_avatar_choices()506 void Conversation::clear_avatar_choices() {
507 // gwin->paint(avatar_face); // Paint over face and answers.
508 gwin->add_dirty(avatar_face);
509 avatar_face.w = 0;
510 }
511
512
513 /*
514 * User clicked during a conversation.
515 *
516 * Output: Index (0-n) of choice, or -1 if not on a choice.
517 */
518
conversation_choice(int x,int y)519 int Conversation::conversation_choice(int x, int y) {
520 int i;
521 for (i = 0; conv_choices[i].w != 0 &&
522 !conv_choices[i].has_point(x, y); i++)
523 ;
524 if (conv_choices[i].w != 0) // Found one?
525 return i;
526 else
527 return -1;
528 }
529
530 /*
531 * Repaint everything.
532 */
533
paint()534 void Conversation::paint(
535 ) {
536 paint_faces(true);
537 if (avatar_face.w) // Choices?
538 show_avatar_choices();
539 }
540
541 /*
542 * Repaint the faces. Assumes clip has already been set to screen.
543 */
544
paint_faces(bool text)545 void Conversation::paint_faces(
546 bool text // Show text too.
547 ) {
548 if (!num_faces)
549 return;
550 const int max_faces = array_size(face_info);
551 for (int i = 0; i < max_faces; i++) {
552 Npc_face_info *finfo = face_info[i];
553 if (!finfo)
554 continue;
555 Shape_frame *face = finfo->face_num >= 0 ?
556 finfo->shape.get_shape() : nullptr;
557 if (face) {
558 int face_xleft = face->get_xleft();
559 int face_yabove = face->get_yabove();
560 int fx = finfo->face_rect.x + face_xleft;
561 int fy = finfo->face_rect.y + face_yabove;
562 if (finfo->large_face) {
563 // Guardian, serpents: fill whole screen with the
564 // background pixel.
565 unsigned char px = face->get_topleft_pix();
566 const int xfstart = 0xff - sman->get_xforms_cnt();
567 int fw = finfo->face_rect.w;
568 int fh = finfo->face_rect.h;
569 Image_window8 *win = gwin->get_win();
570 int gw = win->get_game_width();
571 int gh = win->get_game_height();
572 // Fill only if (a) not transparent, (b) is a translucent
573 // color and (c) the face is not covering the entire screen.
574 if (px >= xfstart && px <= 0xfe && (gw > fw || gh > fh)) {
575 Xform_palette &xform = sman->get_xform(px - xfstart);
576 int gx = win->get_start_x();
577 int gy = win->get_start_y();
578 // Another option: 4 fills outside the face area.
579 win->fill_translucent8(0, gw, gh, gx, gy, xform);
580 }
581 }
582 // Use translucency.
583 sman->paint_shape(fx, fy, face, true);
584 }
585 if (text) { // Show text too?
586 TileRect &box = finfo->text_rect;
587 // Use red for Guardian, snake.
588 int font = finfo->large_face ? 7 : 0;
589 sman->paint_text_box(font, finfo->cur_text.c_str(),
590 box.x, box.y, box.w, box.h, -1, true,
591 finfo->large_face, gwin->get_text_bg());
592 }
593 }
594 }
595
596
597 /*
598 * return nr. of conversation option 'str'. -1 if not found
599 */
600
locate_answer(const char * str)601 int Conversation::locate_answer(const char *str) {
602 int num = 0;
603 for (auto& answer : answers) {
604 if (answer == str)
605 return num;
606 num++;
607 }
608
609 return -1;
610 }
611
push_answers()612 void Conversation::push_answers() {
613 answer_stack.push_front(answers);
614 answers.clear();
615 }
616
pop_answers()617 void Conversation::pop_answers() {
618 answers = answer_stack.front();
619 answer_stack.pop_front();
620 gwin->paint(); // Really just need to figure tbox.
621 }
622