diff --git a/include/reone/audio/source.h b/include/reone/audio/source.h index 3d0261545..4f47bc9a0 100644 --- a/include/reone/audio/source.h +++ b/include/reone/audio/source.h @@ -37,7 +37,17 @@ class AudioSource : boost::noncopyable { ~AudioSource() { deinit(); } + /** + * Prepares the sound. A clip whose frames share one format and rate goes + * into one buffer whole and plays on the device's clock; any other clip is + * streamed a few frames at a time. + */ void init(); + /** + * Refills a stream's queue with the sound that follows. A stream that ran + * out of queued sound since the last refill, because the frame came late, + * plays on from where it ran out. + */ void render(); void play(); @@ -53,6 +63,8 @@ class AudioSource : boost::noncopyable { void startPaused(); void setPosition(glm::vec3 position); + /** Sets whether the source starts again from the beginning when it ends. */ + void setLooping(bool loop); /** Scales the gain the source was created with; used to fade it out. */ void setGainScale(float scale); /** Plays the source at \p pitch times its own rate, which raises or lowers its pitch. */ @@ -70,6 +82,8 @@ class AudioSource : boost::noncopyable { bool _inited {false}; bool _playing {false}; + /** Paused, or created paused, and not resumed since. */ + bool _paused {false}; std::vector _buffers; bool _streaming {false}; diff --git a/include/reone/game/areamusicplayer.h b/include/reone/game/areamusicplayer.h index 13d8797d5..bb1df0713 100644 --- a/include/reone/game/areamusicplayer.h +++ b/include/reone/game/areamusicplayer.h @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -64,6 +65,15 @@ class AreaMusicPlayer : boost::noncopyable { */ void init(bool tsl); + /** + * What hands over the playing load-screen music when the background + * music asks for the same track; it returns none when it does not. Only + * TSL sets it. + */ + void setLoadMusicHandover(std::function(const std::string &)> handover) { + _loadMusicHandover = std::move(handover); + } + /** * Take the area being entered. Its music and ambient loop wait for the * start; whether each is on starts from what the area asks for. @@ -73,6 +83,12 @@ class AreaMusicPlayer : boost::noncopyable { void start(); /** Fade out the music, end the ambient loop and forget the pending restart and battle state. */ void stopSounds(); + /** + * A movie starts. As stopSounds once the area's sounds have started; + * before then the music is left to start on arrival, but the battle + * music, the pending restart and the ambient loop are switched off. + */ + void stopSoundsForMovie(); /** Stop for the area being left; nothing plays until the next area is loaded and started. */ void unload(); /** Advance the fades, repeat ended tracks and restart an ended ambient loop. */ @@ -133,6 +149,7 @@ class AreaMusicPlayer : boost::noncopyable { }; ServicesView &_services; + std::function(const std::string &)> _loadMusicHandover; // Tables @@ -203,8 +220,12 @@ class AreaMusicPlayer : boost::noncopyable { bool isMusicPlaying(); /** Whether the ambient loop sounds, a fading loop until its fade ends. */ bool isAmbientPlaying(); - /** Plays the music track \p resRef; false when it does not play. */ - bool playTrack(const std::string &resRef); + /** + * Plays the music track \p resRef; false when it does not play. + * @param takeLoadMusic whether the playing load-screen music of the same + * track is taken over instead, as the background music does + */ + bool playTrack(const std::string &resRef, bool takeLoadMusic = false); /** Fades out the music that is playing. */ void fadeAndStop(); /** Plays one of the track's stingers at random, once the area has been arrived in. */ diff --git a/include/reone/game/combat.h b/include/reone/game/combat.h index edd9980c0..c885ddb55 100644 --- a/include/reone/game/combat.h +++ b/include/reone/game/combat.h @@ -304,8 +304,8 @@ class Combat { * round: a round the partner began, or one of its own, ends. A round the * two share starts again from the beginning for both; a partner whose * round was its own gets a side, with no command yet, in the attacker's - * round, held for the attack's pause. An engaged partner owning no round - * is held for the attack's pause. + * round, held for the attack's pause and free to act as soon as that hold + * ends. An engaged partner owning no round is held for the attack's pause. */ bool engagePhysicalTarget(const Creature &attacker, Object &target, int pauseMilliseconds); /** The round the action runs in is engaged. */ @@ -319,6 +319,8 @@ class Combat { bool awaitsRoundEnd(const Creature &actor, std::optional> self) const; /** Whether the creature has a command in a round that has not ended. */ bool ownsRound(const Creature &actor) const; + /** The action carries out its creature's side of a round that has not ended. */ + bool runsRound(const Action &action) const; /** The creature owns a round that no action still waiting in its queue belongs to. */ bool ownsDetachedRound(const Creature &actor) const; /** The creature takes part in an engaged (paired) round. */ diff --git a/include/reone/game/game.h b/include/reone/game/game.h index 54ef2ea34..6e27e28c7 100644 --- a/include/reone/game/game.h +++ b/include/reone/game/game.h @@ -259,6 +259,11 @@ class Game : boost::noncopyable { * that one is due. */ bool consumeTimingDiscontinuity(); + /** + * The world waits while the window is out of focus: none of that time + * passes in it, and its clock takes up again from the next update. + */ + void holdWorldClock() { _worldClockSample.reset(); } /** * Play a movie, ending any module transition movies. @@ -464,6 +469,8 @@ class Game : boost::noncopyable { void openMainMenu(); void openInGame(); + /** Covers the screen with \p color at opacity \p alpha, over what is drawn so far. */ + void renderScreenFill(const glm::vec3 &color, float alpha); bool hasPlayableRuntimeSession() const { return _runtimeSessionPlayable; } @@ -554,6 +561,20 @@ class Game : boost::noncopyable { // END KotOR I Pazaak lifecycle void startCharacterGeneration(); + + /** + * Sets the music the next loading screens play to the one the + * loading-screen table gives the module named \p moduleName. + */ + void setLoadMusicForModule(const std::string &moduleName); + /** Stops the loading-screen music; the track stays set for the next loading screen. */ + void stopLoadMusic(); + /** + * Fades the menu music out for a fixed wait and then stops it, holding + * everything else meanwhile, as starting a new game or loading a save + * from the main menu does. + */ + void fadeOutMenuMusic(); void startDialog(const std::shared_ptr &owner, const std::string &resRef, GlobalFade::DialogTicket admission = {}, const std::shared_ptr &listener = nullptr); @@ -584,7 +605,8 @@ class Game : boost::noncopyable { StatusSummaryAccumulator &statusSummary() { return _statusSummary; } /** * A status summary was taken down or passed by unseen. TSL then looks for - * the story's reaction to the player character's alignment. + * the story's reaction to the player character's alignment, and, the first + * time the player's own character can level up, runs the level-up dialog. */ void finishStatusSummaryCycle(); @@ -607,6 +629,11 @@ class Game : boost::noncopyable { return _screen == Screen::Conversation; } + /** A conversation owns the screen, and its dialog is an animated cutscene. */ + bool isAnimatedCutsceneShowing() const { + return isConversationActive() && _conversation && _conversation->isAnimatedCutscene(); + } + /** The object is the running dialogue's current speaker or listener. */ bool isConversationSpeakerOrListener(const Object &object) const { return isConversationActive() && _conversation == _dialog.get() && _dialog->isSpeakerOrListener(object); @@ -628,6 +655,11 @@ class Game : boost::noncopyable { return _pointer->type(); } + /** Where the pointer last moved to, in screen coordinates. */ + glm::ivec2 pointerPosition() const { + return _pointer->position(); + } + bool relativeMouseMode() const; // Module loading @@ -671,6 +703,8 @@ class Game : boost::noncopyable { std::string startWaypoint; uint32_t pauseDay {0}; uint32_t pauseTime {0}; + /** The loading-screen music the save asks for. */ + std::string loadMusic; }; std::unique_ptr session; @@ -1858,17 +1892,11 @@ class Game : boost::noncopyable { bool _showEndGameGui {true}; uint32_t _lastPartyMemberTempKilled {script::kObjectInvalid}; /** - * How fast the world, its presentation and the fade run during the death - * sequence. The first title slows play from full speed to a fifth over - * the first four seconds of real time; its time and rate carry over to - * later deaths for the program's lifetime. The second title runs at a quarter - * while its gameover panel is up. + * How fast the world, its presentation and the fade run: a quarter while + * the second title's gameover panel is up, else full speed. The first + * title's death sequence keeps full speed. */ float _deathTimeScale {1.0f}; - uint32_t _deathSequenceSample {0}; - float _deathSequenceSeconds {0.0f}; - float _deathSlowRate {0.0f}; - bool _deathSequenceStarting {true}; std::unique_ptr _container; std::unique_ptr _partySelect; std::unique_ptr _saveLoad; @@ -1920,6 +1948,16 @@ class Game : boost::noncopyable { std::string _musicResRef; std::shared_ptr _music; AreaMusicPlayer _areaMusic; + /** + * The music loading screens play. It is set by a new game, a save load + * and the module picker, and kept from one load, and one session, to the + * next. + */ + std::string _loadMusicResRef; + /** The loading-screen music that is playing, or paused under a movie. */ + std::shared_ptr _loadMusic; + /** The coming module load follows transition movies, and plays no loading-screen music. */ + bool _loadComesWithMovies {false}; // END Audio @@ -1946,6 +1984,9 @@ class Game : boost::noncopyable { std::optional> subject, bool hideActions, bool &hidden) const; + /** TSL: runs the level-up dialog script the first time only. */ + void runLevelUpDialogOnce(); + void advanceWorldTime(double dt); void advanceTimeStopClock(double dt); void advancePlayedTime(float dt); @@ -2044,6 +2085,10 @@ class Game : boost::noncopyable { bool loadParty(); void loadNextModule(); void playMusic(const std::string &resRef); + /** Starts, or resumes, the loading-screen music, as a loading screen does when it is shown. */ + void startLoadMusic(); + /** Hands the playing loading-screen music over to the background music when it asks for the same track. */ + std::shared_ptr takeLoadMusic(const std::string &resRef); void toggleInGameCameraType(); void enterFreeLook(); void exitFreeLook(); diff --git a/include/reone/game/gui/chargen/abilities.h b/include/reone/game/gui/chargen/abilities.h index e95669d12..d6411777b 100644 --- a/include/reone/game/gui/chargen/abilities.h +++ b/include/reone/game/gui/chargen/abilities.h @@ -158,7 +158,8 @@ class CharGenAbilities : public GameGUI { int getPointCost(Ability ability) const; - void onAbilityLabelSelectionChanged(Ability ability, bool selected); + /** Pulses the row's name and points while the row is hovered, and describes its ability. */ + void onAbilityLabelSelectionChanged(Ability ability, gui::Label &label, gui::Button &points, bool selected); void onMinusButtonClick(Ability ability); void onPlusButtonClick(Ability ability); }; diff --git a/include/reone/game/gui/chargen/custom.h b/include/reone/game/gui/chargen/custom.h index e095d2bc2..ac408e658 100644 --- a/include/reone/game/gui/chargen/custom.h +++ b/include/reone/game/gui/chargen/custom.h @@ -53,6 +53,12 @@ class CustomCharacterGeneration : public GameGUI { void setStep(int step); + /** + * Shows the current step as current again, its number and name pulsing, + * as when the panel returns from one of the steps' screens. + */ + void refreshStep() { doSetStep(_step); } + private: struct Controls { std::shared_ptr BTN_BACK; diff --git a/include/reone/game/gui/chargen/levelup.h b/include/reone/game/gui/chargen/levelup.h index 67fbe8193..4b4616d20 100644 --- a/include/reone/game/gui/chargen/levelup.h +++ b/include/reone/game/gui/chargen/levelup.h @@ -53,6 +53,12 @@ class LevelUpMenu : public GameGUI { void setStep(int step); + /** + * Shows the current step as current again, its number and name pulsing, + * as when the panel returns from one of the steps' screens. + */ + void refreshStep() { doSetStep(_step); } + /** Whether the coming level offers a choice of feats. */ bool hasFeatChoices() const; /** Whether the coming level offers a choice of powers. */ diff --git a/include/reone/game/gui/chargen/quick.h b/include/reone/game/gui/chargen/quick.h index fde12ffa5..5ff46b859 100644 --- a/include/reone/game/gui/chargen/quick.h +++ b/include/reone/game/gui/chargen/quick.h @@ -53,6 +53,12 @@ class QuickCharacterGeneration : public GameGUI { void setStep(int step); + /** + * Shows the current step as current again, its number and name pulsing, + * as when the panel returns from one of the steps' screens. + */ + void refreshStep() { doSetStep(_step); } + private: struct Controls { std::shared_ptr BTN_BACK; diff --git a/include/reone/game/gui/chargen/skills.h b/include/reone/game/gui/chargen/skills.h index c86e0d73a..83065131f 100644 --- a/include/reone/game/gui/chargen/skills.h +++ b/include/reone/game/gui/chargen/skills.h @@ -170,7 +170,8 @@ class CharGenSkills : public GameGUI { void onMinusButtonClick(SkillType skill); void onPlusButtonClick(SkillType skill); - void onSkillLabelSelectionChanged(SkillType skill, bool selected); + /** Pulses the row's name and points while the row is hovered, and describes its skill. */ + void onSkillLabelSelectionChanged(SkillType skill, gui::Label &label, gui::Button &points, bool selected); }; } // namespace game diff --git a/include/reone/game/gui/computer.h b/include/reone/game/gui/computer.h index a8320b645..36b6c5c81 100644 --- a/include/reone/game/gui/computer.h +++ b/include/reone/game/gui/computer.h @@ -17,6 +17,7 @@ #pragma once +#include "reone/audio/clip.h" #include "reone/gui/control/label.h" #include "reone/gui/control/listbox.h" @@ -31,7 +32,7 @@ class ComputerGUI : public Conversation { public: ComputerGUI(Game &game, ServicesView &services) : Conversation(game, services), - _cameraGUI(game, services, [this]() { returnFromCamera(); }) { + _cameraGUI(game, services) { _resRef = guiResRef("computer"); } @@ -47,15 +48,28 @@ class ComputerGUI : public Conversation { void onEntryEnded() override; private: + /** What the conversation shows: nothing yet, the panel, or a camera shot. */ enum class Presentation { + Hidden, Normal, Camera }; ComputerCamGUI _cameraGUI; - Presentation _presentation {Presentation::Normal}; - - void returnFromCamera(); + Presentation _presentation {Presentation::Hidden}; + /** The terminal hum, looping while the panel is up. */ + std::shared_ptr _humClip; + /** The playing hum; none while the panel is down or a camera shot shows. */ + std::shared_ptr _hum; + /** KotOR: the computer type the panel was last framed for, which numbers the backdrop. */ + int _computerType {0}; + + /** Switches between the panel and a camera shot; the terminal hum starts when the panel shows. */ + void setPresentation(Presentation presentation); + /** Starts the terminal hum from its beginning. */ + void playHum(); + /** Stops the terminal hum. */ + void stopHum(); struct Controls { std::shared_ptr LBL_BAR1; @@ -98,32 +112,44 @@ class ComputerGUI : public Conversation { _controls.LBL_BAR5 = findControl("LBL_BAR5"); _controls.LBL_BAR6 = findControl("LBL_BAR6"); _controls.LBL_COMP_SKILL = findControl("LBL_COMP_SKILL"); - _controls.LBL_COMP_SKILL_ICON = findControl("LBL_COMP_SKILL_ICON"); _controls.LBL_COMP_SKILL_VAL = findControl("LBL_COMP_SKILL_VAL"); _controls.LBL_COMP_SPIKES = findControl("LBL_COMP_SPIKES"); - _controls.LBL_COMP_SPIKES_ICON = findControl("LBL_COMP_SPIKES_ICON"); _controls.LBL_COMP_SPIKES_VAL = findControl("LBL_COMP_SPIKES_VAL"); - _controls.LBL_OBSCURE = findControl("LBL_OBSCURE"); _controls.LBL_REP_SKILL = findControl("LBL_REP_SKILL"); - _controls.LBL_REP_SKILL_ICON = findControl("LBL_REP_SKILL_ICON"); _controls.LBL_REP_SKILL_VAL = findControl("LBL_REP_SKILL_VAL"); _controls.LBL_REP_UNITS = findControl("LBL_REP_UNITS"); - _controls.LBL_REP_UNITS_ICON = findControl("LBL_REP_UNITS_ICON"); _controls.LBL_REP_UNITS_VAL = findControl("LBL_REP_UNITS_VAL"); - _controls.LBL_STATIC1 = findControl("LBL_STATIC1"); - _controls.LBL_STATIC2 = findControl("LBL_STATIC2"); - _controls.LBL_STATIC3 = findControl("LBL_STATIC3"); - _controls.LBL_STATIC4 = findControl("LBL_STATIC4"); _controls.LB_MESSAGE = findControl("LB_MESSAGE"); _controls.LB_REPLIES = findControl("LB_REPLIES"); + if (!isTSL()) { + // Only the KotOR terminal sets up the skill icons, the obscuring + // label and the static bands. + _controls.LBL_COMP_SKILL_ICON = findControl("LBL_COMP_SKILL_ICON"); + _controls.LBL_COMP_SPIKES_ICON = findControl("LBL_COMP_SPIKES_ICON"); + _controls.LBL_OBSCURE = findControl("LBL_OBSCURE"); + _controls.LBL_REP_SKILL_ICON = findControl("LBL_REP_SKILL_ICON"); + _controls.LBL_REP_UNITS_ICON = findControl("LBL_REP_UNITS_ICON"); + _controls.LBL_STATIC1 = findControl("LBL_STATIC1"); + _controls.LBL_STATIC2 = findControl("LBL_STATIC2"); + _controls.LBL_STATIC3 = findControl("LBL_STATIC3"); + _controls.LBL_STATIC4 = findControl("LBL_STATIC4"); + } } void configureMessage(); void configureReplies(); - void hideStaticBands(); + /** Names the two skills, and in KotOR shows their icons. */ + void configureSkillLabels(); + /** KotOR draws the static bands faintly. */ + void configureBands(); + /** KotOR frames the panel by the conversation's computer type, read from comptypes.2da. */ + void applyPanelFrame(); + /** Writes the leader's Computer Use and Repair totals and the party's spike and repair-part counts. */ + void refreshSkills(); void setMessage(std::string message) override; void setReplyLines(std::vector lines) override; + std::shared_ptr replyList() const override; }; } // namespace game diff --git a/include/reone/game/gui/computercam.h b/include/reone/game/gui/computercam.h index 7611f88c1..827fa4893 100644 --- a/include/reone/game/gui/computercam.h +++ b/include/reone/game/gui/computercam.h @@ -18,15 +18,11 @@ namespace game { // Transparent presentation only; the owning computer conversation drives it. class ComputerCamGUI : public GameGUI { public: - ComputerCamGUI(Game &game, ServicesView &services, std::function onReturn); - - bool handle(const input::Event &event) override; + /** The overlay of a computer conversation's camera shot; it has no input of its own. */ + ComputerCamGUI(Game &game, ServicesView &services); private: - std::function _onReturn; - void preload(gui::IGUI &gui) override; - void onGUILoaded() override; }; } // namespace game diff --git a/include/reone/game/gui/conversation.h b/include/reone/game/gui/conversation.h index f931c6794..68f963d96 100644 --- a/include/reone/game/gui/conversation.h +++ b/include/reone/game/gui/conversation.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include "reone/audio/source.h" @@ -33,6 +34,12 @@ namespace reone { +namespace gui { + +class ListBox; + +} + namespace game { class Conversation : public GameGUI { @@ -75,6 +82,9 @@ class Conversation : public GameGUI { void setAutoSkip(AutoSkip *skip) { _autoSkip = skip; } + /** The conversation's dialog is an animated cutscene. */ + bool isAnimatedCutscene() const { return _dialog && _dialog->isAnimatedCutscene(); } + protected: std::shared_ptr owner() const { return _owner.resolve(); } std::shared_ptr _dialog; @@ -116,6 +126,30 @@ class Conversation : public GameGUI { // Complete the active entry's presentation using the same path as expiry. void endCurrentEntry(); + /** + * Asks to skip the shown entry, paused or not. The next update that runs + * the conversation honours it, if the entry can be skipped then. + */ + void requestSkip(); + /** + * Whether a press asks for a skip: while the entry shows, and while a + * reply taken without a choice holds the next entry back. + */ + bool acceptsSkip() const; + /** As the replies are listed, selects the row under the pointer, if any. */ + void selectReplyUnderPointer(); + + /** + * The conversation's own input: a left button press or Return or keypad + * Enter asks to skip the shown entry. With the replies listed, Return or + * keypad Enter chooses the selected reply, a number key the reply of its + * number, and the up and down arrows move the selection. + * + * @return whether \p event was used up + */ + bool handleConversationInput(const input::Event &event); + /** The list the replies are listed in; none in a presentation without one. */ + virtual std::shared_ptr replyList() const; /** One line per reply, in reply order; an empty line stands for a reply that is not listed. */ virtual void setReplyLines(std::vector lines) = 0; @@ -129,14 +163,46 @@ class Conversation : public GameGUI { virtual void onEntryEnded(); /** The player chose \p reply from the listed replies; called before the reply's scripts run. */ virtual void onReplyPicked(const resource::Dialog::EntryReply &reply); + /** A reply was taken without a choice, as the entry ended; called before the reply's scripts run. */ + virtual void onReplyTaken(const resource::Dialog::EntryReply &reply); + /** Whether a participant of the list waited for still shows a dialog animation. */ + virtual bool isDialogAnimationPlaying() const; + + /** + * The dialogue letterbox's fade: a colour over the whole screen, which an + * entry sets and the arrival of its replies clears. It runs only while + * the letterbox is drawn. + */ + struct EntryFade { + glm::vec3 color {0.0f}; + float alpha {0.0f}; + float delay {0.0f}; + float length {0.0f}; + /** Seconds since the fade was set, counted from a tenth of a second; 0 without a fade. */ + float clock {0.0f}; + /** The fade goes from the colour to clear, rather than from clear to the colour. */ + bool fromColor {false}; + }; + + const EntryFade &entryFade() const { return _entryFade; } + /** Runs the letterbox fade on by \p dt seconds, as the letterbox is drawn. */ + void advanceEntryFade(float dt); + /** Clears the letterbox fade. */ + void resetEntryFade(); private: std::shared_ptr _currentVoice; Timer _endEntryTimer; - float _entryDuration {0.0f}; + /** What the current entry, or the reply taken without a choice, waits for past its delay. */ + int _waitFlags {0}; + /** The entry that follows a reply taken without a choice, once the reply's delay and waits are over. */ + std::optional _pendingEntry; + /** A skip of the shown entry has been asked for and not yet honoured. */ + bool _entrySkipped {false}; + EntryFade _entryFade; std::vector _replies; bool _autoPickFirstReply {false}; - /** Whether the conversation has shown the entry it ends on. */ + /** Whether the conversation has listed the replies of the entry it ends on. */ bool _endReached {false}; AutoSkip *_autoSkip {nullptr}; GlobalFade::DialogTicket _fadeDialog; @@ -144,12 +210,28 @@ class Conversation : public GameGUI { /** The objects this conversation has made its participants. */ std::vector> _dialogParticipants; + /** + * A walk through a cycle of values, which starts where the dialog's name + * puts it and moves by a step in one direction. + */ + struct DrawCycle { + static constexpr int kLength = 19; + using Values = std::array; + + int place {0}; + int step {1}; + bool forward {false}; + + /** Starts the walk where \p seed puts it. */ + void seed(uint32_t seed); + /** The value at the walk's place in \p values; the walk then moves on. */ + int draw(const Values &values); + }; + // Camera shots - /** The place in the cycle of camera angles, and the step and direction it moves by. */ - int _angleCyclePlace {0}; - int _angleCycleStep {1}; - bool _angleCycleForward {false}; + /** The walk through the cycle of camera angles. */ + DrawCycle _angleCycle; /** No entry, or no list of replies, has been given a camera angle yet. */ bool _firstEntryShot {true}; bool _firstReplyShot {true}; @@ -166,13 +248,18 @@ class Conversation : public GameGUI { /** The view is an animated shot, until another shot is taken. */ bool _animatedView {false}; - /** Starts the cycle of camera angles where the dialog's name puts it. */ + /** Starts the cycles of camera angles and sound variations where the dialog's name puts them. */ void seedCameraAngles(); /** The next angle of the cycle, for an entry or reply without one. */ int drawCameraAngle(); // END Camera shots + /** The walk through the cycle of sound variation numbers. */ + DrawCycle _soundVariationCycle; + /** The sounds of the entries by index that took a variation number, once shown. */ + std::map _entrySounds; + /** Make \p object a participant of this conversation, unless another conversation still holds it. */ void attachDialogParticipant(const std::shared_ptr &object); /** Release the participants that \p owner's conversation still holds. */ @@ -182,18 +269,31 @@ class Conversation : public GameGUI { bool isCurrent(uint64_t generation) const; - void loadConversationBackground(); void loadCameraModel(); void loadStartEntry(); - void loadVoiceOver(); + /** Plays the current entry's voice: \p sound, or else its voice-over. */ + void loadVoiceOver(const std::string &sound); void scheduleEndOfEntry(); + /** Holds the next entry back after \p reply, taken without a choice. */ + void scheduleTakenReply(const resource::Dialog::EntryReply &reply); + /** Whether something the current entry or the taken reply waits for still goes on. */ + bool isWaiting() const; + /** Stops the conversation's voice. */ + void stopVoice(); + /** Sets the letterbox fade of \p entry, if it has one. */ + void startEntryFade(const resource::Dialog::EntryReply &entry); + /** Whether the letterbox fade is over, or none is set. */ + bool isEntryFadeDone() const; + /** Whether the conversation's animated camera still plays its clip. */ + bool isCameraAnimationPlaying() const; void loadReplies(); bool isSkippableEntry() const; - bool isNonPresentationalEntry() const; /** Add the shown entry's \p text to the dialogue log under its speaker's name. */ void logEntryLine(const std::string &text); + /** How many of the replies are listed, and so can be taken. */ + size_t listedReplyCount() const; void refreshReplies(); /** @@ -227,7 +327,12 @@ class Conversation : public GameGUI { // Event handlers bool handleMouseButtonDown(const input::MouseButtonEvent &event); - bool handleKeyUp(const input::KeyEvent &event); + /** Return, keypad Enter, the arrows and the number keys, on their press. */ + bool handleKeyDown(const input::KeyEvent &event); + /** Chooses the reply the reply list has selected, if it has one. */ + void pickSelectedReply(); + /** Moves the reply list's selection by \p step rows. */ + void moveReplySelection(int step); // END Event handlers }; diff --git a/include/reone/game/gui/dialog.h b/include/reone/game/gui/dialog.h index 6949a8393..a9e648722 100644 --- a/include/reone/game/gui/dialog.h +++ b/include/reone/game/gui/dialog.h @@ -42,6 +42,8 @@ class DialogGUI : public Conversation { } void update(float dt) override; + /** Draws the letterbox fade under the conversation's panel. */ + void render() override; /** Selects, but does not activate, a reply for a scripted visual capture. */ void selectReplyForCapture(int index); @@ -96,6 +98,8 @@ class DialogGUI : public Conversation { /** The previous list's animation per participant, so unchanged ones are left alone. */ std::map _previousAnimations; + /** The participants whose dialog animations the current node waits for, in list order. */ + std::vector _waitedParticipants; /** The animations of the list before the previous one, whose participants the end of the conversation releases. */ std::map _lastButOneAnimations; /** The pull-back of the face shots in a row, while one is set. */ @@ -181,7 +185,11 @@ class DialogGUI : public Conversation { void applyDialogAnimation(const std::string &participant, int ordinal); /** Mixed-stunt participants that \p node does not drive from their stunt model leave their staging. */ void restoreInactiveStuntParticipants(const resource::Dialog::EntryReply &node); - bool enterMixedStunt(Participant &participant, const std::shared_ptr &animation, bool looping); + /** + * Stages a mixed-stunt participant in \p animation, a loop or a one-shot, + * shown as animation \p animationId. + */ + bool enterMixedStunt(Participant &participant, const std::shared_ptr &animation, bool looping, int animationId); void leaveMixedStunt(Participant &participant); static std::optional decodeCutAnimation(int ordinal); @@ -202,12 +210,16 @@ class DialogGUI : public Conversation { void setMessage(std::string message) override; void setReplyLines(std::vector lines) override; + std::shared_ptr replyList() const override; void onStart() override; void onFinish() override; void onLoadEntry() override; void onEntryEnded() override; void onReplyPicked(const resource::Dialog::EntryReply &reply) override; + void onReplyTaken(const resource::Dialog::EntryReply &reply) override; + /** A creature named by the last list of animations still shows a dialog animation. */ + bool isDialogAnimationPlaying() const override; // Loading diff --git a/include/reone/game/gui/ingame/inventory.h b/include/reone/game/gui/ingame/inventory.h index a3ebfb2ea..87d55036b 100644 --- a/include/reone/game/gui/ingame/inventory.h +++ b/include/reone/game/gui/ingame/inventory.h @@ -61,6 +61,8 @@ class InventoryMenu : public PresentationGUI { std::shared_ptr BTN_ARMOR; std::shared_ptr BTN_CHANGE1; std::shared_ptr BTN_CHANGE2; + std::shared_ptr BTN_CHARLEFT; + std::shared_ptr BTN_CHARRIGHT; std::shared_ptr BTN_DATAPADS; std::shared_ptr BTN_EXIT; std::shared_ptr BTN_MISC; @@ -119,6 +121,8 @@ class InventoryMenu : public PresentationGUI { _controls.BTN_ARMOR = findControl("BTN_ARMOR"); _controls.BTN_CHANGE1 = findControl("BTN_CHANGE1"); _controls.BTN_CHANGE2 = findControl("BTN_CHANGE2"); + _controls.BTN_CHARLEFT = findControl("BTN_CHARLEFT"); + _controls.BTN_CHARRIGHT = findControl("BTN_CHARRIGHT"); _controls.BTN_DATAPADS = findControl("BTN_DATAPADS"); _controls.BTN_EXIT = findControl("BTN_EXIT"); _controls.BTN_MISC = findControl("BTN_MISC"); diff --git a/include/reone/game/gui/loadscreen.h b/include/reone/game/gui/loadscreen.h index bac7cd422..867d0d99b 100644 --- a/include/reone/game/gui/loadscreen.h +++ b/include/reone/game/gui/loadscreen.h @@ -43,6 +43,12 @@ class LoadingScreen : public GameGUI { * there is one, else the one the table's default row names. */ void setModuleImage(const std::string &moduleName); + /** + * The music the loading screen of the module named \p moduleName plays: + * the one its row of the loading-screen table names, else its own load_ + * track when there is one, else the one the table's default row names. + */ + std::string musicResRef(const std::string &moduleName) const; void setProgress(int progress); private: diff --git a/include/reone/game/gui/map.h b/include/reone/game/gui/map.h index 5152a7f2f..0d6eba84a 100644 --- a/include/reone/game/gui/map.h +++ b/include/reone/game/gui/map.h @@ -51,12 +51,14 @@ class Map { /** * @param scale the factor the owning GUI draws its layout at. + * @param pulsingAlpha the owning GUI's pulsing alpha, which the party + * leader's arrow is drawn at on the map screen. * * The map and its markers are drawn into a control's rect but not by the * control, so nothing else carries the layout scale to them: the frame * shrank with the GUI while the map inside it stayed unscaled. */ - void render(Mode mode, const glm::vec4 &bounds, float scale); + void render(Mode mode, const glm::vec4 &bounds, float scale, float pulsingAlpha); bool isLoaded() const { return static_cast(_areaTexture); } @@ -83,7 +85,7 @@ class Map { void loadTextures(const std::string &area); void renderArea(Mode mode, const glm::vec4 &bounds, float scale); - void renderPartyLeader(Mode mode, const glm::vec4 &bounds, float scale); + void renderPartyLeader(Mode mode, const glm::vec4 &bounds, float scale, float pulsingAlpha); void renderNotes(Mode mode, const glm::vec4 &bounds, float scale); glm::vec2 getMapPosition(const glm::vec2 &world) const; diff --git a/include/reone/game/object.h b/include/reone/game/object.h index 1f310d536..95b8b70ce 100644 --- a/include/reone/game/object.h +++ b/include/reone/game/object.h @@ -368,6 +368,15 @@ class Object : public scene::IUser, boost::noncopyable { */ void setPlacedAtOrigin(bool placed); + /** + * Whether the object takes no other animation: while an animated + * cutscene is showing, an object playing an animation authored in world + * space keeps it until the conversation's animation list releases it. + */ + bool holdsCutsceneAnimation() const; + /** The conversation's animation list releases the object's world-space animation. */ + void releaseCutsceneAnimation() { _playingCutsceneAnimation = false; } + // END Stunt mode // Hit Points @@ -531,6 +540,12 @@ class Object : public scene::IUser, boost::noncopyable { const resource::Gff &gff, const SerializedIdentityContext &identityContext, SaveRecordOrigin origin = {}); + /** + * Keep the record an object placed from a blueprint was made from: the + * blueprint's fields, with those the placement gives in their place. A + * save writes the object over it. + */ + void captureAuthoredRecord(const resource::Gff &blueprint, const resource::Gff &placement); const std::optional &saveRecordProvenance() const { return _saveRecordProvenance; } @@ -730,6 +745,8 @@ class Object : public scene::IUser, boost::noncopyable { bool _stunt {false}; /** Stunt mode was entered for a world-space animation and ends with it. */ bool _placedAtOrigin {false}; + /** The object last took an animation authored in world space, and no animation list has released it. */ + bool _playingCutsceneAnimation {false}; std::string _activeAnimName; std::string _feedbackText; float _feedbackTextRemaining {0.0f}; diff --git a/include/reone/game/object/camera/animated.h b/include/reone/game/object/camera/animated.h index 4099f56b8..e7cf32f6d 100644 --- a/include/reone/game/object/camera/animated.h +++ b/include/reone/game/object/camera/animated.h @@ -61,13 +61,16 @@ class AnimatedCamera : public Camera { */ static std::string getShotClipName(int ordinal); - bool isAnimationFinished() const; + /** Whether the clip the last request named is playing; a request whose clip is missing plays nothing. */ + bool isRequestedClipPlaying() const; void setModel(std::shared_ptr model); void setFieldOfView(float fovy); private: std::shared_ptr _model; + /** The clip the last request named; empty when it named none. */ + std::string _requestedClip; float _fovy {kDefaultAnimCamFOV}; float projectionFovy() const override; diff --git a/include/reone/game/object/creature.h b/include/reone/game/object/creature.h index 32d1ce784..0a02de450 100644 --- a/include/reone/game/object/creature.h +++ b/include/reone/game/object/creature.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -330,6 +331,8 @@ class Creature : public Object, public scene::IAnimationEventListener { public: /** The Fury damage bonus of a creature that is not in a Fury. */ static constexpr int kNoFuryDamageBonus = -1; + /** The most experience a creature holds; an addition past it is refused. */ + static constexpr int kMaxExperience = std::numeric_limits::max(); // AI state @@ -571,9 +574,11 @@ class Creature : public Object, public scene::IAnimationEventListener { /** * Adds experience. With two or more classes, every class other than the * favoured one that is two or more levels below the highest of those - * classes takes a fifth of the amount, rounded down; the amount counts as - * unsigned. A race without a favoured class favours the highest-level - * class, the earliest on a tie. + * classes takes a fifth of the amount, rounded down. A race without a + * favoured class favours the highest-level class, the earliest on a tie; + * five or more such classes take the whole amount. Experience only + * grows: a negative amount, or a positive one that would take it past + * kMaxExperience, changes nothing. */ void giveXP(int amount); void setXP(int xp); @@ -771,21 +776,35 @@ class Creature : public Object, public scene::IAnimationEventListener { * nor a party member at zero vitality. */ bool clicksTriggerInReach(const Object &target) const; - /** Enough experience for the next level, below the title's maximum, neither dead nor downed. */ + /** + * Enough experience for the next level by the experience table, below the + * title's maximum, neither dead nor downed. + */ bool isLevelUpPending() const; /** The level-up chime may still sound for the creature: from its creation, and again after each level-up. */ bool isLevelUpChimeArmed() const { return _levelUpChimeArmed; } void setLevelUpChimeArmed(bool armed) { _levelUpChimeArmed = armed; } - /** The level the creature's experience reaches, up to the title's maximum. */ + /** + * The level the creature's experience reaches by the experience table, up + * to the title's maximum, for a creature whose IsPC is exactly 1; any + * other creature's level, as getSpellLevel(false) gives it. + */ int potentialLevel() const; glm::vec3 getSelectablePosition() const override; - int getNeededXP() const; /** - * The experience the creature lacks for its next level: KotOR counts from - * the level its experience reaches, TSL from the levels it has taken, - * scaled as an autobalanced creature's. At the title's maximum level, the - * unreachable amount less the experience held, which is negative. + * The experience table's amount at the creature's level, up to and + * including the title's maximum level; none past it. The maximum level's + * row normally holds 0xFFFFFFFF, the amount no experience reaches. + */ + std::optional getNeededXP() const; + /** + * The experience the creature lacks for its next level, from the + * experience table: KotOR up to the first row its experience falls short + * of, counted from the first row; TSL from the levels it has taken, + * scaled as an autobalanced creature's, a level past the table reading + * the maximum level's row. The difference wraps as 32 bits, so it is + * negative when the creature has more, as at the maximum level. */ int experienceToNextLevel() const; @@ -945,7 +964,22 @@ class Creature : public Object, public scene::IAnimationEventListener { /** The creature scripts name for this one to heal. It is not saved. */ uint32_t healTarget() const { return _healTarget; } void setHealTarget(uint32_t target) { _healTarget = target; } - bool isPC() const { return _isPC; } + /** + * Whether the creature's record marks it a player character: its IsPC is + * not 0. The record's value is saved with the creature. + */ + bool isPC() const { return _isPC != 0; } + /** The record's IsPC as stored, 0 to 255; a player character's is 1. */ + int isPCValue() const { return _isPC; } + /** + * Whether the creature acts as a player character: its own copy of the + * record's IsPC, taken whenever the record is read, cleared when an + * encounter spawns the creature, and set with the record's when control + * of the party changes hands. The copy is not 0. + */ + bool actsAsPC() const { return _actsAsPC != 0; } + /** The creature's own copy of IsPC as it stands, 0 to 255. */ + int actsAsPCValue() const { return _actsAsPC; } /** Whether a click to talk leaves the creature facing the way it was. */ bool isNotReorienting() const { return _notReorienting; } /** The character was made by the player (TSL item limitation). */ @@ -957,10 +991,14 @@ class Creature : public Object, public scene::IAnimationEventListener { void forceEquipClothing(); void setPlayerCreated(bool value) { _playerCreated = value; } /** - * The player-character flag a creature record carries as IsPC. A creature - * whose record does not carry it is a player character. + * Sets the player-character flag, both the record's IsPC and the + * creature's own copy, to 1 or 0. A creature whose record does not carry + * IsPC is a player character. */ - void setPC(bool value) { _isPC = value; } + void setPC(bool value) { + _isPC = value ? 1 : 0; + _actsAsPC = _isPC; + } /** The appearance's race label, which picks droid discharges. */ const std::string &appearanceRace() const { return _appearanceRace; } /** The appearance's blood colour: R, G, Y or none. */ @@ -2329,7 +2367,10 @@ class Creature : public Object, public scene::IAnimationEventListener { bool _disguised {false}; Gender _gender {Gender::Male}; uint16_t _portraitId {0}; - bool _isPC {true}; + /** The record's IsPC byte; any value but 0 marks a player character. */ + uint8_t _isPC {1}; + /** The creature's own copy of IsPC (see actsAsPC()); not saved. */ + uint8_t _actsAsPC {1}; bool _playerCreated {false}; /** Body armour this creature could not wear when it was loaded. */ bool _bodyRefusedOnLoad {false}; @@ -2385,7 +2426,7 @@ class Creature : public Object, public scene::IAnimationEventListener { bool _leftEncounter {false}; uint32_t _orientationLock {script::kObjectInvalid}; bool _engagedExchange {false}; - uint32_t _xp {0}; + int _xp {0}; bool _levelUpChimeArmed {true}; int _joiningXP {0}; CombatForm _currentForm {CombatForm::None}; diff --git a/include/reone/game/presentationgui.h b/include/reone/game/presentationgui.h index 570e35a17..b2b0d94ef 100644 --- a/include/reone/game/presentationgui.h +++ b/include/reone/game/presentationgui.h @@ -106,7 +106,13 @@ class PresentationGUI : public gui::IGUIEventListener, boost::noncopyable { virtual void preload(gui::IGUI &gui); virtual void onGUILoaded() {} - void loadBackground(BackgroundType type); + /** + * Shows the full-screen backdrop of \p type behind the GUI. + * + * @param computerType for a computer backdrop, the computer type that + * numbers KotOR's backdrop + */ + void loadBackground(BackgroundType type, int computerType = 0); /** Centers the root panel in its authored canvas and reapplies layout. */ void centerRootInCanvas(int canvasWidth, int canvasHeight); @@ -134,6 +140,11 @@ class PresentationGUI : public gui::IGUIEventListener, boost::noncopyable { void fillK2SectionStrip(const std::shared_ptr &topBar, const std::shared_ptr &bottomBar); void useK2ShellTitle(const std::shared_ptr &control); void updateK2FilterButton(const std::shared_ptr &button, bool selected); + /** + * Turns KotOR's right party arrow to point right: the layouts give both + * arrows beside the party portraits the left-pointing art. + */ + void turnK1PartyArrowRight(); virtual void configureControls() {} void onClick(const std::string &control) override; diff --git a/include/reone/game/presentationpointer.h b/include/reone/game/presentationpointer.h index 5db9042ec..1ab202dcf 100644 --- a/include/reone/game/presentationpointer.h +++ b/include/reone/game/presentationpointer.h @@ -35,6 +35,8 @@ class PresentationPointer { void setType(resource::CursorType type); resource::CursorType type() const { return _type; } void setPosition(glm::ivec2 position); + /** Where the pointer last moved to, in screen coordinates. */ + glm::ivec2 position() const { return _position; } void setPressed(bool pressed); void render(const graphics::GraphicsOptions &options, bool relativeMouseMode); @@ -42,6 +44,7 @@ class PresentationPointer { resource::ICursors &_cursors; resource::CursorType _type {resource::CursorType::None}; std::shared_ptr _cursor; + glm::ivec2 _position {0}; }; /** Existing cursor decision for an already selected object; performs no picking. */ diff --git a/include/reone/game/room.h b/include/reone/game/room.h index 8f53b8c66..9963f2972 100644 --- a/include/reone/game/room.h +++ b/include/reone/game/room.h @@ -52,7 +52,8 @@ class Room : public scene::IUser { /** * Whether the room's box holds a point seen from above, edges included. * The box spans the room's meshes, but for animated meshes and background - * geometry; a room without such a mesh holds no point. + * geometry; a room without such a mesh, or whose model has no walkmesh, + * holds no point. */ bool boxHolds(const glm::vec2 &point) const; /** The room's box (see boxHolds). */ diff --git a/include/reone/game/script/runner.h b/include/reone/game/script/runner.h index 4845467c4..93e624916 100644 --- a/include/reone/game/script/runner.h +++ b/include/reone/game/script/runner.h @@ -24,7 +24,10 @@ namespace reone { namespace script { +struct ExecutionContext; + class IRoutines; +class ScriptProgram; } @@ -67,6 +70,17 @@ class ScriptRunner { private: script::IRoutines &_routines; resource::IScripts &_scripts; + + /** The instruction count of the outermost run, shared by the runs it starts. */ + std::shared_ptr _instructionsRun; + /** How many runs are under way, one inside another. */ + int _runDepth {0}; + + /** + * Runs a program. A run started while another is under way shares its + * instruction count; an outermost run starts a new one. + */ + int runProgram(std::shared_ptr program, std::unique_ptr ctx); }; } // namespace game diff --git a/include/reone/game/types.h b/include/reone/game/types.h index 304abf37a..43e31ee19 100644 --- a/include/reone/game/types.h +++ b/include/reone/game/types.h @@ -51,8 +51,7 @@ enum class BackgroundType { None, Menu, Load, - Computer0, - Computer1 + Computer }; enum class WeaponType { diff --git a/include/reone/gui/control.h b/include/reone/gui/control.h index 586e721da..17780782c 100644 --- a/include/reone/gui/control.h +++ b/include/reone/gui/control.h @@ -96,8 +96,14 @@ class Control : boost::noncopyable { std::shared_ptr edge; std::shared_ptr fill; glm::vec3 color {1.0f}; + /** The opacity every piece of the border is drawn with. */ + float alpha {1.0f}; int dimension {0}; FillTransform fillTransform {FillTransform::None}; + /** While set, the corners and edges are drawn at the GUI's pulsing alpha instead of alpha. */ + bool pulseFrame {false}; + /** While set, the fill is drawn at the GUI's pulsing alpha instead of alpha. */ + bool pulseFill {false}; }; struct Text { @@ -106,6 +112,13 @@ class Control : boost::noncopyable { glm::vec3 color {1.0f}; TextAlign align {TextAlign::CenterCenter}; float opacity {1.0f}; + /** The text is drawn at the GUI's pulsing alpha. */ + bool pulsing {false}; + /** + * While the control is selected, the text is drawn at the GUI's pulsing + * alpha; deselecting the control stops the text's pulse. + */ + bool hilightPulsing {false}; }; static ControlType getType(const resource::generated::GUI_BASECONTROL &gui); @@ -166,6 +179,8 @@ class Control : boost::noncopyable { void setBorderFill(std::shared_ptr texture); void setBorderFillTransform(Border::FillTransform transform); void setBorderColor(glm::vec3 color); + /** Sets the opacity every piece of the border is drawn with. */ + void setBorderAlpha(float alpha); void setBorderColorOverride(glm::vec3 color); void setDisabled(bool disabled); virtual void setExtent(Extent extent); @@ -185,6 +200,10 @@ class Control : boost::noncopyable { void setText(Text text); void setTextAlignment(TextAlign align); void setTextColor(glm::vec3 color); + /** Sets whether the text is drawn at the GUI's pulsing alpha. */ + void setTextPulsing(bool pulsing) { _text.pulsing = pulsing; } + /** The hilight's frame and fill pulse, as chosen; a control without a hilight is left alone. */ + void setHilightPulsing(bool frame, bool fill); /** Sets the alpha the text is drawn with. */ void setTextOpacity(float opacity) { _text.opacity = opacity; } void setTextMessage(std::string text); @@ -192,8 +211,6 @@ class Control : boost::noncopyable { void setTextPaddingLeft(int padding) { _textPaddingLeft = padding; } int textPaddingLeft() const; void setTintBorderFill(bool tint) { _tintBorderFill = tint; } - /** Sets the alpha the border's fill is drawn with. */ - void setBorderFillOpacity(float opacity) { _borderFillOpacity = opacity; } void setSharpenBorderFillAlpha(bool sharpen) { _sharpenBorderFillAlpha = sharpen; } void setUseBorderColorOverride(bool use); void setVisible(bool visible); @@ -259,8 +276,9 @@ class Control : boost::noncopyable { bool _selected {false}; bool _hilightOverBorder {false}; bool _selectable {false}; + /** Whether this control is a list's prototype, drawn once for each row. */ + bool _isProtoItem {false}; bool _tintBorderFill {false}; - float _borderFillOpacity {1.0f}; bool _sharpenBorderFillAlpha {false}; glm::vec3 _borderColorOverride {1.0f}; bool _useBorderColorOverride {false}; @@ -297,11 +315,19 @@ class Control : boost::noncopyable { _resourceSvc(resourceSvc) { } + /** + * Draws a border over the given size. + * + * @param part the parts of the border drawn + * @param pulsing whether the whole border is drawn at the GUI's pulsing + * alpha, whatever the border's own pulse + */ void renderBorder(const Border &border, const glm::ivec2 &offset, const glm::ivec2 &size, scene::IRenderPass &pass, - BorderRenderPart part = BorderRenderPart::All); + BorderRenderPart part = BorderRenderPart::All, + bool pulsing = false); void renderScene(const glm::ivec2 &screenSize, const glm::ivec2 &offset, diff --git a/include/reone/gui/control/iconchain.h b/include/reone/gui/control/iconchain.h index 41e799840..ae946d64f 100644 --- a/include/reone/gui/control/iconchain.h +++ b/include/reone/gui/control/iconchain.h @@ -100,7 +100,6 @@ class IconChain : public Control { bool handleMouseMotion(int x, int y) override; bool handleMouseWheel(int x, int y) override; bool handleClick(int x, int y, int clicks = 1) override; - void update(float dt) override; void render(const glm::ivec2 &screenSize, const glm::ivec2 &offset, scene::IRenderPass &pass) override; void setSelected(bool selected) override; @@ -134,7 +133,6 @@ class IconChain : public Control { CellStyle _cellStyle; int _rowOffset {0}; int _focusedItemIndex {-1}; - float _focusedBorderPulsePhase {0.0f}; // Event listeners @@ -162,7 +160,6 @@ class IconChain : public Control { glm::vec3 getItemBorderColor(const Item &item, bool focused) const; glm::vec4 getItemIconColor(const Item &item) const; std::optional getFocusedBorderColor(const Item &item, bool focused) const; - float getFocusedBorderPulseFactor() const; void renderLink( const Link &link, const glm::ivec2 &offset, diff --git a/include/reone/gui/control/imagebutton.h b/include/reone/gui/control/imagebutton.h index 0a13d31bb..8a7613f7d 100644 --- a/include/reone/gui/control/imagebutton.h +++ b/include/reone/gui/control/imagebutton.h @@ -50,7 +50,20 @@ class ImageButton : public Control { const std::shared_ptr &iconFrame, scene::IRenderPass &pass); + /** + * While selected, the icon frame takes this colour (unset, the hilight's) + * and, if pulsing, the GUI's pulsing alpha. + */ + void setIconFrameHilight(std::optional color, bool pulsing) { + _iconFrameHilightColor = std::move(color); + _iconFrameHilightPulsing = pulsing; + } + private: + /** The selected icon frame's colour; unset, the hilight's. */ + std::optional _iconFrameHilightColor; + /** Whether the selected icon frame is drawn at the GUI's pulsing alpha. */ + bool _iconFrameHilightPulsing {false}; std::shared_ptr _iconFrame; std::shared_ptr _iconFont; diff --git a/include/reone/gui/control/listbox.h b/include/reone/gui/control/listbox.h index aa295d323..b37f9db1f 100644 --- a/include/reone/gui/control/listbox.h +++ b/include/reone/gui/control/listbox.h @@ -84,6 +84,11 @@ class ListBox : public Control { void setExtent(Extent extent) override; void setExtentHeight(int height) override; void setSelectionMode(SelectionMode mode); + /** + * Whether a hovered list keeps its selected row when the pointer is over + * no row or leaves the list, rather than losing it. + */ + void setKeepsSelection(bool keeps) { _keepsSelection = keeps; } void setSelectedItemIndex(int index); void setItemsInteractive(bool interactive); void setScrollBarEnabled(bool enabled); @@ -103,6 +108,11 @@ class ListBox : public Control { void setTextLineRows(bool lines); void setRenderItemIconsForButtonProto(bool render); void scrollToBottom(); + /** + * Scroll the least that brings row \p index into view: up to it when it + * is above the visible rows, or down until it is the last visible row. + */ + void scrollToItem(int index); /** * Draws underneath the list's own frame and rows. Lets a GUI repaint @@ -168,6 +178,7 @@ class ListBox : public Control { private: SelectionMode _selectionMode {SelectionMode::OnHover}; + bool _keepsSelection {false}; std::shared_ptr _protoItem; std::shared_ptr _scrollBar; std::vector _items; diff --git a/include/reone/gui/control/progressbar.h b/include/reone/gui/control/progressbar.h index fe2214371..fe8edcf3c 100644 --- a/include/reone/gui/control/progressbar.h +++ b/include/reone/gui/control/progressbar.h @@ -44,13 +44,12 @@ class ProgressBar : public Control { void setValue(int value); private: - struct Progress { - std::shared_ptr fill; - /** Multiplies the fill. TSL authors greyscale bars and colours them here. */ - glm::vec3 color {1.0f}; - }; - - Progress _progress; + /** + * The bar drawn over the border up to the value. Its colour multiplies the + * fill: TSL authors greyscale bars and colours them here. Its fill pulses + * as authored. + */ + std::shared_ptr _progress; int _value {0}; bool _startFromLeft {true}; }; diff --git a/include/reone/gui/gui.h b/include/reone/gui/gui.h index bcaf95884..f310ac850 100644 --- a/include/reone/gui/gui.h +++ b/include/reone/gui/gui.h @@ -49,6 +49,40 @@ class IGUIEventListener { virtual void onSelectionChanged(const std::string &control, bool selected) = 0; }; +/** + * The opacity shared by every pulsing border, image and text: it falls from 1 + * to 0.4 over 0.75 s, rises back over the next 0.75 s, and repeats. + */ +class PulsingAlpha { +public: + /** Advances the pulse by dt seconds. */ + void update(float dt) { + _timeLeft -= dt; + if (_timeLeft <= 0.0f) { + _rising = !_rising; + _timeLeft = kHalfPeriod; + } + const float progress = (kHalfPeriod - _timeLeft) / kHalfPeriod; + _value = _rising ? kLow + (1.0f - kLow) * progress : 1.0f - (1.0f - kLow) * progress; + } + + /** The opacity now, 0.4 to 1. */ + float value() const { return _value; } + +private: + /** Seconds the pulse takes to fall, or to rise. */ + static constexpr float kHalfPeriod = 0.75f; + /** The opacity at the bottom of the pulse. */ + static constexpr float kLow = 0.4f; + + /** The opacity now. */ + float _value {1.0f}; + /** Seconds left before the pulse turns. */ + float _timeLeft {kHalfPeriod}; + /** Whether the opacity is rising; it starts falling from 1. */ + bool _rising {false}; +}; + class IGUI { public: enum class ScalingMode { @@ -101,6 +135,10 @@ class IGUI { virtual float borderScale() const = 0; virtual float listScale() const = 0; virtual bool tintBorderFills() const = 0; + /** Whether an image button's text pulses while the button is selected, as a button's does. */ + virtual bool imageButtonTextPulses() const = 0; + /** The opacity pulsing borders and texts are drawn with now. */ + virtual float pulsingAlpha() const = 0; /** Whether scene-backed controls may render their 3D content. */ virtual bool sceneRenderEnabled() const = 0; @@ -110,6 +148,8 @@ class IGUI { virtual void setScaling(ScalingMode scaling) = 0; virtual void setTextScale(float scale) = 0; virtual void setTintBorderFills(bool tint) = 0; + /** Sets whether the image buttons loaded afterwards pulse their text while selected. */ + virtual void setImageButtonTextPulses(bool pulses) = 0; virtual void setControlScaling(const std::string &tag, ScalingMode scaling) = 0; virtual void setDefaultHilightColor(glm::vec3 color) = 0; virtual void setBackground(std::shared_ptr texture) = 0; @@ -130,12 +170,13 @@ class GUI : public IGUI, boost::noncopyable { graphics::GraphicsOptions &options, scene::ISceneGraphs &sceneGraphs, graphics::GraphicsServices &graphicsSvc, - resource::ResourceServices &resourceSvc) : + resource::ResourceServices &resourceSvc, + const PulsingAlpha &pulsingAlpha) : _options(options), _sceneGraphs(sceneGraphs), _graphicsSvc(graphicsSvc), - _resourceSvc(resourceSvc) { - + _resourceSvc(resourceSvc), + _pulsingAlpha(pulsingAlpha) { } /** Derived from the current resolution, which can change at runtime. */ @@ -197,6 +238,10 @@ class GUI : public IGUI, boost::noncopyable { _tintBorderFills = tint; } + void setImageButtonTextPulses(bool pulses) override { + _imageButtonTextPulses = pulses; + } + float textScale() const override { return _textScale.value_or(_options.guiTextScale); } @@ -220,6 +265,14 @@ class GUI : public IGUI, boost::noncopyable { return _tintBorderFills; } + bool imageButtonTextPulses() const override { + return _imageButtonTextPulses; + } + + float pulsingAlpha() const override { + return _pulsingAlpha.value(); + } + bool sceneRenderEnabled() const override { return _options.sceneRender; } @@ -256,6 +309,7 @@ class GUI : public IGUI, boost::noncopyable { ScalingMode _scaling {ScalingMode::Center}; std::optional _textScale; bool _tintBorderFills {false}; + bool _imageButtonTextPulses {false}; glm::ivec2 _rootOffset {0}; glm::ivec2 _controlOffset {0}; std::shared_ptr _background; @@ -278,6 +332,7 @@ class GUI : public IGUI, boost::noncopyable { scene::ISceneGraphs &_sceneGraphs; graphics::GraphicsServices &_graphicsSvc; resource::ResourceServices &_resourceSvc; + const PulsingAlpha &_pulsingAlpha; // END Services diff --git a/include/reone/gui/guis.h b/include/reone/gui/guis.h index 56a516250..c46217596 100644 --- a/include/reone/gui/guis.h +++ b/include/reone/gui/guis.h @@ -36,6 +36,9 @@ class IGUIs { virtual void clear() = 0; virtual std::shared_ptr get(const std::string &resRef, std::function preload = nullptr) = 0; + + /** Advances the pulse every GUI shares, once a frame, by dt seconds. */ + virtual void updatePulsingAlpha(float dt) = 0; }; class GUIs : public IGUIs, boost::noncopyable { @@ -63,12 +66,18 @@ class GUIs : public IGUIs, boost::noncopyable { return _guis.insert(make_pair(resRef, std::move(gui))).first->second; } + void updatePulsingAlpha(float dt) override { + _pulsingAlpha.update(dt); + } + private: graphics::GraphicsOptions &_graphicsOpt; scene::ISceneGraphs &_sceneGraphs; graphics::GraphicsServices &_graphicsSvc; resource::ResourceServices &_resourceSvc; + /** The pulse every GUI shares; declared before the GUIs that hold it, so it outlives them. */ + PulsingAlpha _pulsingAlpha; std::unordered_map> _guis; std::shared_ptr doGet(std::string resRef, std::function preload); diff --git a/include/reone/input/event.h b/include/reone/input/event.h index 5f55de4b1..31b379913 100644 --- a/include/reone/input/event.h +++ b/include/reone/input/event.h @@ -119,6 +119,7 @@ enum class KeyCode { Delete = '\x7F', End = 77 | (1 << 30), PageDown = 78 | (1 << 30), + KeypadEnter = 88 | (1 << 30), Right = 79 | (1 << 30), Left = 80 | (1 << 30), Down = 81 | (1 << 30), diff --git a/include/reone/movie/movie.h b/include/reone/movie/movie.h index 8688e94ab..f634377f9 100644 --- a/include/reone/movie/movie.h +++ b/include/reone/movie/movie.h @@ -68,7 +68,8 @@ class Movie : public IMovie, boost::noncopyable { void update(float dt) override; void render() override; - void finish() override { _finished = true; } + /** Ends the movie at once, its sound with its picture. */ + void finish() override; bool isFinished() const override { return _finished; } diff --git a/include/reone/resource/dialog.h b/include/reone/resource/dialog.h index b3e9edef5..e6ee9f380 100644 --- a/include/reone/resource/dialog.h +++ b/include/reone/resource/dialog.h @@ -28,6 +28,12 @@ namespace resource { struct Dialog { struct WaitFlags { static constexpr int waitAnimFinish = 1; + /** The node waits for its sound to finish. */ + static constexpr int waitSoundFinish = 2; + /** The node waits for its participants' dialog animations to finish. */ + static constexpr int waitParticipantAnimFinish = 4; + /** The node waits for its fade to finish. */ + static constexpr int waitFadeFinish = 8; }; struct Stunt { @@ -66,6 +72,8 @@ struct Dialog { std::string speaker; std::string text; + /** The talk table string of the node's text, or -1 for none. */ + int textStrRef {-1}; std::string voResRef; std::string script; std::string script2; @@ -77,8 +85,35 @@ struct Dialog { float plotXPPercentage {0.0f}; ActionParams actionParams; ActionParams actionParams2; + /** Seconds the node stays, or -1 for none of its own. */ int delay {0}; int waitFlags {0}; + /** The node's sound is a recorded voice, which times the node by the dialog's entry delay. */ + bool soundExists {false}; + /** + * TSL: the node's sound, or else its voice-over, takes a variation + * number added to its name, as alien speech does. + */ + bool soundTakesVariation {false}; + /** + * TSL: the node is authored as having its variation number already: + * a shown entry adds none, though a barked one-liner still does. + */ + bool soundVariationAdded {false}; + /** + * The node is spoken by its voice-over alone: with subtitles off, the + * cinematic panel does not show its text. + */ + bool voiceOverOnly {false}; + /** TSL: the entry cannot be skipped, even in a skippable conversation. */ + bool nodeUnskippable {false}; + /** How the entry fades the screen: 0 not at all; 1 and 4 towards the colour, others away from it. */ + int fadeType {0}; + glm::vec3 fadeColor {0.0f}; + /** Seconds before the fade starts. */ + float fadeDelay {0.0f}; + /** Seconds the fade takes; none for types 1 and 2, which cut. */ + float fadeLength {0.0f}; int cameraId {-1}; int cameraAngle {0}; int cameraAnimation {0}; @@ -104,6 +139,8 @@ struct Dialog { std::string abortScript; int entryIndex {-1}; bool animatedCutscene {false}; + /** Seconds a voiced node without a delay of its own stays, besides waiting for its voice. */ + uint32_t entryDelay {0}; /** Shots frame the participants' models where they stand, rather than their camera hooks. */ bool oldHitCheck {false}; std::vector stunts; diff --git a/include/reone/resource/provider/audioclips.h b/include/reone/resource/provider/audioclips.h index 0d5e0356a..5cd0a901f 100644 --- a/include/reone/resource/provider/audioclips.h +++ b/include/reone/resource/provider/audioclips.h @@ -39,6 +39,9 @@ class IAudioClips { virtual void clear() = 0; virtual std::shared_ptr get(const std::string &key) = 0; + + /** Whether an MP3 named \p resRef is there, streamed or not; nothing is decoded. */ + virtual bool hasMp3(const std::string &resRef) = 0; }; /** @@ -61,6 +64,10 @@ class AudioClips : public IAudioClips { _objects.clear(); } + bool hasMp3(const std::string &resRef) override { + return findClipData(resRef, ResType::Mp3).has_value(); + } + std::shared_ptr get(const std::string &key) override { auto maybeObject = _objects.find(key); if (maybeObject != _objects.end()) { diff --git a/include/reone/resource/provider/dialogs.h b/include/reone/resource/provider/dialogs.h index de964cba2..ab15878c8 100644 --- a/include/reone/resource/provider/dialogs.h +++ b/include/reone/resource/provider/dialogs.h @@ -17,6 +17,8 @@ #pragma once +#include + #include "../dialog.h" #include "../parser/gff/dlg.h" @@ -27,6 +29,8 @@ namespace resource { class Gffs; class Gff; class Strings; +class TwoDA; +class TwoDAs; class IDialogs { public: @@ -39,9 +43,15 @@ class IDialogs { class Dialogs : public IDialogs { public: - Dialogs(Gffs &gffs, Strings &strings) : + /** + * @param twoDas the tables TSL's alien speech is looked up in + * @param game the title the dialogs are read for + */ + Dialogs(Gffs &gffs, Strings &strings, TwoDAs &twoDas, GameID game) : _gffs(gffs), - _strings(strings) { + _strings(strings), + _twoDas(twoDas), + _game(game) { } void clear() override { @@ -58,17 +68,44 @@ class Dialogs : public IDialogs { } private: + /** TSL: the tables alien speech is looked up in. */ + struct AlienSpeechTables { + std::shared_ptr emotions; + std::shared_ptr voices; + }; + + /** TSL: the tables a node's alien speech is looked up in, and the race of the dialog's owner. */ + struct AlienSpeech { + AlienSpeechTables tables; + int ownerRace {0}; + }; + Gffs &_gffs; Strings &_strings; + TwoDAs &_twoDas; + GameID _game; + /** TSL: the alien speech tables, read with the first dialog. */ + std::optional _alienSpeechTables; std::unordered_map> _objects; std::shared_ptr doGet(std::string resRef); std::unique_ptr loadDialog(const generated::DLG &dlg); + /** TSL: the alien speech tables, read once. */ + const AlienSpeechTables &alienSpeechTables(); Dialog::EntryReplyLink getEntryReplyLink(const generated::DLG_EntryReplyList_EntriesRepliesList &dlg) const; - Dialog::EntryReply getEntryReply(const generated::DLG_EntryReplyList &dlg) const; + /** + * @param alienSpeech TSL: where the node's sound is looked up; KotOR + * reads the node's own Sound field instead + */ + Dialog::EntryReply getEntryReply(const generated::DLG_EntryReplyList &dlg, const std::optional &alienSpeech) const; + /** + * TSL: the alien speech of \p dlg, from the alien voice table's row for + * its race and the column its emotion names; empty when there is none. + */ + std::string getAlienSpeech(const generated::DLG_EntryReplyList &dlg, const AlienSpeech &alienSpeech) const; Dialog::Stunt getStunt(const generated::DLG_StuntList &dlg) const; Dialog::ParticipantAnimation getParticipantAnimation(const generated::DLG_EntryReplyList_AnimList &dlg) const; }; diff --git a/include/reone/script/executioncontext.h b/include/reone/script/executioncontext.h index 9cafd56d9..a992d7072 100644 --- a/include/reone/script/executioncontext.h +++ b/include/reone/script/executioncontext.h @@ -34,6 +34,12 @@ struct ExecutionContext { std::vector args; + /** + * The instructions run so far by this run and by the runs it started, + * which share the count; a run given none counts its own. + */ + std::shared_ptr instructionsRun; + /// Find an argument variable or return nullptr. /// /// Arguments are specific to a script run. For example, LastOpenedBy diff --git a/src/apps/engine/engine.cpp b/src/apps/engine/engine.cpp index a9f7e6331..c10c24a28 100644 --- a/src/apps/engine/engine.cpp +++ b/src/apps/engine/engine.cpp @@ -427,6 +427,13 @@ void Engine::processEvents(bool &quit) { imguiHandle(sdlEvent); continue; } + // While the window is out of focus the world waits. None of that time + // is charged to it: the frame clock and the world clock start again + // from the moment the window is back. + if (sdlEvent.type == SDL_EVENT_WINDOW_FOCUS_GAINED) { + _ticks = _services->system.clock.micros(); + _game->holdWorldClock(); + } if (_window->handle(sdlEvent)) { if (_window->isCloseRequested()) { quit = true; diff --git a/src/libs/audio/source.cpp b/src/libs/audio/source.cpp index c2bd4dea0..e7595e3f1 100644 --- a/src/libs/audio/source.cpp +++ b/src/libs/audio/source.cpp @@ -50,6 +50,42 @@ static void fillBuffer(const AudioClip::Frame &frame, uint32_t buffer) { frame.sampleRate); } +// A clip whose frames share one format and rate goes into one buffer whole, so +// its playback runs on the device's clock whatever the frame rate or however +// long a load holds the main loop; only a clip that changes format part-way is +// streamed a few frames at a time. +static bool fillWholeClip(const AudioClip &clip, uint32_t buffer) { + int frameCount = clip.getFrameCount(); + if (frameCount < 2) { + return false; + } + const auto &first = clip.getFrame(0); + size_t size = 0; + for (int i = 0; i < frameCount; ++i) { + const auto &frame = clip.getFrame(i); + if (frame.format != first.format || frame.sampleRate != first.sampleRate) { + return false; + } + size += frame.samples.size(); + } + if (size == 0) { + return false; + } + ByteBuffer samples; + samples.reserve(size); + for (int i = 0; i < frameCount; ++i) { + const auto &frame = clip.getFrame(i); + samples.insert(samples.end(), frame.samples.begin(), frame.samples.end()); + } + alBufferData( + buffer, + getALFormat(first.format), + samples.data(), + static_cast(samples.size()), + first.sampleRate); + return true; +} + void AudioSource::init() { if (_inited) { return; @@ -57,6 +93,26 @@ void AudioSource::init() { checkMainThread(); int frameCount = _stream->getFrameCount(); + if (frameCount > 1) { + uint32_t whole = 0; + alGenBuffers(1, &whole); + if (fillWholeClip(*_stream, whole)) { + _buffers.assign(1, whole); + _streaming = false; + alGenSources(1, &_source); + alSourcef(_source, AL_GAIN, _gain); + if (_position) { + alSource3f(_source, AL_POSITION, _position->x, _position->y, _position->z); + } else { + alSourcei(_source, AL_SOURCE_RELATIVE, AL_TRUE); + } + alSourcei(_source, AL_BUFFER, whole); + alSourcei(_source, AL_LOOPING, _loop); + _inited = true; + return; + } + alDeleteBuffers(1, &whole); + } int bufferCount = std::min(std::max(frameCount, 1), kMaxBufferCount); _buffers.resize(bufferCount); @@ -134,6 +190,18 @@ void AudioSource::render() { alGetSourcei(_source, AL_BUFFERS_QUEUED, &queued); if (queued == 0) { _playing = false; + return; + } + // A stream plays on whatever the frame rate: one that ran out of queued + // sound before this refill carries on from where it ran out, unless it + // was stopped or is paused. + if (!_playing || _paused) { + return; + } + ALint state = 0; + alGetSourcei(_source, AL_SOURCE_STATE, &state); + if (state == AL_STOPPED) { + alSourcePlay(_source); } } @@ -143,10 +211,12 @@ void AudioSource::play() { } alSourcePlay(_source); _playing = true; + _paused = false; } void AudioSource::pause() { alSourcePause(_source); + _paused = true; } // Only a source that is still playing, and paused or created paused, resumes: @@ -157,6 +227,7 @@ void AudioSource::resume() { if (!_playing) { return; } + _paused = false; ALint state = 0; alGetSourcei(_source, AL_SOURCE_STATE, &state); if (state == AL_PAUSED || state == AL_INITIAL) { @@ -166,6 +237,7 @@ void AudioSource::resume() { void AudioSource::startPaused() { _playing = true; + _paused = true; } void AudioSource::stop() { @@ -190,6 +262,13 @@ void AudioSource::setPosition(glm::vec3 position) { _position = std::move(position); } +void AudioSource::setLooping(bool loop) { + _loop = loop; + if (_source && !_streaming) { + alSourcei(_source, AL_LOOPING, _loop); + } +} + void AudioSource::setGainScale(float scale) { alSourcef(_source, AL_GAIN, _gain * scale); } diff --git a/src/libs/game/action/attackobject.cpp b/src/libs/game/action/attackobject.cpp index b74269aa3..63145e9ca 100644 --- a/src/libs/game/action/attackobject.cpp +++ b/src/libs/game/action/attackobject.cpp @@ -166,8 +166,10 @@ void AttackObjectAction::execute(std::shared_ptr self, Object &actor, fl attacker.setDesiredFacingToward(target->position()); - // Once its round has been ended, a started attack has nothing left to do. - if ((_schedule.isMelee() || _schedule.isRanged()) && !_game.combat().ownsRound(attacker)) { + // Once its round has been ended, a started attack has nothing left to do, + // also when its creature has been given a new round meanwhile: the attack + // does not carry over into it. + if ((_schedule.isMelee() || _schedule.isRanged()) && !_game.combat().runsRound(*self)) { finish(attacker); return; } diff --git a/src/libs/game/action/commands.cpp b/src/libs/game/action/commands.cpp index df64a9742..09f63d1a0 100644 --- a/src/libs/game/action/commands.cpp +++ b/src/libs/game/action/commands.cpp @@ -304,7 +304,7 @@ std::shared_ptr WaitAction::restore(const SavedActionRecord &record, Gam // surrender fails. void SurrenderToEnemiesAction::execute(std::shared_ptr self, Object &actor, float dt) { auto &creature = static_cast(actor); - if (!creature.isPC()) { + if (creature.actsAsPCValue() != 1) { creature.surrenderToEnemies(false); } complete(); diff --git a/src/libs/game/action/conversation.cpp b/src/libs/game/action/conversation.cpp index 48c2a3f2d..118fa1a49 100644 --- a/src/libs/game/action/conversation.cpp +++ b/src/libs/game/action/conversation.cpp @@ -184,6 +184,11 @@ void StartConversationAction::admit() { } bool StartConversationAction::cancel(std::shared_ptr self, Object &actor) { + // A request that has started its conversation is done: clearing it, as a + // script of the opening entry may, leaves the conversation running. + if (isCompleted()) { + return true; + } // A dropped or failed request ends the conversation being under way. _game.setDialogUnderWay(false); _game.globalFade().finishDialog(_fadeDialog); @@ -317,11 +322,14 @@ void StartConversationAction::execute(std::shared_ptr self, Object &acto return; } + // The request is done before its conversation starts, and the + // conversation owns the admission from then on: the opening entry's + // scripts run inside the start, and may clear this request. const auto listener = dialogOwner == actorPtr ? _objectToConverse : actorPtr; - _game.startDialog(dialogOwner, _dialogResRef.empty() ? dialogOwner->conversation() : _dialogResRef, - _fadeDialog, listener); - _fadeDialog.reset(); // successful startup is now owned by Conversation + auto admission = std::move(_fadeDialog); complete(); + _game.startDialog(dialogOwner, _dialogResRef.empty() ? dialogOwner->conversation() : _dialogResRef, + std::move(admission), listener); } // A conversation names its dialog by a resource name of at most sixteen diff --git a/src/libs/game/action/usefeat.cpp b/src/libs/game/action/usefeat.cpp index 87d830e4f..240ae22db 100644 --- a/src/libs/game/action/usefeat.cpp +++ b/src/libs/game/action/usefeat.cpp @@ -100,8 +100,10 @@ void UseFeatAction::execute(std::shared_ptr self, Object &actor, float d attacker.setDesiredFacingToward(target->position()); - // Once its round has been ended, a started attack has nothing left to do. - if ((_schedule.isMelee() || _schedule.isRanged()) && !_game.combat().ownsRound(attacker)) { + // Once its round has been ended, a started attack has nothing left to do, + // also when its creature has been given a new round meanwhile: the attack + // does not carry over into it. + if ((_schedule.isMelee() || _schedule.isRanged()) && !_game.combat().runsRound(*self)) { finish(attacker); return; } diff --git a/src/libs/game/areamusicplayer.cpp b/src/libs/game/areamusicplayer.cpp index ad12c966a..114052c46 100644 --- a/src/libs/game/areamusicplayer.cpp +++ b/src/libs/game/areamusicplayer.cpp @@ -139,10 +139,20 @@ bool AreaMusicPlayer::isAmbientPlaying() { return _ambient && _ambient->isPlaying(); } -bool AreaMusicPlayer::playTrack(const std::string &resRef) { +bool AreaMusicPlayer::playTrack(const std::string &resRef, bool takeLoadMusic) { if (resRef.empty()) { return false; } + // TSL's background music takes over the load-screen music when it is the + // same track, and lets it play on, once through. + if (takeLoadMusic && _loadMusicHandover) { + if (auto loadMusic = _loadMusicHandover(resRef)) { + _music = std::move(loadMusic); + _fadeEnd.reset(); + _playingResRef = resRef; + return true; + } + } _music = _services.audio.mixer.play(_services.resource.audioClips.get(resRef), AudioType::Music, _musicGroupGain); _fadeEnd.reset(); _playingResRef = resRef; @@ -205,6 +215,25 @@ void AreaMusicPlayer::stopSounds() { _ambientFailed = false; } +// A movie turns the music off only once the area's sounds have started: before +// that, the music starts on arrival as the area asks. The battle music, the +// pending restart and the ambient loop are switched off either way, so an area +// whose enter script plays a movie before arrival has no ambient loop. +void AreaMusicPlayer::stopSoundsForMovie() { + if (_started) { + stopSounds(); + return; + } + _battleOn = false; + _countdown = 0; + if (_ambient) { + _ambient->stop(); + } + _ambientFadeEnd.reset(); + _ambientOn = false; + _ambientFailed = false; +} + // Nothing is on once the area is left, so nothing restarts until the next one // is loaded; a fading track still fades out. void AreaMusicPlayer::unload() { @@ -228,7 +257,7 @@ void AreaMusicPlayer::switchMusic(bool play) { if (play) { if (!isMusicPlaying()) { _countdown = 0; - if (!playTrack(track(_area->ambientAudio().musicDay).resRef)) { + if (!playTrack(track(_area->ambientAudio().musicDay).resRef, true)) { _countdown = kMusicRetryMilliseconds; } } diff --git a/src/libs/game/attack.cpp b/src/libs/game/attack.cpp index daae2ff36..dfc735af9 100644 --- a/src/libs/game/attack.cpp +++ b/src/libs/game/attack.cpp @@ -3309,7 +3309,7 @@ AttackApproachStep approachAttackTarget(Creature &attacker, Object &target, floa const bool sight = area.isEyeLineClear(eye, targetEye, attacker, target, rightHand && rightHand->isRanged()); if (sight) { attacker.clearBlockedAttackSight(); - } else if (attacker.noteBlockedAttackSight() && attacker.isPC()) { + } else if (attacker.noteBlockedAttackSight() && attacker.actsAsPC()) { // The player character gives up a target it cannot see; the line // reaches the player only while controlling it. if (game.party().getLeader().get() == &attacker) { diff --git a/src/libs/game/combat.cpp b/src/libs/game/combat.cpp index 1709aa6ae..4d2b31765 100644 --- a/src/libs/game/combat.cpp +++ b/src/libs/game/combat.cpp @@ -556,6 +556,14 @@ bool Combat::ownsRound(const Creature &actor) const { }); } +bool Combat::runsRound(const Action &action) const { + return std::any_of(_rounds.begin(), _rounds.end(), [&action](const auto &round) { + return std::any_of(round->actions.begin(), round->actions.end(), [&action](const auto &entry) { + return entry.running() && (entry.action.get() == &action || &entry.action->combatAction() == &action); + }); + }); +} + bool Combat::ownsDetachedRound(const Creature &actor) const { const auto &nodes = actor.actions().nodes; bool owned = false; @@ -2336,7 +2344,9 @@ bool Combat::engagePhysicalTarget(const Creature &attacker, Object &target, int endRound(*partner, 1); // The partner's round starts again on the attacker's side, with no // command yet, aimed at the attacker and held for the attack's - // pause; it runs out like any round. + // pause. The hold stands for the attacker's turn: once it ends, + // the partner may act at once, and its round runs out a full + // round later. if (round && round->actions.size() == 1) { auto placeholder = _game.newAction(0.0f); placeholder->complete(); @@ -2345,6 +2355,7 @@ bool Combat::engagePhysicalTarget(const Creature &attacker, Object &target, int round->actions.emplace_back(placeholder, live, master, false); auto &side = round->actions.back(); side.slot = 1; + side.state = CombatRound::SecondAction; side.pauseRemaining = pauseMilliseconds / 1000.0f; side.pauseOwner = master; round->duel = true; diff --git a/src/libs/game/effect/creaturestate.cpp b/src/libs/game/effect/creaturestate.cpp index 2e12a2cbe..574d16946 100644 --- a/src/libs/game/effect/creaturestate.cpp +++ b/src/libs/game/effect/creaturestate.cpp @@ -353,7 +353,7 @@ void CreatureStateInternalEffect::onRemove(Object &object, const EffectInstance creature->onInternalStateRemoved(instance.id, stateRemovalAppliesPose(state, object.game().isTSL())); // A creature other than a player character forgets what it perceived // and looks around anew. - if (!creature->isPC()) { + if (!creature->actsAsPC()) { creature->perceiveAfresh(); } } @@ -711,7 +711,7 @@ EffectApplicationResult EntangleEffect::onApply(Object &object, EffectInstance & } void EntangleEffect::onRemove(Object &object, const EffectInstance &) { - if (auto *creature = dyn_cast(&object); creature && !creature->isPC()) { + if (auto *creature = dyn_cast(&object); creature && !creature->actsAsPC()) { creature->perceiveAfresh(); } } diff --git a/src/libs/game/effect/modifiers.cpp b/src/libs/game/effect/modifiers.cpp index a55324ed1..f84088375 100644 --- a/src/libs/game/effect/modifiers.cpp +++ b/src/libs/game/effect/modifiers.cpp @@ -737,7 +737,7 @@ EffectApplicationResult DisguiseEffect::onApply(Object &object, EffectInstance & void DisguiseEffect::onRemove(Object &object, const EffectInstance &) { auto *creature = dyn_cast(&object); - if (!creature || (creature->isDead() && !creature->isPC() && object.isDestroyable())) { + if (!creature || (creature->isDead() && !creature->actsAsPC() && object.isDestroyable())) { return; } creature->removeDisguiseAppearance(); diff --git a/src/libs/game/equipmentoperation.cpp b/src/libs/game/equipmentoperation.cpp index d51539ef6..a7cedfe13 100644 --- a/src/libs/game/equipmentoperation.cpp +++ b/src/libs/game/equipmentoperation.cpp @@ -86,7 +86,7 @@ static EquipmentOperationOutcome equipFrom( // Refusals and replacements are reported to the controlled creature only. const bool controlled = game.party().getLeader().get() == &subject; // The player character must reach the item's level before anything else. - if (subject.isPC() && subject.attributes().getAggregateLevel() < minimumEquipLevel(subject, *item)) { + if (subject.isPCValue() == 1 && subject.attributes().getAggregateLevel() < minimumEquipLevel(subject, *item)) { if (controlled) { game.addFeedbackMessage(*equipmentRefusalFeedback(EquipmentCandidateReason::MinimumLevel)); } diff --git a/src/libs/game/equipmentrules.cpp b/src/libs/game/equipmentrules.cpp index 187adf3a4..0daa533da 100644 --- a/src/libs/game/equipmentrules.cpp +++ b/src/libs/game/equipmentrules.cpp @@ -334,7 +334,7 @@ bool canUseItem(const Creature &creature, const Item &item, bool menuCheck) { if (tsl && (!meetsGenderLimits(creature, item) || !meetsPlayerCharacterLimits(creature, item) || !meetsAttributeLimits(creature, item) || isExcludedForCreature(creature, item))) return false; - if (creature.isPC() && creature.attributes().getAggregateLevel() < minimumEquipLevel(creature, item)) { + if (creature.isPCValue() == 1 && creature.attributes().getAggregateLevel() < minimumEquipLevel(creature, item)) { return false; } // A weapon may be at most one size larger than its user, unless it may be diff --git a/src/libs/game/game.cpp b/src/libs/game/game.cpp index 2de08bf20..76504f263 100644 --- a/src/libs/game/game.cpp +++ b/src/libs/game/game.cpp @@ -26,6 +26,9 @@ #include #include +#include +#include + #include #include #include @@ -78,6 +81,7 @@ #include "reone/graphics/texture.h" #include "reone/graphics/uniforms.h" #include "reone/gui/gui.h" +#include "reone/gui/guis.h" #include "reone/movie/format/bikreader.h" #include "reone/resource/2da.h" #include "reone/resource/di/services.h" @@ -614,7 +618,8 @@ void Game::init() { _moduleNames = _services.resource.director.moduleNames(); _saveNames = _services.resource.director.saveNames(); - playVideo("legal"); + // No movie plays at start-up: the main menu opens, and its music starts, + // at once. openMainMenu(); } @@ -687,6 +692,11 @@ void Game::initTables() { _trapTypes = Trap::readTypes(twoDas); _areaMusic.init(isTSL()); + if (isTSL()) { + _areaMusic.setLoadMusicHandover([this](const std::string &resRef) { + return takeLoadMusic(resRef); + }); + } _party.init(); } @@ -935,6 +945,9 @@ void Game::update(float frameTime) { // fade run this frame. A pending end keeps the death GUI up while its // delay lasts, then returns to the main menu once the game is not over. updateDeathSequence(); + // Every GUI's pulse runs through pauses, whatever the screen, and slows + // with the world under the sequel's gameover panel. + _services.gui.guis.updatePulsingAlpha(frameTime * _deathTimeScale); if (_endGamePending) { if (_endGameDelay <= 0.0f) { if (!_gameOver) { @@ -1935,6 +1948,8 @@ void Game::retireRuntimeSession() { _services.audio.mixer.stopAll(); _music.reset(); + _loadMusic.reset(); + _loadComesWithMovies = false; _movie.reset(); _musicResRef.clear(); while (!_moduleTransitionMovies.empty()) { @@ -2111,6 +2126,7 @@ Game::PreparedSaveLoad Game::prepareSaveLoad(const resource::SaveSlotDescriptor } autosave.pauseDay = params->getUint("TIME_PAUSEDAY"); autosave.pauseTime = params->getUint("TIME_PAUSETIME"); + autosave.loadMusic = params->getString("LOADMUSIC"); prepared.autosave = std::move(autosave); } validatePartyLoad(prepared.partyTable.get()); @@ -2130,6 +2146,12 @@ bool Game::loadGame(const resource::SaveSlotDescriptor &slot) { // Commit. The old runtime and the old mounts retire together, and only // then does the candidate become authoritative: no runtime ever // observes the other session's resources. + // A save loads with no loading-screen music, except an autosave of the + // world's template, which plays the one it recorded. + _loadMusicResRef.clear(); + if (prepared.autosave) { + _loadMusicResRef = prepared.autosave->loadMusic; + } resetGame(); _services.resource.director.commitGameLoad(std::move(prepared.session)); return restoreSaveLoad(std::move(prepared)); @@ -2854,6 +2876,8 @@ void Game::setCursorType(CursorType type) { void Game::playVideo(const std::string &name, bool streamingMusic) { _moduleTransitionMovies = std::queue(); + // A script's movie empties the transition list when it ends. + _loadComesWithMovies = false; startVideo(name, streamingMusic); } @@ -2872,6 +2896,81 @@ void Game::playMusic(const std::string &resRef) { } } +// A new game, TSL's character generation and the module picker set the +// loading-screen music from the module's row of the loading-screen table. +void Game::setLoadMusicForModule(const std::string &moduleName) { + if (!_loadScreen) { + _loadScreen = tryLoadGUI(); + } + _loadMusicResRef = _loadScreen ? _loadScreen->musicResRef(moduleName) : ""; +} + +// Every loading screen shown with music starts it: the menu music stops; +// nothing starts while transition movies are listed for the load; music that +// is there but not playing is resumed; else the set track starts, looping, in +// the streamed music group, at that group's volume. +void Game::startLoadMusic() { + playMusic(""); + if (_loadComesWithMovies) { + return; + } + if (_loadMusic) { + if (!_loadMusic->isPlaying()) { + _loadMusic->resume(); + } + return; + } + if (_loadMusicResRef.empty()) { + return; + } + _loadMusic = _services.audio.mixer.play( + _services.resource.audioClips.get(_loadMusicResRef), + AudioType::Music, + _areaMusic.musicGroupGain(), + true); +} + +// KotOR's loading screen stops the loading music when it goes; both titles +// stop it when the area's sounds start, and TSL when character generation +// ends. +void Game::stopLoadMusic() { + if (_loadMusic) { + _loadMusic->stop(); + _loadMusic.reset(); + } +} + +// The menu music fades out over 2500 ms; only the sound runs until 750 ms have +// passed, and then the menu music stops at once. +static constexpr uint32_t kMenuMusicFadeMilliseconds = 2500; +static constexpr uint32_t kMenuMusicWaitMilliseconds = 750; + +// The wait is real time. +void Game::fadeOutMenuMusic() { + using namespace std::chrono; + const auto start = steady_clock::now(); + for (auto elapsed = milliseconds(0); elapsed < milliseconds(kMenuMusicWaitMilliseconds); + elapsed = duration_cast(steady_clock::now() - start)) { + if (_music) { + _music->setGainScale(1.0f - static_cast(elapsed.count()) / kMenuMusicFadeMilliseconds); + } + _services.audio.mixer.render(); + std::this_thread::sleep_for(milliseconds(1)); + } + playMusic(""); +} + +// The playing loading music, when it is the track asked for (ignoring case), +// becomes the background music and plays once through. +std::shared_ptr Game::takeLoadMusic(const std::string &resRef) { + if (!_loadMusic || !boost::iequals(resRef, _loadMusicResRef)) { + return nullptr; + } + auto music = std::move(_loadMusic); + music->setLooping(false); + return music; +} + void Game::renderScene() { if (!_module || !_options.graphics.sceneRender) { _lastRenderedSceneOutput = nullptr; @@ -2900,11 +2999,6 @@ static constexpr int kHighestHostileReputation = 10; static constexpr float kDeathFadeWait = 12.0f; static constexpr float kDeathFadeLength = 1.0f; static constexpr float kHurriedFadeLength = 0.5f; -// The first title's slow motion: it reaches kDeathSlowestSpeed after -// kDeathSlowSeconds of real time, easing logarithmically from full speed. -static constexpr float kDeathSlowSeconds = 4.0f; -static constexpr float kDeathSlowestSpeed = 0.2f; -static constexpr double kDeathSlowSpan = 0.8; // The sequel's gameover panel runs the world at a quarter of its speed. static constexpr float kDeathPanelTimeScale = 0.25f; // The first title's death message. @@ -3043,7 +3137,6 @@ void Game::setLastPartyMemberTempKilled(const Creature &creature) { // holds before fading to black. Only a fallen party in a module's area starts it. void Game::runDeathSequence() { _gameOver = true; - _deathSequenceStarting = true; // Every panel closes, a tutorial window about to open included. _tutorialPending.reset(); stopMovement(); @@ -3096,7 +3189,6 @@ void Game::endGame(bool showGui) { _endGameDelay = showGui ? (isTSL() ? kEndGameGuiDelayTSL : kEndGameGuiDelayKotOR) : 0.0f; if (showGui && !isTSL()) { _gameOver = true; - _deathSequenceStarting = true; } } @@ -3115,36 +3207,22 @@ void Game::hurryDeathSequence() { _globalFade.request(GlobalFade::Direction::Out, 0.0f, kHurriedFadeLength, glm::vec3 {0.0f}); } -// The first title slows the world while its game is over, and ends the game -// once the fade to black has finished or an end was asked for, and the death -// message of a scripted end has had its time. The sequel's gameover panel -// slows the world for as long as it is up. +// The first title ends the game once the fade to black has finished or an end +// was asked for, and the death message of a scripted end has had its time; its +// world keeps full speed throughout. The sequel's gameover panel slows the +// world for as long as it is up. void Game::updateDeathSequence() { if (isTSL()) { _deathTimeScale = _deathDisplay && _deathDisplay->isPresented() ? kDeathPanelTimeScale : 1.0f; return; } - const auto now = static_cast(_services.system.clock.millis()); - const uint32_t elapsed = now - _deathSequenceSample; - _deathSequenceSample = now; if (!_gameOver) { return; } - _deathSequenceSeconds += static_cast(elapsed) / 1000.0f; - if (_deathSequenceStarting) { - _deathSequenceStarting = false; - _deathSlowRate = static_cast( - -std::log(static_cast(_deathSequenceSeconds * (1.0f / kDeathSlowSeconds))) / kDeathSlowSpan); - } - _deathTimeScale = _deathSequenceSeconds > kDeathSlowSeconds - ? kDeathSlowestSpeed - : -std::log(_deathSequenceSeconds * (1.0f / kDeathSlowSeconds)) / _deathSlowRate + kDeathSlowestSpeed; if ((_globalFade.fading() && !_endGamePending) || _endGameDelay > 0.0f) { return; } - _deathTimeScale = 1.0f; _gameOver = false; - _deathSequenceStarting = true; _globalFade.stop(); requestEndGame(); if (_deathMessage) { @@ -4435,6 +4513,13 @@ void Game::renderGlobalFade() { if (alpha <= 0.0f) { return; } + // Compatibility approximation: color, then blackfill. + for (auto color : {_globalFade.color(), glm::vec3(0.0f)}) { + renderScreenFill(color, alpha); + } +} + +void Game::renderScreenFill(const glm::vec3 &color, float alpha) { auto &graphics = _services.graphics; auto &context = graphics.context; GlobalUniforms previousGlobals; @@ -4454,17 +4539,14 @@ void Game::renderGlobalFade() { context.withPolygonMode(PolygonMode::Fill, [&]() { // Inherit the GUI viewport, including window/drawable // scaling. Logical GUI dimensions need not be pixels. - // Compatibility approximation: color, then blackfill. - for (auto color : {_globalFade.color(), glm::vec3(0.0f)}) { - graphics.uniforms.setLocals([&](auto &locals) { - locals.reset(); - locals.model = glm::scale(glm::mat4(1.0f), - glm::vec3(_options.graphics.width, _options.graphics.height, 1.0f)); - locals.color = glm::vec4(color, alpha); - }); - context.useProgram(graphics.shaderRegistry.get(ShaderProgramId::mvpColor)); - graphics.meshRegistry.get(MeshName::quad).draw(graphics.statistic); - } + graphics.uniforms.setLocals([&](auto &locals) { + locals.reset(); + locals.model = glm::scale(glm::mat4(1.0f), + glm::vec3(_options.graphics.width, _options.graphics.height, 1.0f)); + locals.color = glm::vec4(color, alpha); + }); + context.useProgram(graphics.shaderRegistry.get(ShaderProgramId::mvpColor)); + graphics.meshRegistry.get(MeshName::quad).draw(graphics.statistic); }); }); }); @@ -4484,6 +4566,7 @@ void Game::settleFadeArrival() { if (_musicResRef.empty()) { _areaMusic.start(); } + stopLoadMusic(); _globalFade.settleArrival(_fadeArrival); } _fadeArrival.reset(); @@ -4927,11 +5010,12 @@ bool Game::startVideo(const std::string &name, bool streamingMusic) { if (!_movie) { return false; } - // A movie ends the area's music and ambient loop for good. Every other - // sound waits for it, including sounds started before it is first shown; - // a movie that lets music play leaves area music set to play through - // movies sounding while it fades. - _areaMusic.stopSounds(); + // A movie ends the area's music and ambient loop for good; one that + // comes before arrival, as an enter script's, leaves the music to start + // on arrival but no ambient loop. Every other sound waits for it, including + // sounds started before it is first shown; a movie that lets music play + // leaves area music set to play through movies sounding while it fades. + _areaMusic.stopSoundsForMovie(); _services.audio.mixer.setSoundsHeldForMovie(true, streamingMusic ? _areaMusic.musicPlayingThroughMovies() : nullptr); return true; } @@ -4942,6 +5026,11 @@ bool Game::playNextModuleTransitionMovie() { _moduleTransitionMovies.pop(); if (startVideo(name, false)) { + // A transition list holds only movies that exist, and its load + // plays no loading-screen music. + if (!_nextModule.empty()) { + _loadComesWithMovies = true; + } return true; } } @@ -5616,7 +5705,15 @@ void Game::withLoadingScreen(const std::string &moduleName, const std::function< } changeScreen(Screen::Loading); render(); + startLoadMusic(); block(); + // KotOR's loading screen takes its music with it; TSL's plays on until + // the area's sounds start. The transition list is cleared at the load's + // end. + if (!isTSL()) { + stopLoadMusic(); + } + _loadComesWithMovies = false; // Player input returns once a load is done. _playerInputBlocked = false; } @@ -8375,11 +8472,17 @@ void Game::notifyLevelUpAvailable(const Creature &creature, bool newlyAvailable) } // TSL introduces levelling up the first time the player character can, // outside a conversation. - if (isTSL() && creature.isPlayerCreated() && !isConversationActive() && - getGlobalNumber(kLevelUpDialogFiredGlobal) == 0) { - setGlobalNumber(kLevelUpDialogFiredGlobal, 1); - _scriptRunner->run(kLevelUpDialogScript); + if (isTSL() && creature.isPlayerCreated() && !isConversationActive()) { + runLevelUpDialogOnce(); + } +} + +void Game::runLevelUpDialogOnce() { + if (getGlobalNumber(kLevelUpDialogFiredGlobal) != 0) { + return; } + setGlobalNumber(kLevelUpDialogFiredGlobal, 1); + _scriptRunner->run(kLevelUpDialogScript); } /** The name character generation loads under. */ @@ -8395,11 +8498,18 @@ void Game::startCharacterGeneration() { } // Character generation loads as the class selection does, and every new // character starts from a new class selection. + // TSL's character generation plays its loading screen's music throughout; + // KotOR's plays its menu music once the panel is up. + if (isTSL()) { + setLoadMusicForModule(kClassSelectionLoadScreen); + } withLoadingScreen(kClassSelectionLoadScreen, [this]() { _charGen->startCreation(); _loadScreen->setProgress(100); render(); - playMusic(_charGen->musicResRef()); + if (!isTSL()) { + playMusic(_charGen->musicResRef()); + } changeScreen(Screen::CharacterGeneration); }); } @@ -8650,8 +8760,13 @@ void Game::submitStatusSummary( void Game::finishStatusSummaryCycle() { static constexpr char kAlignmentMovieScript[] = "k_align_movie"; - if (isTSL()) { - _scriptRunner->run(kAlignmentMovieScript); + if (!isTSL()) { + return; + } + _scriptRunner->run(kAlignmentMovieScript); + const auto &player = *_party.player(); + if (player.isPlayerCreated() && player.isLevelUpPending()) { + runLevelUpDialogOnce(); } } diff --git a/src/libs/game/gui/chargen.cpp b/src/libs/game/gui/chargen.cpp index dcff53f02..fa77ec9bb 100644 --- a/src/libs/game/gui/chargen.cpp +++ b/src/libs/game/gui/chargen.cpp @@ -86,7 +86,9 @@ CharacterGeneration::CharacterGeneration(Game &game, ServicesView &services) : GameGUI(game, services) { _resRef = guiResRef("maincg"); - _musicResRef = game.isTSL() ? "mus_main" : "mus_theme_rep"; + // KotOR's character generation plays its own menu music; TSL's plays its + // loading screen's music instead, from the loading-screen table. + _musicResRef = game.isTSL() ? "" : "mus_theme_rep"; } void CharacterGeneration::onGUILoaded() { @@ -345,11 +347,13 @@ void CharacterGeneration::openSteps() { } void CharacterGeneration::openQuick() { + _quick->refreshStep(); _controls.MODEL_LBL->setVisible(true); changeScreen(CharGenScreen::Quick); } void CharacterGeneration::openCustom() { + _custom->refreshStep(); setAttributesVisible(_custom->step() > 1); _controls.MODEL_LBL->setVisible(true); changeScreen(CharGenScreen::Custom); @@ -441,6 +445,7 @@ void CharacterGeneration::openNameEntry() { void CharacterGeneration::openLevelUp() { _levelUp->reset(); + _levelUp->refreshStep(); _controls.MODEL_LBL->setVisible(true); changeScreen(CharGenScreen::LevelUp); } @@ -554,7 +559,16 @@ void CharacterGeneration::finish() { _game.setGlobalNumber(kPlayerLevelGlobal, player->attributes().getAggregateLevel()); } + // Starting the game first fades the menu music out. + _game.fadeOutMenuMusic(); + std::string moduleName(!_game.isTSL() ? "end_m01aa" : "001ebo"); + // The new game's loading screen plays the module's music; TSL first + // stops character generation's. + if (_game.isTSL()) { + _game.stopLoadMusic(); + } + _game.setLoadMusicForModule(moduleName); _game.loadModule(moduleName); } } diff --git a/src/libs/game/gui/chargen/abilities.cpp b/src/libs/game/gui/chargen/abilities.cpp index 31a905e9f..940525860 100644 --- a/src/libs/game/gui/chargen/abilities.cpp +++ b/src/libs/game/gui/chargen/abilities.cpp @@ -120,22 +120,22 @@ void CharGenAbilities::onGUILoaded() { }); _controls.STR_LBL->setOnSelectionChanged([this](bool selected) { - onAbilityLabelSelectionChanged(Ability::Strength, selected); + onAbilityLabelSelectionChanged(Ability::Strength, *_controls.STR_LBL, *_controls.STR_POINTS_BTN, selected); }); _controls.DEX_LBL->setOnSelectionChanged([this](bool selected) { - onAbilityLabelSelectionChanged(Ability::Dexterity, selected); + onAbilityLabelSelectionChanged(Ability::Dexterity, *_controls.DEX_LBL, *_controls.DEX_POINTS_BTN, selected); }); _controls.CON_LBL->setOnSelectionChanged([this](bool selected) { - onAbilityLabelSelectionChanged(Ability::Constitution, selected); + onAbilityLabelSelectionChanged(Ability::Constitution, *_controls.CON_LBL, *_controls.CON_POINTS_BTN, selected); }); _controls.INT_LBL->setOnSelectionChanged([this](bool selected) { - onAbilityLabelSelectionChanged(Ability::Intelligence, selected); + onAbilityLabelSelectionChanged(Ability::Intelligence, *_controls.INT_LBL, *_controls.INT_POINTS_BTN, selected); }); _controls.WIS_LBL->setOnSelectionChanged([this](bool selected) { - onAbilityLabelSelectionChanged(Ability::Wisdom, selected); + onAbilityLabelSelectionChanged(Ability::Wisdom, *_controls.WIS_LBL, *_controls.WIS_POINTS_BTN, selected); }); _controls.CHA_LBL->setOnSelectionChanged([this](bool selected) { - onAbilityLabelSelectionChanged(Ability::Charisma, selected); + onAbilityLabelSelectionChanged(Ability::Charisma, *_controls.CHA_LBL, *_controls.CHA_POINTS_BTN, selected); }); _controls.STR_MINUS_BTN->setOnClick([this]() { @@ -238,7 +238,10 @@ void CharGenAbilities::updateCharacter() { _charGen.setCharacter(std::move(character)); } -void CharGenAbilities::onAbilityLabelSelectionChanged(Ability ability, bool selected) { +void CharGenAbilities::onAbilityLabelSelectionChanged(Ability ability, Label &label, Button &points, bool selected) { + // The hovered row's name and points pulse. + label.setTextPulsing(selected); + points.setTextPulsing(selected); if (!selected) return; diff --git a/src/libs/game/gui/chargen/custom.cpp b/src/libs/game/gui/chargen/custom.cpp index 028c62e75..7b714ed5c 100644 --- a/src/libs/game/gui/chargen/custom.cpp +++ b/src/libs/game/gui/chargen/custom.cpp @@ -129,6 +129,20 @@ void CustomCharacterGeneration::doSetStep(int step) { _controls.BTN_STEPNAME4->setSelected(_step == 3); _controls.BTN_STEPNAME5->setSelected(_step == 4); _controls.BTN_STEPNAME6->setSelected(_step == 5); + + // The current step's number and name pulse. + _controls.LBL_NUM1->setTextPulsing(_step == 0); + _controls.LBL_NUM2->setTextPulsing(_step == 1); + _controls.LBL_NUM3->setTextPulsing(_step == 2); + _controls.LBL_NUM4->setTextPulsing(_step == 3); + _controls.LBL_NUM5->setTextPulsing(_step == 4); + _controls.LBL_NUM6->setTextPulsing(_step == 5); + _controls.BTN_STEPNAME1->setTextPulsing(_step == 0); + _controls.BTN_STEPNAME2->setTextPulsing(_step == 1); + _controls.BTN_STEPNAME3->setTextPulsing(_step == 2); + _controls.BTN_STEPNAME4->setTextPulsing(_step == 3); + _controls.BTN_STEPNAME5->setTextPulsing(_step == 4); + _controls.BTN_STEPNAME6->setTextPulsing(_step == 5); } void CustomCharacterGeneration::goToNextStep() { diff --git a/src/libs/game/gui/chargen/levelup.cpp b/src/libs/game/gui/chargen/levelup.cpp index 4ecba1b88..10cb6cb3b 100644 --- a/src/libs/game/gui/chargen/levelup.cpp +++ b/src/libs/game/gui/chargen/levelup.cpp @@ -162,6 +162,18 @@ void LevelUpMenu::doSetStep(int step) { _controls.BTN_STEPNAME3->setSelected(_step == 2); _controls.BTN_STEPNAME4->setSelected(_step == 3); _controls.BTN_STEPNAME5->setSelected(_step == 4); + + // The current step's number and name pulse. + _controls.LBL_NUM1->setTextPulsing(_step == 0); + _controls.LBL_NUM2->setTextPulsing(_step == 1); + _controls.LBL_NUM3->setTextPulsing(_step == 2); + _controls.LBL_NUM4->setTextPulsing(_step == 3); + _controls.LBL_NUM5->setTextPulsing(_step == 4); + _controls.BTN_STEPNAME1->setTextPulsing(_step == 0); + _controls.BTN_STEPNAME2->setTextPulsing(_step == 1); + _controls.BTN_STEPNAME3->setTextPulsing(_step == 2); + _controls.BTN_STEPNAME4->setTextPulsing(_step == 3); + _controls.BTN_STEPNAME5->setTextPulsing(_step == 4); } bool LevelUpMenu::hasFeatChoices() const { diff --git a/src/libs/game/gui/chargen/quick.cpp b/src/libs/game/gui/chargen/quick.cpp index 6dad85bd0..2e3ac49fe 100644 --- a/src/libs/game/gui/chargen/quick.cpp +++ b/src/libs/game/gui/chargen/quick.cpp @@ -96,6 +96,14 @@ void QuickCharacterGeneration::doSetStep(int step) { _controls.BTN_STEPNAME1->setSelected(_step == 0); _controls.BTN_STEPNAME2->setSelected(_step == 1); _controls.BTN_STEPNAME3->setSelected(_step == 2); + + // The current step's number and name pulse. + _controls.LBL_NUM1->setTextPulsing(_step == 0); + _controls.LBL_NUM2->setTextPulsing(_step == 1); + _controls.LBL_NUM3->setTextPulsing(_step == 2); + _controls.BTN_STEPNAME1->setTextPulsing(_step == 0); + _controls.BTN_STEPNAME2->setTextPulsing(_step == 1); + _controls.BTN_STEPNAME3->setTextPulsing(_step == 2); } void QuickCharacterGeneration::goToNextStep() { diff --git a/src/libs/game/gui/chargen/skills.cpp b/src/libs/game/gui/chargen/skills.cpp index c3367b65e..7085f7aaa 100644 --- a/src/libs/game/gui/chargen/skills.cpp +++ b/src/libs/game/gui/chargen/skills.cpp @@ -106,28 +106,28 @@ void CharGenSkills::onGUILoaded() { }); _controls.COMPUTER_USE_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::ComputerUse, selected); + onSkillLabelSelectionChanged(SkillType::ComputerUse, *_controls.COMPUTER_USE_LBL, *_controls.COMPUTER_USE_POINTS_BTN, selected); }); _controls.DEMOLITIONS_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::Demolitions, selected); + onSkillLabelSelectionChanged(SkillType::Demolitions, *_controls.DEMOLITIONS_LBL, *_controls.DEMOLITIONS_POINTS_BTN, selected); }); _controls.STEALTH_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::Stealth, selected); + onSkillLabelSelectionChanged(SkillType::Stealth, *_controls.STEALTH_LBL, *_controls.STEALTH_POINTS_BTN, selected); }); _controls.AWARENESS_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::Awareness, selected); + onSkillLabelSelectionChanged(SkillType::Awareness, *_controls.AWARENESS_LBL, *_controls.AWARENESS_POINTS_BTN, selected); }); _controls.PERSUADE_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::Persuade, selected); + onSkillLabelSelectionChanged(SkillType::Persuade, *_controls.PERSUADE_LBL, *_controls.PERSUADE_POINTS_BTN, selected); }); _controls.REPAIR_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::Repair, selected); + onSkillLabelSelectionChanged(SkillType::Repair, *_controls.REPAIR_LBL, *_controls.REPAIR_POINTS_BTN, selected); }); _controls.SECURITY_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::Security, selected); + onSkillLabelSelectionChanged(SkillType::Security, *_controls.SECURITY_LBL, *_controls.SECURITY_POINTS_BTN, selected); }); _controls.TREAT_INJURY_LBL->setOnSelectionChanged([this](bool selected) { - onSkillLabelSelectionChanged(SkillType::TreatInjury, selected); + onSkillLabelSelectionChanged(SkillType::TreatInjury, *_controls.TREAT_INJURY_LBL, *_controls.TREAT_INJURY_POINTS_BTN, selected); }); _controls.COM_MINUS_BTN->setOnClick([this]() { @@ -274,7 +274,10 @@ void CharGenSkills::onPlusButtonClick(SkillType skill) { refreshControls(); } -void CharGenSkills::onSkillLabelSelectionChanged(SkillType skill, bool selected) { +void CharGenSkills::onSkillLabelSelectionChanged(SkillType skill, Label &label, Button &points, bool selected) { + // The hovered row's name and points pulse. + label.setTextPulsing(selected); + points.setTextPulsing(selected); if (!selected) return; diff --git a/src/libs/game/gui/computer.cpp b/src/libs/game/gui/computer.cpp index 2a0b2a50c..fdce4ca82 100644 --- a/src/libs/game/gui/computer.cpp +++ b/src/libs/game/gui/computer.cpp @@ -17,7 +17,17 @@ #include "reone/game/gui/computer.h" +#include "reone/audio/di/services.h" +#include "reone/audio/mixer.h" +#include "reone/game/d20/skills.h" +#include "reone/game/di/services.h" #include "reone/game/game.h" +#include "reone/game/object/creature.h" +#include "reone/game/party.h" +#include "reone/resource/2da.h" +#include "reone/resource/di/services.h" +#include "reone/resource/provider/2das.h" +#include "reone/resource/provider/audioclips.h" using namespace reone::audio; @@ -29,20 +39,23 @@ namespace reone { namespace game { -static void setVisible(const std::shared_ptr