Files
scummvm-cursorfix/engines/ultima/ultima4/game/person.cpp
2026-02-02 04:50:13 +01:00

614 lines
16 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "ultima/ultima4/game/person.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/script.h"
#include "ultima/ultima4/controllers/read_choice_controller.h"
#include "ultima/ultima4/controllers/read_int_controller.h"
#include "ultima/ultima4/controllers/read_player_controller.h"
#include "ultima/ultima4/conversation/conversation.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/sound/music.h"
#include "ultima/ultima4/ultima4.h"
namespace Ultima {
namespace Ultima4 {
int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines);
/**
* Returns true of the object that 'punknown' points
* to is a person object
*/
bool isPerson(Object *punknown) {
Person *p;
if ((p = dynamic_cast<Person *>(punknown)) != nullptr)
return true;
else
return false;
}
/**
* Splits a piece of response text into screen-sized chunks.
*/
Common::List<Common::String> replySplit(const Common::String &text) {
Common::String str = text;
int pos, real_lines;
Common::List<Common::String> reply;
/* skip over any initial newlines */
if ((pos = str.find("\n\n")) == 0)
str = str.substr(pos + 1);
uint num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
/* we only have one chunk, no need to split it up */
uint len = str.size();
if (num_chars == len)
reply.push_back(str);
else {
Common::String pre = str.substr(0, num_chars);
/* add the first chunk to the list */
reply.push_back(pre);
/* skip over any initial newlines */
if ((pos = str.find("\n\n")) == 0)
str = str.substr(pos + 1);
while (num_chars != str.size()) {
/* go to the rest of the text */
str = str.substr(num_chars);
/* skip over any initial newlines */
if ((pos = str.find("\n\n")) == 0)
str = str.substr(pos + 1);
/* find the next chunk and add it */
num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
pre = str.substr(0, num_chars);
reply.push_back(pre);
}
}
return reply;
}
Person::Person(MapTile tile) : Creature(tile), _start(0, 0) {
setType(Object::PERSON);
_dialogue = nullptr;
_npcType = NPC_EMPTY;
}
Person::Person(const Person *p) : Creature(p->_tile) {
*this = *p;
}
bool Person::canConverse() const {
return isVendor() || _dialogue != nullptr;
}
bool Person::isVendor() const {
return
_npcType >= NPC_VENDOR_WEAPONS &&
_npcType <= NPC_VENDOR_STABLE;
}
Common::String Person::getName() const {
if (_dialogue)
return _dialogue->getName();
else if (_npcType == NPC_EMPTY)
return Creature::getName();
else
return "(unnamed person)";
}
void Person::goToStartLocation() {
setCoords(_start);
}
void Person::setDialogue(Dialogue *d) {
_dialogue = d;
if (_tile.getTileType()->getName() == "beggar")
_npcType = NPC_TALKER_BEGGAR;
else if (_tile.getTileType()->getName() == "guard")
_npcType = NPC_TALKER_GUARD;
else
_npcType = NPC_TALKER;
}
void Person::setNpcType(PersonNpcType t) {
_npcType = t;
assertMsg(!isVendor() || _dialogue == nullptr, "vendor has dialogue");
}
Common::List<Common::String> Person::getConversationText(Conversation *cnv, const char *inquiry) {
Common::String text;
/*
* a convsation with a vendor
*/
if (isVendor()) {
static const Common::String ids[] = {
"Weapons", "Armor", "Food", "Tavern", "Reagents", "Healer", "Inn", "Guild", "Stable"
};
Script *script = cnv->_script;
/**
* We aren't currently running a script, load the appropriate one!
*/
if (cnv->_state == Conversation::INTRO) {
// unload the previous script if it wasn't already unloaded
if (script->getState() != Script::STATE_UNLOADED)
script->unload();
script->load("vendorScript.xml", ids[_npcType - NPC_VENDOR_WEAPONS], "vendor", g_context->_location->_map->getName());
script->run("intro");
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationChoiceHelper choiceDialog;
#endif
while (script->getState() != Script::STATE_DONE) {
// Gather input for the script
if (script->getState() == Script::STATE_INPUT) {
switch (script->getInputType()) {
case Script::INPUT_CHOICE: {
const Common::String &choices = script->getChoices();
// Get choice
#ifdef IOS_ULTIMA4
choiceDialog.updateChoices(choices, script->getTarget(), npcType);
#endif
char val = ReadChoiceController::get(choices);
if (Common::isSpace(val) || val == '\033')
script->unsetVar(script->getInputName());
else {
Common::String s_val;
s_val = val;
script->setVar(script->getInputName(), s_val);
}
}
break;
case Script::INPUT_KEYPRESS:
ReadChoiceController::get(" \015\033");
break;
case Script::INPUT_NUMBER: {
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper ipadNumberInput;
ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeNumberPad, "Amount?");
#endif
int val = ReadIntController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
script->setVar(script->getInputName(), val);
}
break;
case Script::INPUT_STRING: {
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper ipadNumberInput;
ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeDefault);
#endif
Common::String str = ReadStringController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
if (str.size()) {
lowercase(str);
script->setVar(script->getInputName(), str);
} else script->unsetVar(script->getInputName());
}
break;
case Script::INPUT_PLAYER: {
ReadPlayerController getPlayerCtrl;
eventHandler->pushController(&getPlayerCtrl);
int player = getPlayerCtrl.waitFor();
if (player != -1) {
Common::String player_str = xu4_to_string(player + 1);
script->setVar(script->getInputName(), player_str);
} else script->unsetVar(script->getInputName());
}
break;
default:
break;
} // } switch
// Continue running the script!
g_context->_line++;
script->_continue();
} // } if
} // } while
}
// Unload the script
script->unload();
cnv->_state = Conversation::DONE;
}
/*
* a conversation with a non-vendor
*/
else {
text = "\n\n\n";
switch (cnv->_state) {
case Conversation::INTRO:
text = getIntro(cnv);
break;
case Conversation::TALK:
text += getResponse(cnv, inquiry) + "\n";
break;
case Conversation::CONFIRMATION:
assertMsg(_npcType == NPC_LORD_BRITISH, "invalid state: %d", cnv->_state);
text += lordBritishGetQuestionResponse(cnv, inquiry);
break;
case Conversation::ASK:
case Conversation::ASKYESNO:
assertMsg(_npcType != NPC_HAWKWIND, "invalid state for hawkwind conversation");
text += talkerGetQuestionResponse(cnv, inquiry) + "\n";
break;
case Conversation::GIVEBEGGAR:
assertMsg(_npcType == NPC_TALKER_BEGGAR, "invalid npc type: %d", _npcType);
text = beggarGetQuantityResponse(cnv, inquiry);
break;
case Conversation::FULLHEAL:
case Conversation::ADVANCELEVELS:
/* handled elsewhere */
break;
default:
error("invalid state: %d", cnv->_state);
}
}
return replySplit(text);
}
Common::String Person::getPrompt(Conversation *cnv) {
if (isVendor())
return "";
Common::String prompt;
if (cnv->_state == Conversation::ASK)
prompt = getQuestion(cnv);
else if (cnv->_state == Conversation::GIVEBEGGAR)
prompt = "How much? ";
else if (cnv->_state == Conversation::CONFIRMATION)
prompt = "\n\nHe asks: Art thou well?";
else if (cnv->_state != Conversation::ASKYESNO)
prompt = _dialogue->getPrompt();
return prompt;
}
const char *Person::getChoices(Conversation *cnv) {
if (isVendor())
return cnv->_script->getChoices().c_str();
switch (cnv->_state) {
case Conversation::CONFIRMATION:
case Conversation::CONTINUEQUESTION:
return "ny\015 \033";
case Conversation::PLAYER:
return "012345678\015 \033";
default:
error("invalid state: %d", cnv->_state);
}
return nullptr;
}
Common::String Person::getIntro(Conversation *cnv) {
if (_npcType == NPC_EMPTY) {
cnv->_state = Conversation::DONE;
return Common::String("Funny, no\nresponse!\n");
}
// As far as I can tell, about 50% of the time they tell you their
// name in the introduction
Response *intro;
if (xu4_random(2) == 0)
intro = _dialogue->getIntro();
else
intro = _dialogue->getLongIntro();
cnv->_state = Conversation::TALK;
Common::String text = processResponse(cnv, intro);
return text;
}
Common::String Person::processResponse(Conversation *cnv, Response *response) {
Common::String text;
const Std::vector<ResponsePart> &parts = response->getParts();
for (const auto &i : parts) {
// check for command triggers
if (i.isCommand())
runCommand(cnv, i);
// otherwise, append response part to reply
else
text += i;
}
return text;
}
void Person::runCommand(Conversation *cnv, const ResponsePart &command) {
if (command == g_responseParts->ASK) {
cnv->_question = _dialogue->getQuestion();
cnv->_state = Conversation::ASK;
} else if (command == g_responseParts->END) {
cnv->_state = Conversation::DONE;
} else if (command == g_responseParts->ATTACK) {
cnv->_state = Conversation::ATTACK;
} else if (command == g_responseParts->BRAGGED) {
g_context->_party->adjustKarma(KA_BRAGGED);
} else if (command == g_responseParts->HUMBLE) {
g_context->_party->adjustKarma(KA_HUMBLE);
} else if (command == g_responseParts->ADVANCELEVELS) {
cnv->_state = Conversation::ADVANCELEVELS;
} else if (command == g_responseParts->HEALCONFIRM) {
cnv->_state = Conversation::CONFIRMATION;
} else if (command == g_responseParts->STARTMUSIC_LB) {
g_music->lordBritish();
} else if (command == g_responseParts->STARTMUSIC_HW) {
g_music->hawkwind();
} else if (command == g_responseParts->STOPMUSIC) {
g_music->playMapMusic();
} else if (command == g_responseParts->HAWKWIND) {
g_context->_party->adjustKarma(KA_HAWKWIND);
} else {
error("unknown command trigger in dialogue response: %s\n", Common::String(command).c_str());
}
}
Common::String Person::getResponse(Conversation *cnv, const char *inquiry) {
Common::String reply;
Virtue v;
const ResponsePart &action = _dialogue->getAction();
reply = "\n";
/* Does the person take action during the conversation? */
if (action == g_responseParts->END) {
runCommand(cnv, action);
return _dialogue->getPronoun() + " turns away!\n";
} else if (action == g_responseParts->ATTACK) {
runCommand(cnv, action);
return Common::String("\n") + getName() + " says: On guard! Fool!";
}
if (_npcType == NPC_TALKER_BEGGAR && scumm_strnicmp(inquiry, "give", 4) == 0) {
reply.clear();
cnv->_state = Conversation::GIVEBEGGAR;
}
else if (scumm_strnicmp(inquiry, "join", 4) == 0 &&
g_context->_party->canPersonJoin(getName(), &v)) {
CannotJoinError join = g_context->_party->join(getName());
if (join == JOIN_SUCCEEDED) {
reply += "I am honored to join thee!";
g_context->_location->_map->removeObject(this);
cnv->_state = Conversation::DONE;
} else {
reply += "Thou art not ";
reply += (join == JOIN_NOT_VIRTUOUS) ? getVirtueAdjective(v) : "experienced";
reply += " enough for me to join thee.";
}
}
else if ((*_dialogue)[inquiry]) {
Dialogue::Keyword *kw = (*_dialogue)[inquiry];
reply = processResponse(cnv, kw->getResponse());
}
else if (settings._debug && scumm_strnicmp(inquiry, "dump", 4) == 0) {
Std::vector<Common::String> words = split(inquiry, " \t");
if (words.size() <= 1)
reply = _dialogue->dump("");
else
reply = _dialogue->dump(words[1]);
}
else
reply += processResponse(cnv, _dialogue->getDefaultAnswer());
return reply;
}
Common::String Person::talkerGetQuestionResponse(Conversation *cnv, const char *answer) {
bool valid = false;
bool yes = false;
char ans = tolower(answer[0]);
if (ans == 'y' || ans == 'n') {
valid = true;
yes = ans == 'y';
}
if (!valid) {
cnv->_state = Conversation::ASKYESNO;
return "Yes or no!";
}
cnv->_state = Conversation::TALK;
return "\n" + processResponse(cnv, cnv->_question->getResponse(yes));
}
Common::String Person::beggarGetQuantityResponse(Conversation *cnv, const char *response) {
Common::String reply;
cnv->_quant = (int) strtol(response, nullptr, 10);
cnv->_state = Conversation::TALK;
if (cnv->_quant > 0) {
if (g_context->_party->donate(cnv->_quant)) {
reply = "\n";
reply += _dialogue->getPronoun();
reply += " says: Oh Thank thee! I shall never forget thy kindness!\n";
}
else
reply = "\n\nThou hast not that much gold!\n";
} else
reply = "\n";
return reply;
}
Common::String Person::lordBritishGetQuestionResponse(Conversation *cnv, const char *answer) {
Common::String reply;
cnv->_state = Conversation::TALK;
if (tolower(answer[0]) == 'y') {
reply = "Y\n\nHe says: That is good.\n";
}
else if (tolower(answer[0]) == 'n') {
reply = "N\n\nHe says: Let me heal thy wounds!\n";
cnv->_state = Conversation::FULLHEAL;
}
else
reply = "\n\nThat I cannot\nhelp thee with.\n";
return reply;
}
Common::String Person::getQuestion(Conversation *cnv) {
return "\n" + cnv->_question->getText() + "\n\nYou say: ";
}
/**
* Returns the number of characters needed to get to
* the next line of text (based on column width).
*/
int chars_to_next_line(const char *s, int columnmax) {
int chars = -1;
if (strlen(s) > 0) {
int lastbreak = columnmax;
chars = 0;
for (const char *str = s; *str; str++) {
if (*str == '\n')
return (str - s);
else if (*str == ' ')
lastbreak = (str - s);
else if (++chars >= columnmax)
return lastbreak;
}
}
return chars;
}
/**
* Counts the number of lines (of the maximum width given by
* columnmax) in the Common::String.
*/
int linecount(const Common::String &s, int columnmax) {
int lines = 0;
unsigned ch = 0;
while (ch < s.size()) {
ch += chars_to_next_line(s.c_str() + ch, columnmax);
if (ch < s.size())
ch++;
lines++;
}
return lines;
}
/**
* Returns the number of characters needed to produce a
* valid screen of text (given a column width and row height)
*/
int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines) {
int chars = 0,
totalChars = 0;
Common::String new_str = s;
const char *str = new_str.c_str();
// try breaking text into paragraphs first
Common::String text = s;
Common::String paragraphs;
uint pos;
int lines = 0;
while ((pos = text.find("\n\n")) < text.size()) {
Common::String p = text.substr(0, pos);
lines += linecount(p.c_str(), columnmax);
if (lines <= linesdesired)
paragraphs += p + "\n";
else
break;
text = text.substr(pos + 1);
}
// Seems to be some sort of clang compilation bug in this code, that causes this addition
// to not work correctly.
int totalPossibleLines = lines + linecount(text.c_str(), columnmax);
if (totalPossibleLines <= linesdesired)
paragraphs += text;
if (!paragraphs.empty()) {
*real_lines = lines;
return paragraphs.size();
} else {
// reset variables and try another way
lines = 1;
}
// gather all the line breaks
while ((chars = chars_to_next_line(str, columnmax)) >= 0) {
if (++lines >= linesdesired)
break;
int num_to_move = chars;
if (*(str + num_to_move) == '\n')
num_to_move++;
totalChars += num_to_move;
str += num_to_move;
}
*real_lines = lines;
return totalChars;
}
} // End of namespace Ultima4
} // End of namespace Ultima