1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "ultima/ultima4/game/person.h"
24 #include "ultima/ultima4/game/names.h"
25 #include "ultima/ultima4/game/player.h"
26 #include "ultima/ultima4/views/stats.h"
27 #include "ultima/ultima4/game/context.h"
28 #include "ultima/ultima4/game/script.h"
29 #include "ultima/ultima4/controllers/read_choice_controller.h"
30 #include "ultima/ultima4/controllers/read_int_controller.h"
31 #include "ultima/ultima4/controllers/read_player_controller.h"
32 #include "ultima/ultima4/conversation/conversation.h"
33 #include "ultima/ultima4/core/config.h"
34 #include "ultima/ultima4/core/settings.h"
35 #include "ultima/ultima4/core/types.h"
36 #include "ultima/ultima4/core/utils.h"
37 #include "ultima/ultima4/events/event_handler.h"
38 #include "ultima/ultima4/filesys/savegame.h"
39 #include "ultima/ultima4/map/city.h"
40 #include "ultima/ultima4/map/location.h"
41 #include "ultima/ultima4/sound/music.h"
42 #include "ultima/ultima4/ultima4.h"
43 
44 namespace Ultima {
45 namespace Ultima4 {
46 
47 int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines);
48 
49 /**
50  * Returns true of the object that 'punknown' points
51  * to is a person object
52  */
isPerson(Object * punknown)53 bool isPerson(Object *punknown) {
54 	Person *p;
55 	if ((p = dynamic_cast<Person *>(punknown)) != nullptr)
56 		return true;
57 	else
58 		return false;
59 }
60 
61 /**
62  * Splits a piece of response text into screen-sized chunks.
63  */
replySplit(const Common::String & text)64 Common::List<Common::String> replySplit(const Common::String &text) {
65 	Common::String str = text;
66 	int pos, real_lines;
67 	Common::List<Common::String> reply;
68 
69 	/* skip over any initial newlines */
70 	if ((pos = str.find("\n\n")) == 0)
71 		str = str.substr(pos + 1);
72 
73 	uint num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
74 
75 	/* we only have one chunk, no need to split it up */
76 	uint len = str.size();
77 	if (num_chars == len)
78 		reply.push_back(str);
79 	else {
80 		Common::String pre = str.substr(0, num_chars);
81 
82 		/* add the first chunk to the list */
83 		reply.push_back(pre);
84 		/* skip over any initial newlines */
85 		if ((pos = str.find("\n\n")) == 0)
86 			str = str.substr(pos + 1);
87 
88 		while (num_chars != str.size()) {
89 			/* go to the rest of the text */
90 			str = str.substr(num_chars);
91 			/* skip over any initial newlines */
92 			if ((pos = str.find("\n\n")) == 0)
93 				str = str.substr(pos + 1);
94 
95 			/* find the next chunk and add it */
96 			num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
97 			pre = str.substr(0, num_chars);
98 
99 			reply.push_back(pre);
100 		}
101 	}
102 
103 	return reply;
104 }
105 
Person(MapTile tile)106 Person::Person(MapTile tile) : Creature(tile), _start(0, 0) {
107 	setType(Object::PERSON);
108 	_dialogue = nullptr;
109 	_npcType = NPC_EMPTY;
110 }
111 
Person(const Person * p)112 Person::Person(const Person *p) : Creature(p->_tile) {
113 	*this = *p;
114 }
115 
canConverse() const116 bool Person::canConverse() const {
117 	return isVendor() || _dialogue != nullptr;
118 }
119 
isVendor() const120 bool Person::isVendor() const {
121 	return
122 	    _npcType >= NPC_VENDOR_WEAPONS &&
123 	    _npcType <= NPC_VENDOR_STABLE;
124 }
125 
getName() const126 Common::String Person::getName() const {
127 	if (_dialogue)
128 		return _dialogue->getName();
129 	else if (_npcType == NPC_EMPTY)
130 		return Creature::getName();
131 	else
132 		return "(unnamed person)";
133 }
134 
goToStartLocation()135 void Person::goToStartLocation() {
136 	setCoords(_start);
137 }
138 
setDialogue(Dialogue * d)139 void Person::setDialogue(Dialogue *d) {
140 	_dialogue = d;
141 	if (_tile.getTileType()->getName() == "beggar")
142 		_npcType = NPC_TALKER_BEGGAR;
143 	else if (_tile.getTileType()->getName() == "guard")
144 		_npcType = NPC_TALKER_GUARD;
145 	else
146 		_npcType = NPC_TALKER;
147 }
148 
setNpcType(PersonNpcType t)149 void Person::setNpcType(PersonNpcType t) {
150 	_npcType = t;
151 	assertMsg(!isVendor() || _dialogue == nullptr, "vendor has dialogue");
152 }
153 
getConversationText(Conversation * cnv,const char * inquiry)154 Common::List<Common::String> Person::getConversationText(Conversation *cnv, const char *inquiry) {
155 	Common::String text;
156 
157 	/*
158 	 * a convsation with a vendor
159 	 */
160 	if (isVendor()) {
161 		static const Common::String ids[] = {
162 			"Weapons", "Armor", "Food", "Tavern", "Reagents", "Healer", "Inn", "Guild", "Stable"
163 		};
164 		Script *script = cnv->_script;
165 
166 		/**
167 		 * We aren't currently running a script, load the appropriate one!
168 		 */
169 		if (cnv->_state == Conversation::INTRO) {
170 			// unload the previous script if it wasn't already unloaded
171 			if (script->getState() != Script::STATE_UNLOADED)
172 				script->unload();
173 			script->load("vendorScript.xml", ids[_npcType - NPC_VENDOR_WEAPONS], "vendor", g_context->_location->_map->getName());
174 			script->run("intro");
175 #ifdef IOS_ULTIMA4
176 			U4IOS::IOSConversationChoiceHelper choiceDialog;
177 #endif
178 			while (script->getState() != Script::STATE_DONE) {
179 				// Gather input for the script
180 				if (script->getState() == Script::STATE_INPUT) {
181 					switch (script->getInputType()) {
182 					case Script::INPUT_CHOICE: {
183 						const Common::String &choices = script->getChoices();
184 						// Get choice
185 #ifdef IOS_ULTIMA4
186 						choiceDialog.updateChoices(choices, script->getTarget(), npcType);
187 #endif
188 						char val = ReadChoiceController::get(choices);
189 						if (Common::isSpace(val) || val == '\033')
190 							script->unsetVar(script->getInputName());
191 						else {
192 							Common::String s_val;
193 							s_val = val;
194 							script->setVar(script->getInputName(), s_val);
195 						}
196 					}
197 					break;
198 
199 					case Script::INPUT_KEYPRESS:
200 						ReadChoiceController::get(" \015\033");
201 						break;
202 
203 					case Script::INPUT_NUMBER: {
204 #ifdef IOS_ULTIMA4
205 						U4IOS::IOSConversationHelper ipadNumberInput;
206 						ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeNumberPad, "Amount?");
207 #endif
208 						int val = ReadIntController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
209 						script->setVar(script->getInputName(), val);
210 					}
211 					break;
212 
213 					case Script::INPUT_STRING: {
214 #ifdef IOS_ULTIMA4
215 						U4IOS::IOSConversationHelper ipadNumberInput;
216 						ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeDefault);
217 #endif
218 						Common::String str = ReadStringController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
219 						if (str.size()) {
220 							lowercase(str);
221 							script->setVar(script->getInputName(), str);
222 						} else script->unsetVar(script->getInputName());
223 					}
224 					break;
225 
226 					case Script::INPUT_PLAYER: {
227 						ReadPlayerController getPlayerCtrl;
228 						eventHandler->pushController(&getPlayerCtrl);
229 						int player = getPlayerCtrl.waitFor();
230 						if (player != -1) {
231 							Common::String player_str = xu4_to_string(player + 1);
232 							script->setVar(script->getInputName(), player_str);
233 						} else script->unsetVar(script->getInputName());
234 					}
235 					break;
236 
237 					default:
238 						break;
239 					} // } switch
240 
241 					// Continue running the script!
242 					g_context->_line++;
243 					script->_continue();
244 				} // } if
245 			} // } while
246 		}
247 
248 		// Unload the script
249 		script->unload();
250 		cnv->_state = Conversation::DONE;
251 	}
252 
253 	/*
254 	 * a conversation with a non-vendor
255 	 */
256 	else {
257 		text = "\n\n\n";
258 
259 		switch (cnv->_state) {
260 		case Conversation::INTRO:
261 			text = getIntro(cnv);
262 			break;
263 
264 		case Conversation::TALK:
265 			text += getResponse(cnv, inquiry) + "\n";
266 			break;
267 
268 		case Conversation::CONFIRMATION:
269 			assertMsg(_npcType == NPC_LORD_BRITISH, "invalid state: %d", cnv->_state);
270 			text += lordBritishGetQuestionResponse(cnv, inquiry);
271 			break;
272 
273 		case Conversation::ASK:
274 		case Conversation::ASKYESNO:
275 			assertMsg(_npcType != NPC_HAWKWIND, "invalid state for hawkwind conversation");
276 			text += talkerGetQuestionResponse(cnv, inquiry) + "\n";
277 			break;
278 
279 		case Conversation::GIVEBEGGAR:
280 			assertMsg(_npcType == NPC_TALKER_BEGGAR, "invalid npc type: %d", _npcType);
281 			text = beggarGetQuantityResponse(cnv, inquiry);
282 			break;
283 
284 		case Conversation::FULLHEAL:
285 		case Conversation::ADVANCELEVELS:
286 			/* handled elsewhere */
287 			break;
288 
289 		default:
290 			error("invalid state: %d", cnv->_state);
291 		}
292 	}
293 
294 	return replySplit(text);
295 }
296 
getPrompt(Conversation * cnv)297 Common::String Person::getPrompt(Conversation *cnv) {
298 	if (isVendor())
299 		return "";
300 
301 	Common::String prompt;
302 	if (cnv->_state == Conversation::ASK)
303 		prompt = getQuestion(cnv);
304 	else if (cnv->_state == Conversation::GIVEBEGGAR)
305 		prompt = "How much? ";
306 	else if (cnv->_state == Conversation::CONFIRMATION)
307 		prompt = "\n\nHe asks: Art thou well?";
308 	else if (cnv->_state != Conversation::ASKYESNO)
309 		prompt = _dialogue->getPrompt();
310 
311 	return prompt;
312 }
313 
getChoices(Conversation * cnv)314 const char *Person::getChoices(Conversation *cnv) {
315 	if (isVendor())
316 		return cnv->_script->getChoices().c_str();
317 
318 	switch (cnv->_state) {
319 	case Conversation::CONFIRMATION:
320 	case Conversation::CONTINUEQUESTION:
321 		return "ny\015 \033";
322 
323 	case Conversation::PLAYER:
324 		return "012345678\015 \033";
325 
326 	default:
327 		error("invalid state: %d", cnv->_state);
328 	}
329 
330 	return nullptr;
331 }
332 
getIntro(Conversation * cnv)333 Common::String Person::getIntro(Conversation *cnv) {
334 	if (_npcType == NPC_EMPTY) {
335 		cnv->_state = Conversation::DONE;
336 		return Common::String("Funny, no\nresponse!\n");
337 	}
338 
339 	// As far as I can tell, about 50% of the time they tell you their
340 	// name in the introduction
341 	Response *intro;
342 	if (xu4_random(2) == 0)
343 		intro = _dialogue->getIntro();
344 	else
345 		intro = _dialogue->getLongIntro();
346 
347 	cnv->_state = Conversation::TALK;
348 	Common::String text = processResponse(cnv, intro);
349 
350 	return text;
351 }
352 
processResponse(Conversation * cnv,Response * response)353 Common::String Person::processResponse(Conversation *cnv, Response *response) {
354 	Common::String text;
355 	const Std::vector<ResponsePart> &parts = response->getParts();
356 	for (Std::vector<ResponsePart>::const_iterator i = parts.begin(); i != parts.end(); i++) {
357 
358 		// check for command triggers
359 		if (i->isCommand())
360 			runCommand(cnv, *i);
361 
362 		// otherwise, append response part to reply
363 		else
364 			text += *i;
365 	}
366 	return text;
367 }
368 
runCommand(Conversation * cnv,const ResponsePart & command)369 void Person::runCommand(Conversation *cnv, const ResponsePart &command) {
370 	if (command == g_responseParts->ASK) {
371 		cnv->_question = _dialogue->getQuestion();
372 		cnv->_state = Conversation::ASK;
373 	} else if (command == g_responseParts->END) {
374 		cnv->_state = Conversation::DONE;
375 	} else if (command == g_responseParts->ATTACK) {
376 		cnv->_state = Conversation::ATTACK;
377 	} else if (command == g_responseParts->BRAGGED) {
378 		g_context->_party->adjustKarma(KA_BRAGGED);
379 	} else if (command == g_responseParts->HUMBLE) {
380 		g_context->_party->adjustKarma(KA_HUMBLE);
381 	} else if (command == g_responseParts->ADVANCELEVELS) {
382 		cnv->_state = Conversation::ADVANCELEVELS;
383 	} else if (command == g_responseParts->HEALCONFIRM) {
384 		cnv->_state = Conversation::CONFIRMATION;
385 	} else if (command == g_responseParts->STARTMUSIC_LB) {
386 		g_music->lordBritish();
387 	} else if (command == g_responseParts->STARTMUSIC_HW) {
388 		g_music->hawkwind();
389 	} else if (command == g_responseParts->STOPMUSIC) {
390 		g_music->playMapMusic();
391 	} else if (command == g_responseParts->HAWKWIND) {
392 		g_context->_party->adjustKarma(KA_HAWKWIND);
393 	} else {
394 		error("unknown command trigger in dialogue response: %s\n", Common::String(command).c_str());
395 	}
396 }
397 
getResponse(Conversation * cnv,const char * inquiry)398 Common::String Person::getResponse(Conversation *cnv, const char *inquiry) {
399 	Common::String reply;
400 	Virtue v;
401 	const ResponsePart &action = _dialogue->getAction();
402 
403 	reply = "\n";
404 
405 	/* Does the person take action during the conversation? */
406 	if (action == g_responseParts->END) {
407 		runCommand(cnv, action);
408 		return _dialogue->getPronoun() + " turns away!\n";
409 	} else if (action == g_responseParts->ATTACK) {
410 		runCommand(cnv, action);
411 		return Common::String("\n") + getName() + " says: On guard! Fool!";
412 	}
413 
414 	if (_npcType == NPC_TALKER_BEGGAR && scumm_strnicmp(inquiry, "give", 4) == 0) {
415 		reply.clear();
416 		cnv->_state = Conversation::GIVEBEGGAR;
417 	}
418 
419 	else if (scumm_strnicmp(inquiry, "join", 4) == 0 &&
420 	         g_context->_party->canPersonJoin(getName(), &v)) {
421 		CannotJoinError join = g_context->_party->join(getName());
422 
423 		if (join == JOIN_SUCCEEDED) {
424 			reply += "I am honored to join thee!";
425 			g_context->_location->_map->removeObject(this);
426 			cnv->_state = Conversation::DONE;
427 		} else {
428 			reply += "Thou art not ";
429 			reply += (join == JOIN_NOT_VIRTUOUS) ? getVirtueAdjective(v) : "experienced";
430 			reply += " enough for me to join thee.";
431 		}
432 	}
433 
434 	else if ((*_dialogue)[inquiry]) {
435 		Dialogue::Keyword *kw = (*_dialogue)[inquiry];
436 
437 		reply = processResponse(cnv, kw->getResponse());
438 	}
439 
440 	else if (settings._debug && scumm_strnicmp(inquiry, "dump", 4) == 0) {
441 		Std::vector<Common::String> words = split(inquiry, " \t");
442 		if (words.size() <= 1)
443 			reply = _dialogue->dump("");
444 		else
445 			reply = _dialogue->dump(words[1]);
446 	}
447 
448 	else
449 		reply += processResponse(cnv, _dialogue->getDefaultAnswer());
450 
451 	return reply;
452 }
453 
talkerGetQuestionResponse(Conversation * cnv,const char * answer)454 Common::String Person::talkerGetQuestionResponse(Conversation *cnv, const char *answer) {
455 	bool valid = false;
456 	bool yes = false;
457 	char ans = tolower(answer[0]);
458 
459 	if (ans == 'y' || ans == 'n') {
460 		valid = true;
461 		yes = ans == 'y';
462 	}
463 
464 	if (!valid) {
465 		cnv->_state = Conversation::ASKYESNO;
466 		return "Yes or no!";
467 	}
468 
469 	cnv->_state = Conversation::TALK;
470 	return "\n" + processResponse(cnv, cnv->_question->getResponse(yes));
471 }
472 
beggarGetQuantityResponse(Conversation * cnv,const char * response)473 Common::String Person::beggarGetQuantityResponse(Conversation *cnv, const char *response) {
474 	Common::String reply;
475 
476 	cnv->_quant = (int) strtol(response, nullptr, 10);
477 	cnv->_state = Conversation::TALK;
478 
479 	if (cnv->_quant > 0) {
480 		if (g_context->_party->donate(cnv->_quant)) {
481 			reply = "\n";
482 			reply += _dialogue->getPronoun();
483 			reply += " says: Oh Thank thee! I shall never forget thy kindness!\n";
484 		}
485 
486 		else
487 			reply = "\n\nThou hast not that much gold!\n";
488 	} else
489 		reply = "\n";
490 
491 	return reply;
492 }
493 
lordBritishGetQuestionResponse(Conversation * cnv,const char * answer)494 Common::String Person::lordBritishGetQuestionResponse(Conversation *cnv, const char *answer) {
495 	Common::String reply;
496 
497 	cnv->_state = Conversation::TALK;
498 
499 	if (tolower(answer[0]) == 'y') {
500 		reply = "Y\n\nHe says: That is good.\n";
501 	}
502 
503 	else if (tolower(answer[0]) == 'n') {
504 		reply = "N\n\nHe says: Let me heal thy wounds!\n";
505 		cnv->_state = Conversation::FULLHEAL;
506 	}
507 
508 	else
509 		reply = "\n\nThat I cannot\nhelp thee with.\n";
510 
511 	return reply;
512 }
513 
getQuestion(Conversation * cnv)514 Common::String Person::getQuestion(Conversation *cnv) {
515 	return "\n" + cnv->_question->getText() + "\n\nYou say: ";
516 }
517 
518 /**
519  * Returns the number of characters needed to get to
520  * the next line of text (based on column width).
521  */
chars_to_next_line(const char * s,int columnmax)522 int chars_to_next_line(const char *s, int columnmax) {
523 	int chars = -1;
524 
525 	if (strlen(s) > 0) {
526 		int lastbreak = columnmax;
527 		chars = 0;
528 		for (const char *str = s; *str; str++) {
529 			if (*str == '\n')
530 				return (str - s);
531 			else if (*str == ' ')
532 				lastbreak = (str - s);
533 			else if (++chars >= columnmax)
534 				return lastbreak;
535 		}
536 	}
537 
538 	return chars;
539 }
540 
541 /**
542  * Counts the number of lines (of the maximum width given by
543  * columnmax) in the Common::String.
544  */
linecount(const Common::String & s,int columnmax)545 int linecount(const Common::String &s, int columnmax) {
546 	int lines = 0;
547 	unsigned ch = 0;
548 	while (ch < s.size()) {
549 		ch += chars_to_next_line(s.c_str() + ch, columnmax);
550 		if (ch < s.size())
551 			ch++;
552 		lines++;
553 	}
554 	return lines;
555 }
556 
557 
558 /**
559  * Returns the number of characters needed to produce a
560  * valid screen of text (given a column width and row height)
561  */
chars_needed(const char * s,int columnmax,int linesdesired,int * real_lines)562 int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines) {
563 	int chars = 0,
564 	    totalChars = 0;
565 
566 	Common::String new_str = s;
567 	const char *str = new_str.c_str();
568 
569 	// try breaking text into paragraphs first
570 	Common::String text = s;
571 	Common::String paragraphs;
572 	uint pos;
573 	int lines = 0;
574 	while ((pos = text.find("\n\n")) < text.size()) {
575 		Common::String p = text.substr(0, pos);
576 		lines += linecount(p.c_str(), columnmax);
577 		if (lines <= linesdesired)
578 			paragraphs += p + "\n";
579 		else
580 			break;
581 		text = text.substr(pos + 1);
582 	}
583 	// Seems to be some sort of clang compilation bug in this code, that causes this addition
584 	// to not work correctly.
585 	int totalPossibleLines = lines + linecount(text.c_str(), columnmax);
586 	if (totalPossibleLines <= linesdesired)
587 		paragraphs += text;
588 
589 	if (!paragraphs.empty()) {
590 		*real_lines = lines;
591 		return paragraphs.size();
592 	} else {
593 		// reset variables and try another way
594 		lines = 1;
595 	}
596 	// gather all the line breaks
597 	while ((chars = chars_to_next_line(str, columnmax)) >= 0) {
598 		if (++lines >= linesdesired)
599 			break;
600 
601 		int num_to_move = chars;
602 		if (*(str + num_to_move) == '\n')
603 			num_to_move++;
604 
605 		totalChars += num_to_move;
606 		str += num_to_move;
607 	}
608 
609 	*real_lines = lines;
610 	return totalChars;
611 }
612 
613 } // End of namespace Ultima4
614 } // End of namespace Ultima
615