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