[Scummvm-git-logs] scummvm master -> bf6e75451b01c7af55ea7dd0714b8d8bb60858e3
bluegr
noreply at scummvm.org
Thu May 21 23:50:44 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
bf6e75451b NANCY: Initial work on the conversation popup for Nancy 10+
Commit: bf6e75451b01c7af55ea7dd0714b8d8bb60858e3
https://github.com/scummvm/scummvm/commit/bf6e75451b01c7af55ea7dd0714b8d8bb60858e3
Author: Dukiverse (thedukemc1 at gmail.com)
Date: 2026-05-22T02:50:41+03:00
Commit Message:
NANCY: Initial work on the conversation popup for Nancy 10+
Changed paths:
A engines/nancy/ui/conversationpopup.cpp
A engines/nancy/ui/conversationpopup.h
engines/nancy/action/conversation.cpp
engines/nancy/module.mk
engines/nancy/state/scene.cpp
engines/nancy/state/scene.h
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index d9f5cf1b2ae..7f9c4e4efd5 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -256,13 +256,21 @@ void ConversationSound::execute() {
case kRun:
if (!_hasDrawnTextbox) {
_hasDrawnTextbox = true;
- auto *textboxData = GetEngineData(TBOX);
- assert(textboxData);
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().setOverrideFont(textboxData->conversationFontID);
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ NancySceneState.getConversationPopup().open();
- if (ConfMan.getBool("subtitles")) {
- NancySceneState.getTextbox().addTextLine(_text);
+ if (ConfMan.getBool("subtitles")) {
+ NancySceneState.getConversationPopup().addTextLine(_text);
+ }
+ } else {
+ auto *textboxData = GetEngineData(TBOX);
+ assert(textboxData);
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().setOverrideFont(textboxData->conversationFontID);
+
+ if (ConfMan.getBool("subtitles")) {
+ NancySceneState.getTextbox().addTextLine(_text);
+ }
}
Common::Array<uint> responsesToAdd;
@@ -317,8 +325,16 @@ void ConversationSound::execute() {
responsesToAdd.push_back(i);
}
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ NancySceneState.getConversationPopup().setResponseStart();
+ }
+
for (uint i : responsesToAdd) {
- NancySceneState.getTextbox().addTextLine(_responses[i].text);
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ NancySceneState.getConversationPopup().addTextLine(_responses[i].text);
+ } else {
+ NancySceneState.getTextbox().addTextLine(_responses[i].text);
+ }
_responses[i].isOnScreen = true;
}
}
@@ -406,6 +422,10 @@ void ConversationSound::execute() {
}
}
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ NancySceneState.getConversationPopup().close();
+ }
+
finishExecution();
}
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 35d20548ffe..445597b25bb 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -58,6 +58,7 @@ MODULE_OBJS = \
ui/button.o \
ui/clock.o \
ui/cellphonepopup.o \
+ ui/conversationpopup.o \
ui/inventorybox.o \
ui/inventorypopup.o \
ui/notebookpopup.o \
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 042c8a764b4..977431f5888 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -625,6 +625,7 @@ void Scene::registerGraphics() {
_inventoryPopup.registerGraphics();
_notebookPopup.registerGraphics();
_cellPhonePopup.registerGraphics();
+ _conversationPopup.registerGraphics();
}
_hotspotDebug.registerGraphics();
@@ -1158,6 +1159,7 @@ void Scene::handleInput() {
// the popup that overlapped the textbox area could accidentally pick
// a conversation response.
if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ _conversationPopup.handleInput(input);
_inventoryPopup.handleInput(input);
_notebookPopup.handleInput(input);
_cellPhonePopup.handleInput(input);
@@ -1295,6 +1297,7 @@ void Scene::initStaticData() {
_inventoryPopup.init();
_notebookPopup.init();
_cellPhonePopup.init();
+ _conversationPopup.init();
}
// Init buttons
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 1474f3ca118..e9118c381ea 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -39,6 +39,7 @@
#include "engines/nancy/ui/inventorypopup.h"
#include "engines/nancy/ui/notebookpopup.h"
#include "engines/nancy/ui/cellphonepopup.h"
+#include "engines/nancy/ui/conversationpopup.h"
namespace Common {
class SeekableReadStream;
@@ -177,6 +178,7 @@ public:
UI::InventoryPopup &getInventoryPopup() { return _inventoryPopup; }
UI::NotebookPopup &getNotebookPopup() { return _notebookPopup; }
UI::CellPhonePopup &getCellPhonePopup() { return _cellPhonePopup; }
+ UI::ConversationPopup &getConversationPopup() { return _conversationPopup; }
UI::Clock *getClock();
UI::Taskbar *getTaskbar() { return _taskbar; }
@@ -279,6 +281,7 @@ private:
UI::InventoryPopup _inventoryPopup;
UI::NotebookPopup _notebookPopup;
UI::CellPhonePopup _cellPhonePopup;
+ UI::ConversationPopup _conversationPopup;
UI::Button *_menuButton;
UI::Button *_helpButton;
diff --git a/engines/nancy/ui/conversationpopup.cpp b/engines/nancy/ui/conversationpopup.cpp
new file mode 100644
index 00000000000..dbc44d6abc3
--- /dev/null
+++ b/engines/nancy/ui/conversationpopup.cpp
@@ -0,0 +1,317 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/resource.h"
+
+#include "engines/nancy/state/scene.h"
+
+#include "engines/nancy/ui/conversationpopup.h"
+
+namespace Nancy {
+namespace UI {
+
+static const uint16 kHypertextSurfaceHeight = 4096;
+
+ConversationPopup::ConversationPopup() :
+ RenderObject(12), // z=12: above the viewport (6) and the taskbar (7).
+ _uicoData(nullptr),
+ _tboxData(nullptr),
+ _highlightRObj(13) {}
+
+void ConversationPopup::init() {
+ _uicoData = GetEngineData(UICO);
+ assert(_uicoData);
+ _tboxData = GetEngineData(TBOX);
+ assert(_tboxData);
+
+ g_nancy->_resource->loadImage(_uicoData->header.imageName, _overlayImage);
+
+ Common::Rect popupRect = _uicoData->header.normalDestRect;
+ if (_uicoData->header.overlayInGameFrame) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ popupRect.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ moveTo(popupRect);
+
+ Common::Rect bounds = _screenPosition;
+ bounds.moveTo(0, 0);
+ _drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
+
+ initSurfaces(_uicoData->textRect.width(), kHypertextSurfaceHeight,
+ g_nancy->_graphics->getInputPixelFormat(), 0, 0);
+
+ _highlightRObj.moveTo(popupRect);
+
+ drawBackground();
+ setTransparent(false);
+ setVisible(false);
+
+ RenderObject::init();
+}
+
+void ConversationPopup::registerGraphics() {
+ RenderObject::registerGraphics();
+ _highlightRObj.registerGraphics();
+ _highlightRObj.setVisible(false);
+}
+
+void ConversationPopup::open() {
+ // Always clear, even if already visible â the conversation action
+ // calls open() on every new dialogue state (matching Textbox::clear()
+ // behavior for Nancy 1-9).
+ _scrollPos = 0.0f;
+ _scrollbarDragging = false;
+ _scrollbarHovered = false;
+ _responseStartIdx = 0;
+
+ HypertextParser::clear();
+ _drawnTextHeight = 0;
+ _fullSurface.fillRect(Common::Rect(0, 0, _fullSurface.w, _fullSurface.h), 0);
+ _textHighlightSurface.fillRect(Common::Rect(0, 0, _textHighlightSurface.w, _textHighlightSurface.h), 0);
+
+ drawBackground();
+ drawScrollbar(0);
+ setVisible(true);
+}
+
+void ConversationPopup::addTextLine(const Common::String &text) {
+ HypertextParser::addTextLine(text);
+
+ drawBackground();
+ drawContent();
+ drawScrollbar(0);
+}
+
+void ConversationPopup::close() {
+ setVisible(false);
+ _highlightRObj.setVisible(false);
+}
+
+void ConversationPopup::drawBackground() {
+ _drawSurface.blitFrom(_overlayImage, _uicoData->header.normalSrcRect,
+ Common::Point(0, 0));
+ _needsRedraw = true;
+}
+
+void ConversationPopup::drawContent() {
+ _drawnTextHeight = 0;
+ _numDrawnLines = 0;
+ _hotspots.clear();
+ _fullSurface.fillRect(Common::Rect(0, 0, _fullSurface.w, _fullSurface.h), 0);
+ _textHighlightSurface.fillRect(Common::Rect(0, 0, _textHighlightSurface.w, _textHighlightSurface.h), 0);
+
+ // TODO: Padding doesn't match the original game. leftOffset returns an
+ // unexpected value for Nancy 10+, so upOffset is used as a fallback for
+ // both axes until the TBOX layout for Nancy 10+ is better understood.
+
+ // TODO: Line spacing is tighter than the original game.
+
+ Common::Rect textBounds(0, 0, _fullSurface.w, _fullSurface.h);
+ textBounds.top += _tboxData->upOffset;
+ textBounds.left += _tboxData->upOffset;
+
+ drawAllText(textBounds, 0, _tboxData->conversationFontID, _tboxData->highlightConversationFontID);
+
+ Common::Rect localTextRect = _uicoData->textRect;
+ localTextRect.translate(-_uicoData->header.normalDestRect.left,
+ -_uicoData->header.normalDestRect.top);
+
+ const uint16 inner = getInnerHeight();
+ const uint16 outer = localTextRect.height();
+ int scrollY = 0;
+ if (inner > outer) {
+ scrollY = MIN<int>((int)(_scrollPos * (inner - outer)),
+ MAX<int>(0, _fullSurface.h - outer));
+ }
+
+ _drawSurface.blitFrom(_fullSurface,
+ Common::Rect(0, scrollY, _fullSurface.w, scrollY + outer),
+ Common::Point(localTextRect.left, localTextRect.top));
+
+ _needsRedraw = true;
+}
+
+uint16 ConversationPopup::getInnerHeight() const {
+ return _drawnTextHeight + _tboxData->upOffset;
+}
+
+Common::Rect ConversationPopup::toPopupLocal(const Common::Rect &chunkRect, bool useGameFrame) const {
+ // Chunk-space rects need the viewport offset applied when their own
+ // destUsesGameFrameOffset flag is set, then the popup's screen
+ // position subtracted to get surface-local coordinates.
+ Common::Rect r = chunkRect;
+ if (useGameFrame) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ r.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ r.translate(-_screenPosition.left, -_screenPosition.top);
+ return r;
+}
+
+Common::Point ConversationPopup::popupLocalMouse(const Common::Point &screenMouse) const {
+ return Common::Point(screenMouse.x - _screenPosition.left,
+ screenMouse.y - _screenPosition.top);
+}
+
+Common::Rect ConversationPopup::computeThumbRect() const {
+ const UISliderRecord &sl = _uicoData->header.slider;
+ if (!_uicoData->header.sliderEnabled || sl.destRect.isEmpty() || sl.sourceRects[0].isEmpty()) {
+ return Common::Rect();
+ }
+
+ const int trackHeight = sl.destRect.height();
+ const int thumbHeight = sl.sourceRects[0].height();
+ const int travel = MAX(0, trackHeight - thumbHeight);
+ const int thumbY = sl.destRect.top + (int)(_scrollPos * travel);
+
+ Common::Rect chunkThumb(sl.destRect.left, thumbY,
+ sl.destRect.left + sl.sourceRects[0].width(),
+ thumbY + thumbHeight);
+ return toPopupLocal(chunkThumb, sl.destUsesGameFrameOffset != 0);
+}
+
+void ConversationPopup::drawScrollbar(uint state) {
+ const UISliderRecord &sl = _uicoData->header.slider;
+ if (!_uicoData->header.sliderEnabled) {
+ return;
+ }
+
+ const Common::Rect thumb = computeThumbRect();
+ if (thumb.isEmpty()) {
+ return;
+ }
+
+ _drawSurface.blitFrom(_overlayImage, sl.sourceRects[state],
+ Common::Point(thumb.left, thumb.top));
+}
+
+void ConversationPopup::handleInput(NancyInput &input) {
+ if (!_isVisible) {
+ return;
+ }
+
+ const Common::Point localMouse = popupLocalMouse(input.mousePos);
+ const UISliderRecord &slider = _uicoData->header.slider;
+
+ if (_uicoData->header.sliderEnabled) {
+ const Common::Rect trackLocal = toPopupLocal(slider.destRect, slider.destUsesGameFrameOffset != 0);
+ const int trackHeight = trackLocal.height();
+ const int thumbHeight = slider.sourceRects[0].height();
+ const int travel = MAX(0, trackHeight - thumbHeight);
+ const int thumbY = trackLocal.top + (int)(_scrollPos * travel);
+ Common::Rect thumbLocal(trackLocal.left, thumbY,
+ trackLocal.left + slider.sourceRects[0].width(),
+ thumbY + thumbHeight);
+ const bool overThumb = thumbLocal.contains(localMouse);
+
+ if (_scrollbarDragging) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ const int newThumbTop = localMouse.y - _scrollbarGrabOffset;
+ const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
+ _scrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
+
+ drawBackground();
+ drawContent();
+ drawScrollbar(2);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _scrollbarDragging = false;
+ drawScrollbar(overThumb ? 1 : 0);
+ _needsRedraw = true;
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ if (overThumb != _scrollbarHovered) {
+ _scrollbarHovered = overThumb;
+ drawScrollbar(overThumb ? 1 : 0);
+ _needsRedraw = true;
+ }
+
+ if (overThumb) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (slider.isDraggable && (input.input & NancyInput::kLeftMouseButtonDown)) {
+ _scrollbarDragging = true;
+ _scrollbarGrabOffset = localMouse.y - thumbY;
+ drawScrollbar(2);
+ _needsRedraw = true;
+ input.eatMouseInput();
+ return;
+ }
+ }
+ }
+
+ // Response hotspot handling â mirrors Textbox::handleInput().
+ Common::Rect localTextRect = _uicoData->textRect;
+ localTextRect.translate(-_uicoData->header.normalDestRect.left,
+ -_uicoData->header.normalDestRect.top);
+
+ const uint16 inner = getInnerHeight();
+ const uint16 outer = localTextRect.height();
+ const int scrollY = inner > outer ? (int)(_scrollPos * (inner - outer)) : 0;
+
+ bool hasHighlight = false;
+ for (uint i = _responseStartIdx; i < _hotspots.size(); ++i) {
+ Common::Rect hotspot = _hotspots[i];
+ hotspot.translate(localTextRect.left, localTextRect.top - scrollY);
+ Common::Rect hotspotOnScreen = convertToScreen(hotspot).findIntersectingRect(_screenPosition);
+ if (hotspotOnScreen.contains(input.mousePos)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ _highlightRObj.setVisible(true);
+ Common::Rect hotspotInside = convertToLocal(hotspotOnScreen);
+ hotspotInside.translate(-localTextRect.left, scrollY - localTextRect.top);
+ _highlightRObj._drawSurface.create(_textHighlightSurface, hotspotInside);
+ _highlightRObj.moveTo(hotspotOnScreen);
+ hasHighlight = true;
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ input.input &= ~NancyInput::kLeftMouseButtonUp;
+ NancySceneState.clearLogicConditions();
+ NancySceneState.setLogicCondition(i - _responseStartIdx, g_nancy->_true);
+ }
+
+ break;
+ }
+ }
+
+ if (!hasHighlight && _highlightRObj.isVisible()) {
+ _highlightRObj.setVisible(false);
+ }
+
+ // Swallow clicks on the popup itself so they don't fall through.
+ if (_screenPosition.contains(input.mousePos)) {
+ input.eatMouseInput();
+ }
+}
+
+} // End of namespace UI
+} // End of namespace Nancy
diff --git a/engines/nancy/ui/conversationpopup.h b/engines/nancy/ui/conversationpopup.h
new file mode 100644
index 00000000000..50cf6a0f22d
--- /dev/null
+++ b/engines/nancy/ui/conversationpopup.h
@@ -0,0 +1,99 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_UI_CONVERSATIONPOPUP_H
+#define NANCY_UI_CONVERSATIONPOPUP_H
+
+#include "engines/nancy/misc/hypertext.h"
+#include "engines/nancy/renderobject.h"
+
+namespace Nancy {
+
+struct NancyInput;
+struct TBOX;
+struct UICO;
+
+namespace UI {
+
+// Nancy 10+ conversation popup. Replaces the legacy Textbox for dialogue
+// display. The conversation action calls open(), addTextLine(), and close()
+// to drive the popup lifecycle. Response hotspots and hover highlights
+// mirror the Textbox behavior but render inside a UICO-driven overlay window.
+class ConversationPopup : public RenderObject, public Misc::HypertextParser {
+public:
+ ConversationPopup();
+ ~ConversationPopup() override = default;
+
+ void init() override;
+ void registerGraphics() override;
+ void handleInput(NancyInput &input);
+
+ // Called by ConversationSound::execute() at each new dialogue state.
+ // Always clears previous content, matching Textbox::clear() for Nancy 1-9.
+ void open();
+
+ // Called once per text line after open() â NPC speech first, then
+ // each response option. Triggers a redraw after each addition.
+ void addTextLine(const Common::String &text);
+
+ // Called at kActionTrigger just before finishExecution(). Hides the popup.
+ void close();
+
+ // Called after NPC text lines have been added, before response lines.
+ // Records the hotspot offset so response clicks map to the correct index.
+ void setResponseStart() { _responseStartIdx = (uint)_hotspots.size(); }
+
+private:
+ void drawBackground();
+ void drawContent();
+ void drawScrollbar(uint state);
+ uint16 getInnerHeight() const;
+
+ // Returns the popup-local bounding rect of the scrollbar thumb at
+ // the current scroll position.
+ Common::Rect computeThumbRect() const;
+
+ // Convert a chunk-space rect into popup-local coordinates.
+ Common::Rect toPopupLocal(const Common::Rect &chunkRect, bool useGameFrame) const;
+ Common::Point popupLocalMouse(const Common::Point &screenMouse) const;
+
+ const UICO *_uicoData;
+ const TBOX *_tboxData;
+
+ Graphics::ManagedSurface _overlayImage;
+
+ // Separate render object for drawing hovered response text,
+ // mirroring the Textbox::_highlightRObj approach.
+ RenderObject _highlightRObj;
+
+ // Scrollbar state, driven by header.slider.
+ float _scrollPos = 0.0f;
+ bool _scrollbarDragging = false;
+ bool _scrollbarHovered = false;
+ int _scrollbarGrabOffset = 0;
+
+ uint _responseStartIdx = 0;
+};
+
+} // End of namespace UI
+} // End of namespace Nancy
+
+#endif // NANCY_UI_CONVERSATIONPOPUP_H
More information about the Scummvm-git-logs
mailing list