#include "message_system.h" #include "game.h" #include "util.h" const int CHARS_TO_REVEAL = 1; const int CHARS_PER_BEEP = 8; void MessageSystem::tick(double dt) { if (game_.isGameplayPaused()) return; if (barsState_ == BarsState::Opening || barsState_ == BarsState::Closing) { accum_ += dt; if (accum_ >= length_) { if (barsState_ == BarsState::Opening) { barsState_ = BarsState::Open; } else { barsState_ = BarsState::Closed; } } } else if (barsState_ == BarsState::Open) { if (!linesToShow_.empty()) { textAdvTimer_.accumulate(dt); while (textAdvTimer_.step()) { // Try to advance text on the first line that isn't totally revealed yet. bool advancedChars = false; for (MessageLine& line : linesToShow_) { if (line.charsRevealed < line.text.size()) { // Every so often play a beep. if (line.charsRevealed % CHARS_PER_BEEP == 0) { if (speaker_ == SpeakerType::Man) { game_.getMixer().playSound("../res/sfx/speaking_man.wav"); } else if (speaker_ == SpeakerType::Woman) { game_.getMixer().playSound("../res/sfx/speaking_woman.wav"); } else if (speaker_ == SpeakerType::Boy) { game_.getMixer().playSound("../res/sfx/speaking_boy.wav"); } else if (speaker_ == SpeakerType::Girl) { game_.getMixer().playSound("../res/sfx/speaking_girl.wav"); } else if (speaker_ == SpeakerType::Nonhuman) { game_.getMixer().playSound("../res/sfx/speaking_nonhuman.wav"); } } if (line.isChoice) { line.charsRevealed = line.text.size(); choiceSelection_ = 0; } else { line.charsRevealed += CHARS_TO_REVEAL; if (line.charsRevealed > line.text.size()) { line.charsRevealed = line.text.size(); } } advancedChars = true; break; } } if (!advancedChars) { // If both lines are totally revealed, see if we can scroll up a line. // This is doable as long as the last currently visible line doesn't // have the flag that means an A press is required. if (!lines_.empty() && !linesToShow_.back().pause) { if (linesToShow_.size() == 2) { linesToShow_.pop_front(); } linesToShow_.push_back(lines_.front()); lines_.pop_front(); } else { showNextArrow_ = true; } } } if (showNextArrow_) { nextArrowBobTimer_.accumulate(dt); while (nextArrowBobTimer_.step()) { if (nextArrowBobDown_) { nextArrowBobPos_++; if (nextArrowBobPos_ >= 4) { nextArrowBobDown_ = false; } } else { nextArrowBobPos_--; if (nextArrowBobPos_ <= 0) { nextArrowBobDown_ = true; } } } } } } } void MessageSystem::displayCutsceneBars() { accum_ = 0.0; barsState_ = BarsState::Opening; } void MessageSystem::hideCutsceneBars() { accum_ = 0.0; barsState_ = BarsState::Closing; } void MessageSystem::displayMessage(std::string_view msg, std::string speakerName, SpeakerType speakerType) { if (!(barsState_ == BarsState::Opening || barsState_ == BarsState::Open)) { displayCutsceneBars(); } speaker_ = speakerType; speakerName_ = speakerName; bool shouldAddBlank = false; auto lineChunks = splitStr>(std::string(msg), "\n"); for (std::string text : lineChunks) { if (text.substr(0, 1) == "\f") { text.erase(0, 1); shouldAddBlank = false; if (!lines_.empty()) { lines_.back().pause = true; } } bool bulleted = false; if (text.substr(0, 2) == "* ") { text.erase(0, 2); bulleted = true; } auto words = splitStr>(text, " "); std::string curLine; int curWidth = 0; bool firstWord = true; // I'm gonna be frank and admit it: I'm not gonna take hyphenation into // consideration. Please don't write any words that are wider than the // textbox. for (const std::string& word : words) { int wordWidth = 0; for (int i=0; i MESSAGE_TEXT_WIDTH) { lines_.push_back({.text = curLine, .bulleted = bulleted}); curLine = word; curWidth = wordWidth + game_.getFont().getCharacterWidth(' '); bulleted = false; if (shouldAddBlank) { shouldAddBlank = false; lines_.back().pause = true; } else { shouldAddBlank = true; } } else { curWidth = nextWidth; if (!firstWord) { curLine.append(" "); } curLine.append(word); } firstWord = false; } lines_.push_back({.text = curLine, .bulleted = bulleted}); if (shouldAddBlank) { shouldAddBlank = false; lines_.back().pause = true; } else { shouldAddBlank = true; } } if (!lines_.empty()) { lines_.back().pause = true; } if (linesToShow_.empty()) { linesToShow_.push_back(lines_.front()); lines_.pop_front(); if (!linesToShow_.back().pause) { linesToShow_.push_back(lines_.front()); lines_.pop_front(); } } } void MessageSystem::showChoice(std::string choice1, std::string choice2) { MessageLine line; line.text = std::string(8, ' ') + choice1 + std::string(11, ' ') + choice2; line.pause = true; line.isChoice = true; line.choicePos[0] = 8; line.choicePos[1] = 8 + 11 + choice1.size(); lines_.push_back(line); } void MessageSystem::advanceText() { // Cutscene must be active. if (barsState_ != BarsState::Open) { return; } if (linesToShow_.empty()) { return; } // We can only advance if all visible lines are fully revealed. for (const MessageLine& line : linesToShow_) { if (line.charsRevealed != line.text.size()) { return; } } // If the last visible line doesn't have a pause, then the cutscene // will automatically progress without us doing anything. if (!linesToShow_.back().pause) { return; } // Clear the current pause. linesToShow_.back().pause = false; showNextArrow_ = false; if (lines_.empty()) { if (isChoiceActive()) { if (choiceSelection_ == 0) { game_.getMixer().playSound("../res/sfx/left_selection.wav"); } else { game_.getMixer().playSound("../res/sfx/right_selection.wav"); } } linesToShow_.clear(); } else { game_.getMixer().playSound("../res/sfx/pageflip.wav"); } } void MessageSystem::selectFirstChoice() { if (isChoiceActive() && choiceSelection_ != 0) { choiceSelection_ = 0; game_.getMixer().playSound("../res/sfx/horizontal_menu.wav"); } } void MessageSystem::selectSecondChoice() { if (isChoiceActive() && choiceSelection_ != 1) { choiceSelection_ = 1; game_.getMixer().playSound("../res/sfx/horizontal_menu.wav"); } } double MessageSystem::getCutsceneBarsProgress() const { switch (barsState_) { case BarsState::Closed: return 0.0; case BarsState::Opening: return accum_ / length_; case BarsState::Open: return 1.0; case BarsState::Closing: return 1.0 - (accum_ / length_); } }