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